mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1611bf47fa | ||
|
|
e084cdb145 | ||
|
|
27258ab744 | ||
|
|
07324825e6 | ||
|
|
472becdfe0 | ||
|
|
bc87e4b16d | ||
|
|
28e2a46303 | ||
| 1324015d58 | |||
| e6eae023e7 | |||
| 67cfb07246 | |||
| 12145a614f | |||
| 0b07882a16 | |||
|
|
9073bf5d0b | ||
|
|
f4dd5fe76f | ||
|
|
1f44fc9884 | ||
|
|
44dee7f200 | ||
|
|
dc5ade6ffc | ||
|
|
05ce329976 | ||
|
|
43cabf2135 | ||
|
|
6767136850 | ||
|
|
65999b4625 | ||
|
|
9fde62ac9e | ||
|
|
c74d8b70aa | ||
|
|
0e29b38f9d | ||
|
|
d040c7dca2 | ||
|
|
68ace3a715 | ||
|
|
e63ac69e0f | ||
| 4afda62782 | |||
|
|
abf4410a00 | ||
| 88c2915251 | |||
| 546369241a | |||
| d59bccbd4d | |||
| 75a6989a7f | |||
| ad635bd37d | |||
|
|
b6d63c9e7f | ||
| 805da8cd36 | |||
| 4a13d7807a |
@@ -0,0 +1,65 @@
|
||||
"""Add redundant platform field
|
||||
|
||||
迁移 ID: 6c3206f90cc3
|
||||
父迁移: 9f6582279ce2
|
||||
创建时间: 2023-11-26 20:15:56.033892
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy.orm import Session
|
||||
from ujson import dumps, loads
|
||||
|
||||
revision: str = '6c3206f90cc3'
|
||||
down_revision: str | Sequence[str] | None = '9f6582279ce2'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
|
||||
Base = automap_base() # noqa: N806
|
||||
connection = op.get_bind()
|
||||
Base.prepare(autoload_with=connection)
|
||||
|
||||
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
|
||||
|
||||
with Session(connection) as session:
|
||||
for row in session.query(HistoricalData):
|
||||
platform = row.game_platform
|
||||
game_user = loads(row.game_user)
|
||||
processed_data = loads(row.processed_data)
|
||||
game_user['platform'] = platform
|
||||
processed_data['platform'] = platform
|
||||
row.game_user = dumps(game_user)
|
||||
row.processed_data = dumps(processed_data)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
|
||||
Base = automap_base() # noqa: N806
|
||||
connection = op.get_bind()
|
||||
Base.prepare(autoload_with=connection)
|
||||
|
||||
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
|
||||
|
||||
with Session(connection) as session:
|
||||
for row in session.query(HistoricalData):
|
||||
game_user = loads(row.game_user)
|
||||
processed_data = loads(row.processed_data)
|
||||
game_user.pop('platform', None)
|
||||
processed_data.pop('platform', None)
|
||||
row.game_user = dumps(game_user)
|
||||
row.processed_data = dumps(processed_data)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
@@ -45,8 +45,8 @@ def upgrade(name: str = '') -> None:
|
||||
sa.Column('game_platform', sa.String(length=32), nullable=False),
|
||||
sa.Column('command_type', sa.String(length=16), nullable=False),
|
||||
sa.Column('command_args', sa.JSON(), nullable=False),
|
||||
sa.Column('game_user', PydanticType(), nullable=False),
|
||||
sa.Column('processed_data', PydanticType(), nullable=False),
|
||||
sa.Column('game_user', PydanticType(list), nullable=False),
|
||||
sa.Column('processed_data', PydanticType(list), nullable=False),
|
||||
sa.Column('finish_time', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_historicaldata')),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Del old TOS bind data
|
||||
|
||||
迁移 ID: b9d65badc713
|
||||
父迁移: 6c3206f90cc3
|
||||
创建时间: 2023-12-30 00:27:40.991704
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
revision: str = 'b9d65badc713'
|
||||
down_revision: str | Sequence[str] | None = '6c3206f90cc3'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
|
||||
Base = automap_base() # noqa: N806
|
||||
connection = op.get_bind()
|
||||
Base.prepare(autoload_with=connection)
|
||||
|
||||
Bind = Base.classes.nonebot_plugin_tetris_stats_bind # noqa: N806
|
||||
with Session(connection) as session:
|
||||
session.query(Bind).filter(Bind.game_platform == 'TOS').delete()
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections.abc import Callable, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from nonebot.adapters import Message
|
||||
from nonebot_plugin_orm import Model
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
|
||||
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
|
||||
@@ -14,16 +15,24 @@ from ..utils.typing import CommandType, GameType
|
||||
class PydanticType(TypeDecorator):
|
||||
impl = JSON
|
||||
|
||||
def __init__(self, get_model: Callable[[], Sequence[type[BaseModel]]], *args: Any, **kwargs: Any): # noqa: ANN401
|
||||
self.get_model = get_model
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str: # noqa: ANN401
|
||||
# 将 Pydantic 模型实例转换为 JSON
|
||||
if isinstance(value, BaseModel):
|
||||
return value.json()
|
||||
if isinstance(value, tuple(self.get_model())):
|
||||
return value.json() # type: ignore[union-attr]
|
||||
raise TypeError
|
||||
|
||||
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel: # noqa: ANN401
|
||||
# 将 JSON 转换回 Pydantic 模型实例
|
||||
if isinstance(value, str | bytes):
|
||||
return BaseModel.parse_raw(value)
|
||||
for i in self.get_model():
|
||||
try:
|
||||
return i.parse_raw(value)
|
||||
except ValidationError: # noqa: PERF203
|
||||
...
|
||||
raise TypeError
|
||||
|
||||
|
||||
@@ -46,6 +55,8 @@ class HistoricalData(MappedAsDataclass, Model):
|
||||
game_platform: Mapped[GameType] = mapped_column(String(32), index=True, init=False)
|
||||
command_type: Mapped[CommandType] = mapped_column(String(16), index=True, init=False)
|
||||
command_args: Mapped[list[str]] = mapped_column(JSON, init=False)
|
||||
game_user: Mapped[BaseUser] = mapped_column(PydanticType, init=False)
|
||||
processed_data: Mapped[BaseProcessedData] = mapped_column(PydanticType, init=False)
|
||||
game_user: Mapped[BaseUser] = mapped_column(PydanticType(get_model=BaseUser.__subclasses__), init=False)
|
||||
processed_data: Mapped[BaseProcessedData] = mapped_column(
|
||||
PydanticType(get_model=BaseProcessedData.__subclasses__), init=False
|
||||
)
|
||||
finish_time: Mapped[datetime] = mapped_column(DateTime, init=False)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -12,6 +12,8 @@ from .schemas import BaseProcessedData as ProcessedData
|
||||
from .schemas import BaseRawResponse as RawResponse
|
||||
from .schemas import BaseUser as User
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
class Processor(ABC):
|
||||
event_id: int
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
|
||||
@@ -9,7 +9,7 @@ from nonebot_plugin_orm import get_session
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import NeedCatchError
|
||||
from ...utils.exception import HandleNotFinishedError, NeedCatchError
|
||||
from ...utils.metrics import get_metrics
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
@@ -20,6 +20,8 @@ from .model import IORank
|
||||
from .processor import Processor, User, identify_user_info
|
||||
from .typing import Rank
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
alc = on_alconna(
|
||||
Alconna(
|
||||
'io',
|
||||
@@ -93,7 +95,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -116,7 +119,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -129,7 +133,8 @@ async def _(event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('rank')
|
||||
@@ -154,7 +159,7 @@ async def _(matcher: Matcher, rank: Rank):
|
||||
)
|
||||
).one()
|
||||
message = ''
|
||||
if (datetime.now(UTC) - latest_data.create_time) > timedelta(hours=7):
|
||||
if (datetime.now(UTC) - latest_data.create_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:
|
||||
@@ -183,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=ZoneInfo("UTC")).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
f'数据更新时间: {latest_data.create_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
)
|
||||
await matcher.finish(message)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from aiocache import Cache as ACache # type: ignore[import-untyped]
|
||||
from nonebot.log import logger
|
||||
@@ -7,6 +7,8 @@ from pydantic import parse_raw_as
|
||||
from ...utils.request import Request
|
||||
from .schemas.base import FailedModel, SuccessModel
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
class Cache:
|
||||
cache = ACache(ACache.MEMORY)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from ...utils.typing import GameType
|
||||
from typing import Literal
|
||||
|
||||
from .typing import Rank
|
||||
|
||||
GAME_TYPE: GameType = 'IO'
|
||||
GAME_TYPE: Literal['IO'] = 'IO'
|
||||
BASE_URL = 'https://ch.tetr.io/api/'
|
||||
RANK_PERCENTILE: dict[Rank, float] = {
|
||||
'x': 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nonebot_plugin_orm import Model
|
||||
from sqlalchemy import JSON, DateTime, String
|
||||
@@ -6,6 +6,8 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
|
||||
from .typing import Rank
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
class IORank(MappedAsDataclass, Model):
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import floor
|
||||
from re import match
|
||||
from statistics import mean
|
||||
from typing import Literal
|
||||
|
||||
from nonebot import get_driver
|
||||
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
||||
@@ -15,7 +16,6 @@ from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
||||
from ...utils.request import splice_url
|
||||
from ...utils.retry import retry
|
||||
from ...utils.typing import GameType
|
||||
from .. import Processor as ProcessorMeta
|
||||
from .cache import Cache
|
||||
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
|
||||
@@ -33,6 +33,8 @@ from .schemas.user_records import SoloRecord, UserRecords
|
||||
from .schemas.user_records import SuccessModel as RecordsSuccess
|
||||
from .typing import Rank
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
@@ -55,7 +57,7 @@ class Processor(ProcessorMeta):
|
||||
self.processed_data = ProcessedData()
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
def game_platform(self) -> Literal['IO']:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
from typing import Literal
|
||||
|
||||
from ... import ProcessedData as ProcessedDataMeta
|
||||
from ... import RawResponse as RawResponseMeta
|
||||
from ..constant import GAME_TYPE
|
||||
from .user_info import SuccessModel as InfoSuccess
|
||||
from .user_records import SuccessModel as RecordsSuccess
|
||||
|
||||
|
||||
class RawResponse(RawResponseMeta):
|
||||
platform: Literal['IO'] = GAME_TYPE
|
||||
|
||||
user_info: bytes | None = None
|
||||
user_records: bytes | None = None
|
||||
|
||||
|
||||
class ProcessedData(ProcessedDataMeta):
|
||||
platform: Literal['IO'] = GAME_TYPE
|
||||
|
||||
user_info: InfoSuccess | None = None
|
||||
user_records: RecordsSuccess | None = None
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from typing import Literal
|
||||
|
||||
from ...schemas import BaseUser
|
||||
from ..constant import GAME_TYPE
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
platform: Literal['IO'] = GAME_TYPE
|
||||
|
||||
ID: str | None = None
|
||||
name: str | None = None
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class EndContext(BaseModel):
|
||||
zero: bool
|
||||
locked: bool
|
||||
prev: int
|
||||
frameoffset: int
|
||||
frameoffset: int | None
|
||||
|
||||
class Clears(BaseModel):
|
||||
singles: int
|
||||
@@ -49,8 +49,8 @@ class EndContext(BaseModel):
|
||||
holds: int | None
|
||||
time: Time
|
||||
score: int
|
||||
zenlevel: int
|
||||
zenprogress: int
|
||||
zenlevel: int | None
|
||||
zenprogress: int | None
|
||||
level: int
|
||||
combo: int
|
||||
currentcombopower: int | None # WTF
|
||||
|
||||
@@ -2,8 +2,14 @@ from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..utils.typing import GameType
|
||||
|
||||
class BaseUser(ABC, BaseModel):
|
||||
|
||||
class Base(BaseModel):
|
||||
platform: GameType
|
||||
|
||||
|
||||
class BaseUser(ABC, Base):
|
||||
"""游戏用户"""
|
||||
|
||||
def __eq__(self, __value: object) -> bool:
|
||||
@@ -17,9 +23,9 @@ class BaseUser(ABC, BaseModel):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseRawResponse(BaseModel):
|
||||
class BaseRawResponse(Base):
|
||||
"""原始请求数据"""
|
||||
|
||||
|
||||
class BaseProcessedData(BaseModel):
|
||||
class BaseProcessedData(Base):
|
||||
"""处理/验证后的数据"""
|
||||
|
||||
@@ -5,7 +5,7 @@ from nonebot_plugin_alconna import At, on_alconna
|
||||
from nonebot_plugin_orm import get_session
|
||||
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import NeedCatchError
|
||||
from ...utils.exception import HandleNotFinishedError, NeedCatchError
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
from .. import add_default_handlers
|
||||
@@ -76,7 +76,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -99,7 +100,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -112,7 +114,8 @@ async def _(event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
add_default_handlers(alc)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from ...utils.typing import GameType
|
||||
from typing import Literal
|
||||
|
||||
GAME_TYPE: GameType = 'TOP'
|
||||
GAME_TYPE: Literal['TOP'] = 'TOP'
|
||||
BASE_URL = 'http://tetrisonline.pl/top/'
|
||||
|
||||
@@ -2,7 +2,7 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from re import match
|
||||
from typing import NoReturn
|
||||
from typing import Literal, NoReturn
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import etree
|
||||
@@ -12,7 +12,6 @@ from pandas import read_html
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError
|
||||
from ...utils.request import Request, splice_url
|
||||
from ...utils.typing import GameType
|
||||
from .. import Processor as ProcessorMeta
|
||||
from ..schemas import BaseUser
|
||||
from .constant import BASE_URL, GAME_TYPE
|
||||
@@ -20,6 +19,8 @@ from .schemas.response import ProcessedData, RawResponse
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
platform: Literal['TOP'] = GAME_TYPE
|
||||
|
||||
name: str
|
||||
|
||||
@property
|
||||
@@ -56,7 +57,7 @@ class Processor(ProcessorMeta):
|
||||
self.processed_data = ProcessedData()
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
def game_platform(self) -> Literal['TOP']:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from typing import Literal
|
||||
|
||||
from ...schemas import BaseProcessedData, BaseRawResponse
|
||||
from ..constant import GAME_TYPE
|
||||
|
||||
|
||||
class RawResponse(BaseRawResponse):
|
||||
platform: Literal['TOP'] = GAME_TYPE
|
||||
|
||||
user_profile: bytes | None = None
|
||||
|
||||
|
||||
class ProcessedData(BaseProcessedData):
|
||||
platform: Literal['TOP'] = GAME_TYPE
|
||||
|
||||
user_profile: str | None = None
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import NoReturn
|
||||
|
||||
from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -5,7 +7,7 @@ from nonebot_plugin_alconna import At, on_alconna
|
||||
from nonebot_plugin_orm import get_session
|
||||
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import NeedCatchError
|
||||
from ...utils.exception import HandleNotFinishedError, NeedCatchError, RequestError
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
from .. import add_default_handlers
|
||||
@@ -66,27 +68,63 @@ alc = on_alconna(
|
||||
aliases={'tos', 'TOS'},
|
||||
)
|
||||
|
||||
try:
|
||||
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
|
||||
from nonebot.adapters.onebot.v11 import Bot as OB11Bot
|
||||
|
||||
@alc.assign('bind')
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
await matcher.finish('QQ 平台无需绑定')
|
||||
async def finish_special_query(matcher: Matcher, proc: Processor) -> NoReturn:
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
if isinstance(e, RequestError) and '未找到此用户' in e.message:
|
||||
matcher.skip()
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
try:
|
||||
from nonebot.adapters.onebot.v11 import GROUP as OB11GROUP
|
||||
from nonebot.adapters.onebot.v11 import Bot as OB11Bot
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent as OB11MessageEvent
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(bot: OB11Bot, event: MessageEvent, matcher: Matcher, target: At | Me):
|
||||
if event.is_tome() and await GROUP(bot, event):
|
||||
async def _(bot: OB11Bot, event: OB11MessageEvent, matcher: Matcher, target: At | Me):
|
||||
if event.is_tome() and await OB11GROUP(bot, event):
|
||||
await matcher.finish('不能查询bot的信息')
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(teaid=target.target if isinstance(target, At) else event.get_user_id()),
|
||||
user=User(teaid=f'onebot-{target.target}' if isinstance(target, At) else f'onebot-{event.get_user_id()}'),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await finish_special_query(matcher, proc)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from nonebot.adapters.kaiheila.event import MessageEvent as KookMessageEvent
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(event: KookMessageEvent, matcher: Matcher, target: At | Me):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(teaid=f'kook-{target.target}' if isinstance(target, At) else f'kook-{event.get_user_id()}'),
|
||||
command_args=[],
|
||||
)
|
||||
await finish_special_query(matcher, proc)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from nonebot.adapters.discord import MessageEvent as DiscordMessageEvent
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(event: DiscordMessageEvent, matcher: Matcher, target: At | Me):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(teaid=f'discord-{target.target}' if isinstance(target, At) else f'discord-{event.get_user_id()}'),
|
||||
command_args=[],
|
||||
)
|
||||
await finish_special_query(matcher, proc)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -101,7 +139,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -118,13 +157,14 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(name=bind.game_account),
|
||||
user=User(teaid=bind.game_account),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
@@ -137,7 +177,8 @@ async def _(event: Event, matcher: Matcher, account: User):
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
await matcher.send(str(e))
|
||||
raise HandleNotFinishedError from e
|
||||
|
||||
|
||||
add_default_handlers(alc)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from ...utils.typing import GameType
|
||||
from typing import Literal
|
||||
|
||||
GAME_TYPE: GameType = 'TOS'
|
||||
GAME_TYPE: Literal['TOS'] = 'TOS'
|
||||
BASE_URL = 'https://teatube.cn:8888/'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from re import match
|
||||
from typing import Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from nonebot_plugin_orm import get_session
|
||||
@@ -8,7 +9,6 @@ from pydantic import parse_raw_as
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError
|
||||
from ...utils.request import Request, splice_url
|
||||
from ...utils.typing import GameType
|
||||
from .. import Processor as ProcessorMeta
|
||||
from ..schemas import BaseUser
|
||||
from .constant import BASE_URL, GAME_TYPE
|
||||
@@ -19,6 +19,8 @@ from .schemas.user_profile import UserProfile
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
platform: Literal['TOS'] = GAME_TYPE
|
||||
|
||||
teaid: str | None = None
|
||||
name: str | None = None
|
||||
|
||||
@@ -51,7 +53,7 @@ def identify_user_info(info: str) -> User | MessageFormatError:
|
||||
and 2 <= len(info) <= 18 # noqa: PLR2004
|
||||
):
|
||||
return User(name=info)
|
||||
if info.isdigit():
|
||||
if info.startswith(('onebot-', 'qqguild-', 'kook-', 'discord-')) and info.split('-', maxsplit=1)[1].isdigit():
|
||||
return User(teaid=info)
|
||||
return MessageFormatError('用户名/QQ号不合法')
|
||||
|
||||
@@ -67,22 +69,20 @@ class Processor(ProcessorMeta):
|
||||
self.processed_data = ProcessedData(user_profile={})
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
def game_platform(self) -> Literal['TOS']:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
"""处理绑定消息"""
|
||||
self.command_type = 'bind'
|
||||
await self.get_user()
|
||||
if self.user.name is None:
|
||||
raise # FIXME: 不知道怎么才能把这类型给变过来了
|
||||
async with get_session() as session:
|
||||
return await create_or_update_bind(
|
||||
session=session,
|
||||
chat_platform=platform,
|
||||
chat_account=account,
|
||||
game_platform=GAME_TYPE,
|
||||
game_account=self.user.name,
|
||||
game_account=self.user.unique_identifier,
|
||||
)
|
||||
|
||||
async def handle_query(self) -> str:
|
||||
@@ -202,11 +202,7 @@ class Processor(ProcessorMeta):
|
||||
message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )"
|
||||
message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )'
|
||||
message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )'
|
||||
message += (
|
||||
f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s'
|
||||
if user_info.pb_sprint != 2147483647 # noqa: PLR2004
|
||||
else ''
|
||||
)
|
||||
message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != 0 else ''
|
||||
message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != 0 else ''
|
||||
message += f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s' if user_info.pb_sprint != '2147483647' else ''
|
||||
message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != '0' else ''
|
||||
message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != '0' else ''
|
||||
return message
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from typing import Literal
|
||||
|
||||
from ...schemas import BaseProcessedData, BaseRawResponse
|
||||
from ..constant import GAME_TYPE
|
||||
from .user_info import SuccessModel as InfoSuccess
|
||||
from .user_profile import UserProfile
|
||||
|
||||
|
||||
class RawResponse(BaseRawResponse):
|
||||
platform: Literal['TOS'] = GAME_TYPE
|
||||
|
||||
user_profile: dict[str, bytes]
|
||||
user_info: bytes | None = None
|
||||
|
||||
|
||||
class ProcessedData(BaseProcessedData):
|
||||
platform: Literal['TOS'] = GAME_TYPE
|
||||
|
||||
user_profile: dict[str, UserProfile]
|
||||
user_info: InfoSuccess | None = None
|
||||
|
||||
@@ -15,10 +15,6 @@ class NeedCatchError(TetrisStatsError):
|
||||
"""需要被捕获的异常基类"""
|
||||
|
||||
|
||||
class DoNotCatchError(TetrisStatsError):
|
||||
"""不应该被捕获的异常基类"""
|
||||
|
||||
|
||||
class RequestError(NeedCatchError):
|
||||
"""请求错误"""
|
||||
|
||||
@@ -27,9 +23,13 @@ class MessageFormatError(NeedCatchError):
|
||||
"""用户发送的消息格式不正确"""
|
||||
|
||||
|
||||
class DatabaseVersionError(DoNotCatchError):
|
||||
"""数据库版本错误"""
|
||||
class DoNotCatchError(TetrisStatsError):
|
||||
"""不应该被捕获的异常基类"""
|
||||
|
||||
|
||||
class WhatTheFuckError(DoNotCatchError):
|
||||
"""用于表示不应该出现的情况 ("""
|
||||
|
||||
|
||||
class HandleNotFinishedError(DoNotCatchError):
|
||||
"""任务没有正常完成处理的错误"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import ClassVar
|
||||
|
||||
from nonebot import get_driver, get_plugin
|
||||
@@ -9,6 +9,8 @@ from nonebot_plugin_orm import get_session
|
||||
|
||||
from ..db.models import HistoricalData
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from aiofiles import open
|
||||
@@ -38,7 +39,7 @@ def splice_url(url_list: list[str]) -> str:
|
||||
class Request:
|
||||
"""网络请求相关类"""
|
||||
|
||||
_CACHE_FILE = CACHE_PATH.joinpath('cloudflare_cache.json')
|
||||
_CACHE_FILE = CACHE_PATH / 'cloudflare_cache.json'
|
||||
_headers: dict | None = None
|
||||
_cookies: dict | None = None
|
||||
|
||||
@@ -46,36 +47,31 @@ class Request:
|
||||
async def _anti_cloudflare(cls, url: str) -> bytes:
|
||||
"""用firefox硬穿五秒盾"""
|
||||
browser = await BrowserManager.get_browser()
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
response = await page.goto(url)
|
||||
attempts = 0
|
||||
while attempts < 60: # noqa: PLR2004
|
||||
attempts += 1
|
||||
text = await page.locator('body').text_content()
|
||||
if text is None:
|
||||
await page.wait_for_timeout(1000)
|
||||
continue
|
||||
if await page.title() == 'Please Wait... | Cloudflare':
|
||||
logger.warning('疑似触发了 Cloudflare 的验证码')
|
||||
break
|
||||
try:
|
||||
loads(text)
|
||||
except JSONDecodeError:
|
||||
await page.wait_for_timeout(1000)
|
||||
else:
|
||||
if not isinstance(response, Response):
|
||||
raise RequestError('api请求失败')
|
||||
cls._headers = await response.request.all_headers()
|
||||
async with await browser.new_context() as context, await context.new_page() as page:
|
||||
response = await page.goto(url)
|
||||
attempts = 0
|
||||
while attempts < 60: # noqa: PLR2004
|
||||
attempts += 1
|
||||
text = await page.locator('body').text_content()
|
||||
if text is None:
|
||||
await page.wait_for_timeout(1000)
|
||||
continue
|
||||
if await page.title() == 'Please Wait... | Cloudflare':
|
||||
logger.warning('疑似触发了 Cloudflare 的验证码')
|
||||
break
|
||||
try:
|
||||
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
|
||||
except KeyError:
|
||||
cls._cookies = None
|
||||
await page.close()
|
||||
await context.close()
|
||||
return await response.body()
|
||||
await page.close()
|
||||
await context.close()
|
||||
loads(text)
|
||||
except JSONDecodeError:
|
||||
await page.wait_for_timeout(1000)
|
||||
else:
|
||||
if not isinstance(response, Response):
|
||||
raise RequestError('api请求失败')
|
||||
cls._headers = await response.request.all_headers()
|
||||
try:
|
||||
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
|
||||
except KeyError:
|
||||
cls._cookies = None
|
||||
return await response.body()
|
||||
raise RequestError('绕过五秒盾失败')
|
||||
|
||||
@classmethod
|
||||
@@ -118,11 +114,15 @@ class Request:
|
||||
try:
|
||||
async with AsyncClient(cookies=cls._cookies, timeout=config.tetris_req_timeout) as session:
|
||||
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}'
|
||||
)
|
||||
if is_json:
|
||||
loads(response.content)
|
||||
return response.content
|
||||
except HTTPError as e:
|
||||
raise RequestError(f'请求错误\n{e!r}') from e
|
||||
raise RequestError(f'请求错误 \n{e!r}') from e
|
||||
except JSONDecodeError:
|
||||
if urlparse(url).netloc.lower().endswith('tetr.io'):
|
||||
return await cls._anti_cloudflare(url)
|
||||
|
||||
2188
poetry.lock
generated
2188
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.a8'
|
||||
version = '1.0.0.a15'
|
||||
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
|
||||
authors = ['scdhh <wallfjjd@gmail.com>']
|
||||
readme = 'README.md'
|
||||
@@ -10,34 +10,36 @@ license = 'AGPL-3.0'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = '^3.10'
|
||||
nonebot2 = '^2.0.0-beta.3'
|
||||
lxml = '^4.9.1'
|
||||
nonebot2 = '^2.2.0'
|
||||
lxml = '^5.1.0'
|
||||
pandas = '>=1.4.3,<3.0.0'
|
||||
playwright = '^1.24.1'
|
||||
ujson = '^5.4.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.25.0"
|
||||
nonebot-plugin-alconna = ">=0.30,<0.34"
|
||||
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"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = '>=0.991,<1.8'
|
||||
types-ujson = '^5.7.0'
|
||||
mypy = '>=1.9'
|
||||
types-ujson = '^5.9.0'
|
||||
pandas-stubs = '>=1.5.2,<3.0.0'
|
||||
ruff = '>=0.0.239,<0.1.7'
|
||||
types-aiofiles = "^23.2.0.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.1.1" }
|
||||
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.0"
|
||||
ruff = '>=0.3.0'
|
||||
types-aiofiles = "^23.2.0.20240106"
|
||||
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.10.0"
|
||||
nonebot-adapter-kaiheila = "^0.3.1"
|
||||
nonebot-adapter-discord = "^0.1.3"
|
||||
|
||||
[tool.poetry.group.debug.dependencies]
|
||||
objprint = '^0.2.2'
|
||||
viztracer = "^0.16.0"
|
||||
viztracer = "^0.16.2"
|
||||
|
||||
[build-system]
|
||||
requires = ['poetry-core>=1.0.0']
|
||||
|
||||
Reference in New Issue
Block a user