mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cb428ed71 | |||
|
|
ec392ee384 | ||
|
|
d037cf6d44 | ||
|
|
6964e9b655 | ||
|
|
7a032bf947 | ||
|
|
9a91e5ef5b | ||
|
|
5b58697fce | ||
|
|
b14cebe832 | ||
|
|
4306195ee5 | ||
|
|
ac9c6e79d9 | ||
|
|
ed035c65c1 | ||
| dc8bc9b306 | |||
| 454dd57007 | |||
| b396a6d450 | |||
| 7f584a46eb | |||
| 27518c0408 | |||
|
|
d2a3801dac | ||
| 563564ac8d | |||
| 87c87ad231 | |||
| 30515d1907 | |||
|
|
bd0a8ea447 | ||
|
|
1db1e6dbba | ||
|
|
9040aa9fba | ||
|
|
3a5f1eb266 | ||
|
|
43e927430a | ||
| e1b0918a52 | |||
| c86b2eb31b | |||
|
|
47b3f3e881 | ||
|
|
7caee587b4 | ||
|
|
28ae564e59 | ||
|
|
90dee8402d | ||
|
|
8b560e55cb | ||
|
|
3080531503 | ||
|
|
fae0088533 | ||
|
|
db9286a369 | ||
|
|
420fb29318 | ||
|
|
433a6edd3b | ||
|
|
fa81231f78 | ||
|
|
c474cf0af2 | ||
|
|
e38eb5cdff | ||
|
|
7bacf89840 | ||
|
|
4622e90995 | ||
|
|
fa8c2b11e6 | ||
|
|
2123b747af | ||
|
|
e65233d09f | ||
|
|
7e81bf6b8b | ||
|
|
c4614aa006 | ||
|
|
79a657b9f5 | ||
|
|
0164f29c1e | ||
|
|
8db56366df | ||
|
|
de0a1e4c73 | ||
|
|
3670ce7221 | ||
|
|
101ed737ab |
@@ -0,0 +1,51 @@
|
||||
"""Rename field
|
||||
|
||||
迁移 ID: 09d4bb60160d
|
||||
父迁移: b9d65badc713
|
||||
创建时间: 2024-04-23 23:42:04.541672
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = '09d4bb60160d'
|
||||
down_revision: str | Sequence[str] | None = 'b9d65badc713'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.alter_column('create_time', new_column_name='update_time', existing_type=sa.DateTime())
|
||||
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_create_time')
|
||||
op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'),
|
||||
'nonebot_plugin_tetris_stats_iorank',
|
||||
['update_time'],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.alter_column('update_time', new_column_name='create_time')
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'))
|
||||
op.create_index(
|
||||
'ix_nonebot_plugin_tetris_stats_iorank_create_time',
|
||||
'nonebot_plugin_tetris_stats_iorank',
|
||||
['create_time'],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add field
|
||||
|
||||
迁移 ID: 0d50142b780f
|
||||
父迁移: 09d4bb60160d
|
||||
创建时间: 2024-04-24 14:55:08.064098
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = '0d50142b780f'
|
||||
down_revision: str | Sequence[str] | None = '09d4bb60160d'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('file_hash', sa.String(length=128), nullable=True))
|
||||
batch_op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'), ['file_hash'], unique=False
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'))
|
||||
batch_op.drop_column('file_hash')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -30,7 +30,7 @@ class PydanticType(TypeDecorator):
|
||||
if isinstance(value, str | bytes):
|
||||
for i in self.get_model():
|
||||
try:
|
||||
return i.parse_raw(value)
|
||||
return i.model_validate_json(value)
|
||||
except ValidationError: # noqa: PERF203
|
||||
...
|
||||
raise TypeError
|
||||
|
||||
@@ -151,19 +151,19 @@ async def _(matcher: Matcher, rank: Rank):
|
||||
.where(IORank.rank == rank)
|
||||
.order_by(
|
||||
func.abs(
|
||||
func.julianday(IORank.create_time)
|
||||
- func.julianday(latest_data.create_time - timedelta(hours=24))
|
||||
func.julianday(IORank.update_time)
|
||||
- func.julianday(latest_data.update_time - timedelta(hours=24))
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
).one()
|
||||
message = ''
|
||||
if (datetime.now(UTC) - latest_data.create_time.replace(tzinfo=UTC)) > timedelta(hours=7):
|
||||
if (datetime.now(UTC) - latest_data.update_time.replace(tzinfo=UTC)) > timedelta(hours=7):
|
||||
message += 'Warning: 数据超过7小时未更新, 请联系Bot主人查看后台\n'
|
||||
message += f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
|
||||
if compare_data.id != latest_data.id:
|
||||
message += f'对比 {(latest_data.create_time-compare_data.create_time).total_seconds()/3600:.2f} 小时前趋势: {f"↑{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"↓{-difference:.2f}" if difference < 0 else "→"}'
|
||||
message += f'对比 {(latest_data.update_time-compare_data.update_time).total_seconds()/3600:.2f} 小时前趋势: {f"↑{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"↓{-difference:.2f}" if difference < 0 else "→"}'
|
||||
else:
|
||||
message += '暂无对比数据'
|
||||
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
|
||||
@@ -188,7 +188,7 @@ async def _(matcher: Matcher, rank: Rank):
|
||||
f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n'
|
||||
f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n'
|
||||
'\n'
|
||||
f'数据更新时间: {latest_data.create_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
f'数据更新时间: {latest_data.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
)
|
||||
await matcher.finish(message)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from aiocache import Cache as ACache # type: ignore[import-untyped]
|
||||
from nonebot.compat import type_validate_json
|
||||
from nonebot.log import logger
|
||||
from pydantic import parse_raw_as
|
||||
|
||||
from ...utils.request import Request
|
||||
from .schemas.base import FailedModel, SuccessModel
|
||||
@@ -18,7 +18,7 @@ class Cache:
|
||||
cached_data = await cls.cache.get(url)
|
||||
if cached_data is None:
|
||||
response_data = await Request.request(url)
|
||||
parsed_data: SuccessModel | FailedModel = parse_raw_as(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
|
||||
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
|
||||
if isinstance(parsed_data, SuccessModel):
|
||||
await cls.cache.add(
|
||||
url,
|
||||
|
||||
@@ -23,9 +23,8 @@ class IORank(MappedAsDataclass, Model):
|
||||
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
create_time: Mapped[datetime] = mapped_column(
|
||||
update_time: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=lambda: datetime.now(tz=UTC),
|
||||
index=True,
|
||||
init=False,
|
||||
)
|
||||
file_hash: Mapped[str | None] = mapped_column(String(128), index=True)
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from hashlib import sha512
|
||||
from math import floor
|
||||
from re import match
|
||||
from statistics import mean
|
||||
from typing import Literal
|
||||
|
||||
from aiofiles import open
|
||||
from nonebot import get_driver
|
||||
from nonebot.compat import type_validate_json
|
||||
from nonebot.utils import run_sync
|
||||
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
||||
from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyped]
|
||||
from nonebot_plugin_orm import get_session
|
||||
from pydantic import parse_raw_as
|
||||
from sqlalchemy import select
|
||||
from zstandard import ZstdCompressor
|
||||
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
||||
@@ -96,7 +101,7 @@ class Processor(ProcessorMeta):
|
||||
self.raw_response.user_info = await Cache.get(
|
||||
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
|
||||
)
|
||||
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||
if isinstance(user_info, InfoFailed):
|
||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||
self.processed_data.user_info = user_info
|
||||
@@ -108,7 +113,7 @@ class Processor(ProcessorMeta):
|
||||
self.raw_response.user_records = await Cache.get(
|
||||
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records'])
|
||||
)
|
||||
user_records: UserRecords = parse_raw_as(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
|
||||
user_records: UserRecords = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
|
||||
if isinstance(user_records, RecordsFailed):
|
||||
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
|
||||
self.processed_data.user_records = user_records
|
||||
@@ -158,7 +163,10 @@ class Processor(ProcessorMeta):
|
||||
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
|
||||
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
|
||||
async def get_io_rank_data() -> None:
|
||||
league_all: LeagueAll = parse_raw_as(LeagueAll, await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))) # type: ignore[arg-type]
|
||||
league_all: LeagueAll = type_validate_json(
|
||||
LeagueAll, # type: ignore[arg-type]
|
||||
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
|
||||
)
|
||||
if isinstance(league_all, LeagueAllFailed):
|
||||
raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
|
||||
|
||||
@@ -185,6 +193,10 @@ async def get_io_rank_data() -> None:
|
||||
user = sort(users, field)
|
||||
return User(ID=user.id, name=user.username).dict(), field(user)
|
||||
|
||||
data_hash: str | None = await run_sync((await run_sync(sha512)(data)).hexdigest)()
|
||||
async with open(get_data_file('nonebot_plugin_tetris_stats', f'{data_hash}.json.zst'), mode='wb') as file:
|
||||
await file.write(await run_sync(ZstdCompressor(level=12, threads=-1).compress)(data))
|
||||
|
||||
users = [i for i in league_all.data.users if isinstance(i, LeagueAllUser)]
|
||||
rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list)
|
||||
for i in users:
|
||||
@@ -208,6 +220,8 @@ async def get_io_rank_data() -> None:
|
||||
high_pps=(build_extremes_data(rank_users, pps, _max)),
|
||||
high_apm=(build_extremes_data(rank_users, apm, _max)),
|
||||
high_vs=(build_extremes_data(rank_users, vs, _max)),
|
||||
update_time=league_all.cache.cached_at,
|
||||
file_hash=data_hash,
|
||||
)
|
||||
)
|
||||
async with get_session() as session:
|
||||
@@ -218,6 +232,6 @@ async def get_io_rank_data() -> None:
|
||||
@driver.on_startup
|
||||
async def _() -> None:
|
||||
async with get_session() as session:
|
||||
latest_time = await session.scalar(select(IORank.create_time).order_by(IORank.id.desc()).limit(1))
|
||||
latest_time = await session.scalar(select(IORank.update_time).order_by(IORank.id.desc()).limit(1))
|
||||
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
||||
await get_io_rank_data()
|
||||
|
||||
@@ -28,20 +28,20 @@ class SuccessModel(BaseSuccessModel):
|
||||
league: League
|
||||
supporter: bool
|
||||
verified: bool
|
||||
country: str | None
|
||||
country: str | None = None
|
||||
|
||||
class InvalidUser(BaseModel):
|
||||
class League(BaseModel):
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
rating: float
|
||||
glicko: float | None
|
||||
rd: float | None
|
||||
glicko: float | None = None
|
||||
rd: float | None = None
|
||||
rank: Rank
|
||||
bestrank: Rank
|
||||
apm: float | None
|
||||
pps: float | None
|
||||
vs: float | None
|
||||
apm: float | None = None
|
||||
pps: float | None = None
|
||||
vs: float | None = None
|
||||
decaying: bool
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
|
||||
@@ -14,7 +14,7 @@ class SuccessModel(BaseSuccessModel):
|
||||
class Badge(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
ts: datetime | None
|
||||
ts: datetime | None = None
|
||||
|
||||
class NeverPlayedLeague(BaseModel):
|
||||
gamesplayed: Literal[0]
|
||||
@@ -60,8 +60,8 @@ class SuccessModel(BaseSuccessModel):
|
||||
bestrank: Rank
|
||||
standing: int
|
||||
standing_local: int
|
||||
next_rank: Rank | None
|
||||
prev_rank: Rank | None
|
||||
next_rank: Rank | None = None
|
||||
prev_rank: Rank | None = None
|
||||
next_at: int
|
||||
prev_at: int
|
||||
percentile: float
|
||||
@@ -70,7 +70,7 @@ class SuccessModel(BaseSuccessModel):
|
||||
rd: float
|
||||
apm: float
|
||||
pps: float
|
||||
vs: float | None
|
||||
vs: float | None = None
|
||||
decaying: bool
|
||||
|
||||
class Connections(BaseModel):
|
||||
@@ -78,41 +78,41 @@ class SuccessModel(BaseSuccessModel):
|
||||
id: str
|
||||
username: str
|
||||
|
||||
discord: Discord | None
|
||||
discord: Discord | None = None
|
||||
|
||||
class Distinguishment(BaseModel):
|
||||
type: str # noqa: A003
|
||||
type: str
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
|
||||
ts: datetime | None
|
||||
botmaster: str | None
|
||||
ts: datetime | None = None
|
||||
botmaster: str | None = None
|
||||
badges: list[Badge]
|
||||
xp: float
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
gametime: float
|
||||
country: str | None
|
||||
badstanding: bool | None
|
||||
supporter: bool | None # osk说是必有, 但实际上不是 fk osk
|
||||
country: str | None = None
|
||||
badstanding: bool | None = None
|
||||
supporter: bool | None = None # osk说是必有, 但实际上不是 fk osk
|
||||
supporter_tier: int
|
||||
verified: bool
|
||||
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
|
||||
avatar_revision: int | None
|
||||
avatar_revision: int | None = None
|
||||
"""This user's avatar ID. Get their avatar at
|
||||
|
||||
https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }"""
|
||||
banner_revision: int | None
|
||||
banner_revision: int | None = None
|
||||
"""This user's banner ID. Get their banner at
|
||||
|
||||
https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }
|
||||
|
||||
Ignore this field if the user is not a supporter."""
|
||||
bio: str | None
|
||||
bio: str | None = None
|
||||
connections: Connections
|
||||
friend_count: int | None
|
||||
distinguishment: Distinguishment | None
|
||||
friend_count: int | None = None
|
||||
distinguishment: Distinguishment | None = None
|
||||
|
||||
user: User
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ class EndContext(BaseModel):
|
||||
zero: bool
|
||||
locked: bool
|
||||
prev: int
|
||||
frameoffset: int | None
|
||||
frameoffset: int | None = None
|
||||
|
||||
class Clears(BaseModel):
|
||||
singles: int
|
||||
doubles: int
|
||||
triples: int
|
||||
quads: int
|
||||
pentas: int | None
|
||||
pentas: int | None = None
|
||||
realtspins: int
|
||||
minitspins: int
|
||||
minitspinsingles: int
|
||||
@@ -33,7 +33,7 @@ class EndContext(BaseModel):
|
||||
class Garbage(BaseModel):
|
||||
sent: int
|
||||
received: int
|
||||
attack: int | None
|
||||
attack: int | None = None
|
||||
cleared: int
|
||||
|
||||
class Finesse(BaseModel):
|
||||
@@ -46,18 +46,18 @@ class EndContext(BaseModel):
|
||||
level_lines: int
|
||||
level_lines_needed: int
|
||||
inputs: int
|
||||
holds: int | None
|
||||
holds: int | None = None
|
||||
time: Time
|
||||
score: int
|
||||
zenlevel: int | None
|
||||
zenprogress: int | None
|
||||
zenlevel: int | None = None
|
||||
zenprogress: int | None = None
|
||||
level: int
|
||||
combo: int
|
||||
currentcombopower: int | None # WTF
|
||||
currentcombopower: int | None = None # WTF
|
||||
topcombo: int
|
||||
btb: int
|
||||
topbtb: int
|
||||
currentbtbchainpower: int | None # WTF * 2
|
||||
currentbtbchainpower: int | None = None # WTF * 2
|
||||
tspins: int
|
||||
piecesplaced: int
|
||||
clears: Clears
|
||||
@@ -79,7 +79,7 @@ class BaseModeRecord(BaseModel):
|
||||
replayid: str
|
||||
user: User
|
||||
ts: datetime
|
||||
ismulti: bool | None
|
||||
ismulti: bool | None = None
|
||||
endcontext: EndContext
|
||||
|
||||
class MultiRecord(BaseModel):
|
||||
@@ -92,21 +92,19 @@ class BaseModeRecord(BaseModel):
|
||||
replayid: str
|
||||
user: User
|
||||
ts: datetime
|
||||
ismulti: bool | None
|
||||
ismulti: bool | None = None
|
||||
endcontext: list[EndContext]
|
||||
|
||||
record: SoloRecord | MultiRecord | None
|
||||
rank: int | None
|
||||
record: SoloRecord | MultiRecord | None = None
|
||||
rank: int | None = None
|
||||
|
||||
|
||||
class SuccessModel(BaseSuccessModel):
|
||||
class Data(BaseModel):
|
||||
class Records(BaseModel):
|
||||
class Sprint(BaseModeRecord):
|
||||
...
|
||||
class Sprint(BaseModeRecord): ...
|
||||
|
||||
class Blitz(BaseModeRecord):
|
||||
...
|
||||
class Blitz(BaseModeRecord): ...
|
||||
|
||||
sprint: Sprint = Field(..., alias='40l')
|
||||
blitz: Blitz
|
||||
|
||||
@@ -2,7 +2,7 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from re import match
|
||||
from typing import Literal, NoReturn
|
||||
from typing import Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import etree
|
||||
@@ -87,10 +87,9 @@ class Processor(ProcessorMeta):
|
||||
self.processed_data.user_profile = self.raw_response.user_profile.decode()
|
||||
return self.processed_data.user_profile
|
||||
|
||||
async def check_user(self) -> None | NoReturn:
|
||||
async def check_user(self) -> None:
|
||||
if 'user not found!' in await self.get_user_profile():
|
||||
raise RequestError('用户不存在!')
|
||||
return None
|
||||
|
||||
async def get_user_name(self) -> str:
|
||||
"""获取用户名"""
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from typing import Literal
|
||||
|
||||
GAME_TYPE: Literal['TOS'] = 'TOS'
|
||||
BASE_URL = 'http://tos.teatube.cn:19970/'
|
||||
BASE_URL = {
|
||||
'https://teatube.cn:8888/',
|
||||
'http://cafuuchino1.studio26f.org:19970',
|
||||
'http://cafuuchino2.studio26f.org:19970',
|
||||
'http://cafuuchino3.studio26f.org:19970',
|
||||
'http://cafuuchino4.studio26f.org:19970',
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ from re import match
|
||||
from typing import Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from httpx import TimeoutException
|
||||
from nonebot.compat import type_validate_json
|
||||
from nonebot_plugin_orm import get_session
|
||||
from pydantic import parse_raw_as
|
||||
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError
|
||||
@@ -104,23 +105,31 @@ class Processor(ProcessorMeta):
|
||||
"""获取用户信息"""
|
||||
if self.processed_data.user_info is None:
|
||||
if self.user.teaid is not None:
|
||||
url = splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'getTeaIdInfo',
|
||||
f'?{urlencode({"teaId":self.user.teaid})}',
|
||||
]
|
||||
)
|
||||
url = [
|
||||
splice_url(
|
||||
[
|
||||
i,
|
||||
'getTeaIdInfo',
|
||||
f'?{urlencode({"teaId":self.user.teaid})}',
|
||||
]
|
||||
)
|
||||
for i in BASE_URL
|
||||
]
|
||||
else:
|
||||
url = splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'getUsernameInfo',
|
||||
f'?{urlencode({"username":self.user.name})}',
|
||||
]
|
||||
)
|
||||
self.raw_response.user_info = await Request.request(url)
|
||||
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||
url = [
|
||||
splice_url(
|
||||
[
|
||||
i,
|
||||
'getUsernameInfo',
|
||||
f'?{urlencode({"username":self.user.name})}',
|
||||
]
|
||||
)
|
||||
for i in BASE_URL
|
||||
]
|
||||
self.raw_response.user_info = await Request.failover_request(
|
||||
url, failover_code=[502], failover_exc=(TimeoutException,)
|
||||
)
|
||||
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||
if not isinstance(user_info, InfoSuccess):
|
||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||
self.processed_data.user_info = user_info
|
||||
@@ -132,16 +141,23 @@ class Processor(ProcessorMeta):
|
||||
other_parameter = {}
|
||||
params = urlencode(dict(sorted(other_parameter.items())))
|
||||
if self.processed_data.user_profile.get(params) is None:
|
||||
self.raw_response.user_profile[params] = await Request.request(
|
||||
splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'getProfile',
|
||||
f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}',
|
||||
]
|
||||
)
|
||||
self.raw_response.user_profile[params] = await Request.failover_request(
|
||||
[
|
||||
splice_url(
|
||||
[
|
||||
i,
|
||||
'getProfile',
|
||||
f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}',
|
||||
]
|
||||
)
|
||||
for i in BASE_URL
|
||||
],
|
||||
failover_code=[502],
|
||||
failover_exc=(TimeoutException,),
|
||||
)
|
||||
self.processed_data.user_profile[params] = UserProfile.model_validate_json(
|
||||
self.raw_response.user_profile[params]
|
||||
)
|
||||
self.processed_data.user_profile[params] = UserProfile.parse_raw(self.raw_response.user_profile[params])
|
||||
return self.processed_data.user_profile[params]
|
||||
|
||||
async def get_game_data(self) -> GameData | None:
|
||||
|
||||
@@ -18,6 +18,10 @@ class NeedCatchError(TetrisStatsError):
|
||||
class RequestError(NeedCatchError):
|
||||
"""请求错误"""
|
||||
|
||||
def __init__(self, message: str = '', *, status_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class MessageFormatError(NeedCatchError):
|
||||
"""用户发送的消息格式不正确"""
|
||||
|
||||
@@ -144,7 +144,7 @@ class TetrisMetricsProWithLPMADPM(TetrisMetricsBasicWithLPM, TetrisMetricsBaseWi
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
@@ -157,7 +157,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
@@ -170,7 +170,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: None = None,
|
||||
@@ -183,7 +183,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: None = None,
|
||||
@@ -196,7 +196,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
@@ -209,7 +209,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
@@ -222,7 +222,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
@@ -235,7 +235,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
@@ -248,7 +248,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
@@ -261,7 +261,7 @@ def get_metrics( # noqa: PLR0913
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
def get_metrics(
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections.abc import Sequence
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from aiofiles import open
|
||||
from httpx import AsyncClient, HTTPError
|
||||
from nonebot import get_driver
|
||||
from nonebot import get_driver, get_plugin_config
|
||||
from nonebot.log import logger
|
||||
from playwright.async_api import Response
|
||||
from ujson import JSONDecodeError, dumps, loads
|
||||
@@ -13,7 +14,7 @@ from .browser import BrowserManager
|
||||
from .exception import RequestError
|
||||
|
||||
driver = get_driver()
|
||||
config = Config.parse_obj(driver.config)
|
||||
config = get_plugin_config(Config)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@@ -116,7 +117,8 @@ class Request:
|
||||
response = await session.get(url, headers=cls._headers)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
raise RequestError(
|
||||
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
|
||||
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}',
|
||||
status_code=response.status_code,
|
||||
)
|
||||
if is_json:
|
||||
loads(response.content)
|
||||
@@ -127,3 +129,33 @@ class Request:
|
||||
if urlparse(url).netloc.lower().endswith('tetr.io'):
|
||||
return await cls._anti_cloudflare(url)
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def failover_request(
|
||||
cls,
|
||||
urls: Sequence[str],
|
||||
*,
|
||||
failover_code: Sequence[int],
|
||||
failover_exc: tuple[type[BaseException], ...],
|
||||
is_json: bool = True,
|
||||
) -> bytes:
|
||||
error_list: list[RequestError] = []
|
||||
for i in urls:
|
||||
logger.debug(f'尝试请求 {i}')
|
||||
try:
|
||||
return await cls.request(i, is_json=is_json)
|
||||
except RequestError as e:
|
||||
if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL
|
||||
error_list.append(e)
|
||||
continue
|
||||
# 如果状态码不在故障转移列表中, 则查找异常栈, 如果异常栈内有 failover_exc 内的异常类型, 则继续尝试下一个URL
|
||||
tb = e.__traceback__
|
||||
while tb is not None:
|
||||
if isinstance(tb.tb_frame.f_locals.get('exc_value'), failover_exc):
|
||||
error_list.append(e)
|
||||
break
|
||||
tb = tb.tb_next
|
||||
else:
|
||||
raise
|
||||
continue
|
||||
raise RequestError(f'所有地址皆不可用\n{error_list!r}')
|
||||
|
||||
2152
poetry.lock
generated
2152
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = 'nonebot-plugin-tetris-stats'
|
||||
version = '1.0.0.a13'
|
||||
version = '1.0.0.a16'
|
||||
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
|
||||
authors = ['scdhh <wallfjjd@gmail.com>']
|
||||
readme = 'README.md'
|
||||
@@ -10,36 +10,37 @@ license = 'AGPL-3.0'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = '^3.10'
|
||||
nonebot2 = '^2.1.3'
|
||||
nonebot2 = '^2.2.0'
|
||||
lxml = '^5.1.0'
|
||||
pandas = '>=1.4.3,<3.0.0'
|
||||
playwright = '^1.40.0'
|
||||
playwright = '^1.41.2'
|
||||
ujson = '^5.9.0'
|
||||
aiofiles = "^23.2.1"
|
||||
nonebot-plugin-orm = ">=0.1.1,<0.7.0"
|
||||
nonebot-plugin-localstore = "^0.5.1"
|
||||
httpx = "^0.26.0"
|
||||
nonebot-plugin-alconna = ">=0.30,<0.36"
|
||||
nonebot-plugin-apscheduler = "^0.3.0"
|
||||
nonebot-plugin-orm = ">=0.1.1,<0.8.0"
|
||||
nonebot-plugin-localstore = "^0.6.0"
|
||||
httpx = "^0.27.0"
|
||||
nonebot-plugin-alconna = ">=0.40"
|
||||
nonebot-plugin-apscheduler = "^0.4.0"
|
||||
aiocache = "^0.12.2"
|
||||
zstandard = "^0.22.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = '>=0.991,<1.9'
|
||||
mypy = '>=1.9'
|
||||
types-ujson = '^5.9.0'
|
||||
pandas-stubs = '>=1.5.2,<3.0.0'
|
||||
ruff = '>=0.0.239,<0.1.14'
|
||||
ruff = '>=0.3.0'
|
||||
types-aiofiles = "^23.2.0.20240106"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.1.3" }
|
||||
types-lxml = "^2023.3.28"
|
||||
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.7" }
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-adapter-satori = "^0.8.1"
|
||||
nonebot-adapter-kaiheila = "^0.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.2.0" }
|
||||
types-lxml = "^2024.2.9"
|
||||
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.8" }
|
||||
nonebot-adapter-onebot = "^2.4.1"
|
||||
nonebot-adapter-satori = "^0.11.3"
|
||||
nonebot-adapter-kaiheila = "^0.3.4"
|
||||
nonebot-adapter-discord = "^0.1.3"
|
||||
|
||||
[tool.poetry.group.debug.dependencies]
|
||||
objprint = '^0.2.2'
|
||||
viztracer = "^0.16.1"
|
||||
viztracer = "^0.16.2"
|
||||
|
||||
[build-system]
|
||||
requires = ['poetry-core>=1.0.0']
|
||||
|
||||
Reference in New Issue
Block a user