Compare commits

...

15 Commits
0.3.0 ... 0.3.2

Author SHA1 Message Date
61b671354b 版本推进 2022-08-10 14:11:17 +08:00
3f24dc2f1d Add Release CI 2022-08-10 14:10:13 +08:00
scdhh
c63bb52540 Merge pull request #4 from shoucandanghehe/dependabot/pip/autopep8-1.7.0
Bump autopep8 from 1.6.0 to 1.7.0
2022-08-10 04:37:38 +08:00
dependabot[bot]
a0bc307aa6 Bump autopep8 from 1.6.0 to 1.7.0
Bumps [autopep8](https://github.com/hhatto/autopep8) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/hhatto/autopep8/releases)
- [Commits](https://github.com/hhatto/autopep8/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: autopep8
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-09 15:50:44 +00:00
d3258c2bb7 整理代码 2022-08-09 02:50:06 +08:00
b320a23c77 将数据库操作封装成类,并添加数据库路径的config选项 2022-08-09 02:12:29 +08:00
69e4cb97e3 import config 2022-08-09 01:39:45 +08:00
c6f83230c8 修复写入缓存时 传递了错误的变量的bug 2022-08-08 13:44:16 +08:00
e473dca4df 对于io_data_processor:
1.将请求相关封装成了Request类
2.新增了获取cookies后使用aiohttp请求的机制,理论上可以提速+减少不必要的性能开销,并且将cookies存储至cache
3.整理代码
新增config.py,用于配置cache路径
添加依赖项Brotli
2022-08-08 13:32:31 +08:00
scdhh
68028cf3d9 Merge pull request #3 from shoucandanghehe/dependabot/pip/pandas-stubs-1.4.3.220807
Bump pandas-stubs from 1.4.3.220801 to 1.4.3.220807
2022-08-08 13:22:22 +08:00
dependabot[bot]
10929bab03 Bump pandas-stubs from 1.4.3.220801 to 1.4.3.220807
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 1.4.3.220801 to 1.4.3.220807.
- [Release notes](https://github.com/pandas-dev/pandas-stubs/releases)
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v1.4.3.220801...v1.4.3.220807)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-08 01:27:02 +00:00
scdhh
8e5e7d3c33 Create dependabot.yml 2022-08-08 08:56:40 +08:00
ec04af21d6 版本推进 2022-08-07 00:43:31 +08:00
2aef281e8c 修复正确请求到api后没有正确关闭浏览器标签页导致内存溢出的bug 2022-08-07 00:42:59 +08:00
2be62140ac 修复关闭数据库时错误使用await的错误 2022-08-07 00:39:42 +08:00
13 changed files with 482 additions and 236 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

25
.github/workflows/Release.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Release CI
on:
push:
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Poetry
shell: bash
run: |
pip install poetry
- name: Build
shell: bash
run: |
poetry install
poetry env use python
poetry publish --build -u ${{ secrets.USERNAME }} -p ${{ secrets.PASSWORD }} -n

View File

@@ -2,13 +2,13 @@
# A comma-separated list of package or module names from where C extensions may # A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may # be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. # run arbitrary code.
extension-pkg-allow-list=lxml extension-pkg-allow-list=lxml, pydantic
# A comma-separated list of package or module names from where C extensions may # A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may # be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list # run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.) # for backward compatibility.)
extension-pkg-whitelist=lxml extension-pkg-whitelist=lxml, pydantic
# List of module names for which member attributes should not be checked # List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime # (useful for modules/projects where namespaces are manipulated during runtime

View File

@@ -1,26 +1,30 @@
from typing import Any import os
from asyncio import gather from asyncio import gather
from re import I from re import I
from playwright.async_api import Browser, async_playwright from typing import Any
from ujson import loads, JSONDecodeError
import aiohttp import aiohttp
from nonebot import get_driver, on_regex
from nonebot import on_regex, get_driver
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
from nonebot.matcher import Matcher
from nonebot.log import logger from nonebot.log import logger
from nonebot.matcher import Matcher
from playwright.async_api import Browser, Response, async_playwright
from ujson import JSONDecodeError, dumps, loads
from ..utils.message_analyzer import handle_bind_message, handle_stats_query_message from ..utils.config import Config
from ..utils.sql import query_bind_info, write_bind_info from ..utils.database import DataBase
from ..utils.message_analyzer import (
_BROWSER: Browser | None = None handle_bind_message,
handle_stats_query_message
)
IOBind = on_regex(pattern=r'^io绑定|^iobind', flags=I, permission=GROUP) IOBind = on_regex(pattern=r'^io绑定|^iobind', flags=I, permission=GROUP)
IOStats = on_regex(pattern=r'^io查|^iostats', flags=I, permission=GROUP) IOStats = on_regex(pattern=r'^io查|^iostats', flags=I, permission=GROUP)
driver = get_driver() driver = get_driver()
config = Config.parse_obj(get_driver().config)
@IOBind.handle() @IOBind.handle()
async def _(event: MessageEvent, matcher: Matcher): async def _(event: MessageEvent, matcher: Matcher):
@@ -28,7 +32,7 @@ async def _(event: MessageEvent, matcher: Matcher):
if decoded_message[0] is None: if decoded_message[0] is None:
await matcher.finish(decoded_message[1][0]) await matcher.finish(decoded_message[1][0])
if decoded_message[0] == 'ID': if decoded_message[0] == 'ID':
user_id_stats = await check_user_id(user_id=decoded_message[1][1]) user_id_stats = await check_user_id(decoded_message[1][1])
if user_id_stats[0] is False: if user_id_stats[0] is False:
await matcher.finish(user_id_stats[1]) await matcher.finish(user_id_stats[1])
else: else:
@@ -40,13 +44,17 @@ async def _(event: MessageEvent, matcher: Matcher):
elif user_data[1] is False: elif user_data[1] is False:
await matcher.finish(f'用户信息请求错误:\n{user_data[2]["error"]}') await matcher.finish(f'用户信息请求错误:\n{user_data[2]["error"]}')
else: else:
user_id = await get_user_id(user_data=user_data[2]) user_id = await get_user_id(user_data[2])
if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是 if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是
logger.error('获取QQ号失败') logger.error('获取QQ号失败')
await matcher.finish('获取QQ号失败') await matcher.finish('获取QQ号失败')
await matcher.finish(await write_bind_info(qq_number=event.sender.user_id, await matcher.finish(
user=user_id, await DataBase.write_bind_info(
game_type='IO')) qq_number=event.sender.user_id,
user=user_id,
game_type='IO'
)
)
@IOStats.handle() @IOStats.handle()
@@ -56,8 +64,8 @@ async def _(event: MessageEvent, matcher: Matcher):
await matcher.finish(decoded_message[1][0]) await matcher.finish(decoded_message[1][0])
elif decoded_message[0] == 'AT': elif decoded_message[0] == 'AT':
if event.is_tome() is True: if event.is_tome() is True:
await matcher.finish(message='不能查询bot的信息') await matcher.finish('不能查询bot的信息')
bind_info = await query_bind_info(qq_number=decoded_message[1][1], game_type='IO') bind_info = await DataBase.query_bind_info(qq_number=decoded_message[1][1], game_type='IO')
if bind_info is None: if bind_info is None:
message = '未查询到绑定信息' message = '未查询到绑定信息'
else: else:
@@ -66,7 +74,7 @@ async def _(event: MessageEvent, matcher: Matcher):
if event.sender.user_id is None: if event.sender.user_id is None:
logger.error('获取QQ号失败') logger.error('获取QQ号失败')
await matcher.finish('获取QQ号失败, 请联系bot主人') await matcher.finish('获取QQ号失败, 请联系bot主人')
bind_info = await query_bind_info(qq_number=event.sender.user_id, game_type='IO') bind_info = await DataBase.query_bind_info(qq_number=event.sender.user_id, game_type='IO')
if bind_info is None: if bind_info is None:
message = '未查询到绑定信息' message = '未查询到绑定信息'
else: else:
@@ -75,66 +83,24 @@ async def _(event: MessageEvent, matcher: Matcher):
message = await generate_message(user_id=decoded_message[1][1]) message = await generate_message(user_id=decoded_message[1][1])
elif decoded_message[0] == 'Name': elif decoded_message[0] == 'Name':
message = await generate_message(user_name=decoded_message[1][1]) message = await generate_message(user_name=decoded_message[1][1])
await matcher.finish(message=message) await matcher.finish(message)
@driver.on_startup
async def _():
await Request.init_cache()
await Request.read_cache()
@driver.on_shutdown @driver.on_shutdown
async def _(): async def _():
if isinstance(_BROWSER, Browser): await Request.close_browser()
await _BROWSER.close()
async def init_playwright() -> Browser: async def get_user_data(
'''初始化playwright''' user_name: str = None,
global _BROWSER user_id: str = None
p = await async_playwright().start() ) -> tuple[bool, bool, dict[str, Any]]:
_BROWSER = await p.firefox.launch()
return _BROWSER
async def get_browser() -> Browser:
'''获取浏览器对象'''
return _BROWSER or await init_playwright()
async def request(url: str) -> tuple[bool, bool, dict[str, Any]]:
'''请求api'''
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.json()
return (True, data['success'], data)
except aiohttp.client_exceptions.ClientConnectorError as error:
logger.error(f'请求错误\n{error}')
return (False, False, {})
except aiohttp.client_exceptions.ContentTypeError:
# 如果有五秒盾就用firefox硬穿
browser = await get_browser()
page = await browser.new_page()
await page.goto(url)
attempts = 0
while True:
text = await page.locator("body").text_content()
if text is None:
continue
attempts += 1
if await page.title() == 'Please Wait... | Cloudflare':
break
try:
data = loads(text)
except JSONDecodeError:
await page.wait_for_timeout(1000)
else:
return (True, data['success'], data)
if attempts >= 60:
break
await page.close()
return (True, False, {'error': '绕过五秒盾失败'})
async def get_user_data(user_name: str = None,
user_id: str = None
) -> tuple[bool, bool, dict[str, Any]]:
'''获取用户数据''' '''获取用户数据'''
if user_name is not None and user_id is None: if user_name is not None and user_id is None:
user_data_url = f'https://ch.tetr.io/api/users/{user_name}' user_data_url = f'https://ch.tetr.io/api/users/{user_name}'
@@ -142,12 +108,13 @@ async def get_user_data(user_name: str = None,
user_data_url = f'https://ch.tetr.io/api/users/{user_id}' user_data_url = f'https://ch.tetr.io/api/users/{user_id}'
else: else:
raise ValueError('预期外行为, 请上报GitHub') raise ValueError('预期外行为, 请上报GitHub')
return await request(url=user_data_url) return await Request.request(user_data_url)
async def get_solo_data(user_name: str = None, async def get_solo_data(
user_id: str = None user_name: str = None,
) -> tuple[bool, bool, dict[str, Any]]: user_id: str = None
) -> tuple[bool, bool, dict[str, Any]]:
'''获取Solo数据''' '''获取Solo数据'''
if user_name is not None and user_id is None: if user_name is not None and user_id is None:
user_solo_url = f'https://ch.tetr.io/api/users/{user_name}/records' user_solo_url = f'https://ch.tetr.io/api/users/{user_name}/records'
@@ -155,7 +122,7 @@ async def get_solo_data(user_name: str = None,
user_solo_url = f'https://ch.tetr.io/api/users/{user_id}/records' user_solo_url = f'https://ch.tetr.io/api/users/{user_id}/records'
else: else:
raise ValueError('预期外行为, 请上报GitHub') raise ValueError('预期外行为, 请上报GitHub')
return await request(url=user_solo_url) return await Request.request(user_solo_url)
async def get_user_id(user_data: dict) -> str: async def get_user_id(user_data: dict) -> str:
@@ -167,11 +134,11 @@ async def check_user_id(user_id: str) -> tuple[bool, str]:
'''检查用户ID是否有效''' '''检查用户ID是否有效'''
user_data = await get_user_data(user_id=user_id) user_data = await get_user_data(user_id=user_id)
if user_data[0] is False: if user_data[0] is False:
return (False, '用户信息请求失败') return False, '用户信息请求失败'
if user_data[1] is False: if user_data[1] is False:
return (False, f'用户信息请求错误:\n{user_data[2]["error"]}') return False, f'用户信息请求错误:\n{user_data[2]["error"]}'
if user_id == user_data[2]['data']['user']['_id']: if user_id == user_data[2]['data']['user']['_id']:
return (True, '') return True, ''
raise ValueError('服务器返回的userID和用户提供的不一致, 这种情况理论上不应该发生, 以防万一还是写一下x') raise ValueError('服务器返回的userID和用户提供的不一致, 这种情况理论上不应该发生, 以防万一还是写一下x')
@@ -225,8 +192,10 @@ async def get_blitz_stats(solo_data: dict) -> dict[str, Any]:
async def generate_message(user_name: str = None, user_id: str = None) -> str: async def generate_message(user_name: str = None, user_id: str = None) -> str:
'''生成消息''' '''生成消息'''
user_data, solo_data = await gather(get_user_data(user_name=user_name, user_id=user_id), user_data, solo_data = await gather(
get_solo_data(user_name=user_name, user_id=user_id)) get_user_data(user_name=user_name, user_id=user_id),
get_solo_data(user_name=user_name, user_id=user_id)
)
if user_data[0] is False: if user_data[0] is False:
return '用户信息请求失败' return '用户信息请求失败'
if user_data[1] is False: if user_data[1] is False:
@@ -253,10 +222,139 @@ async def generate_message(user_name: str = None, user_id: str = None) -> str:
return f'{message}\nSolo统计数据请求失败' return f'{message}\nSolo统计数据请求失败'
if solo_data[1] is False: if solo_data[1] is False:
return f'{message}\nSolo统计数据请求错误:\n{solo_data[2]["error"]}' return f'{message}\nSolo统计数据请求错误:\n{solo_data[2]["error"]}'
sprint_stats, blitz_stats = await gather(get_sprint_stats(solo_data[2]), sprint_stats, blitz_stats = await gather(
get_blitz_stats(solo_data[2])) get_sprint_stats(solo_data[2]),
get_blitz_stats(solo_data[2])
)
message += f'\n40L: {sprint_stats["Time"]}s' if 'Time' in sprint_stats else '' message += f'\n40L: {sprint_stats["Time"]}s' if 'Time' in sprint_stats else ''
message += f' ( #{sprint_stats["Rank"]} )' if 'Rank' in sprint_stats else '' message += f' ( #{sprint_stats["Rank"]} )' if 'Rank' in sprint_stats else ''
message += f'\nBlitz: {blitz_stats["Score"]}' if 'Score' in blitz_stats else '' message += f'\nBlitz: {blitz_stats["Score"]}' if 'Score' in blitz_stats else ''
message += f' ( #{blitz_stats["Rank"]} )' if 'Rank' in blitz_stats else '' message += f' ( #{blitz_stats["Rank"]} )' if 'Rank' in blitz_stats else ''
return message return message
class Request:
'''网络请求相关类'''
_browser: Browser | None = None
_headers: dict | None = None
_cookies: dict | None = None
@classmethod
async def _init_playwright(cls) -> Browser:
'''初始化playwright'''
playwright = await async_playwright().start()
cls._browser = await playwright.firefox.launch()
return cls._browser
@classmethod
async def _get_browser(cls) -> Browser:
'''获取浏览器对象'''
return cls._browser or await cls._init_playwright()
@classmethod
async def _anti_cloudflare(cls, url: str) -> tuple[bool, bool, dict[str, Any]]:
'''用firefox硬穿五秒盾'''
browser = await cls._get_browser()
context = await browser.new_context()
page = await context.new_page()
response = await page.goto(url)
attempts = 0
while attempts < 60:
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':
# TODO 有无人来做一个过验证码(
break
try:
data = loads(text)
except JSONDecodeError:
await page.wait_for_timeout(1000)
else:
assert isinstance(response, Response)
cls._headers = await response.request.all_headers()
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
await cls._write_cache()
await page.close()
await context.close()
return True, data['success'], data
await page.close()
await context.close()
return True, False, {'error': '绕过五秒盾失败'}
@classmethod
async def init_cache(cls) -> None:
'''初始化缓存文件'''
if not os.path.exists(os.path.dirname(config.cache_path)):
os.makedirs(os.path.dirname(config.cache_path))
if not os.path.exists(config.cache_path):
with open(file=config.cache_path, mode='w', encoding='UTF-8') as file:
file.write(
dumps(
{
'headers': cls._headers,
'cookies': cls._cookies
}
)
)
@classmethod
async def read_cache(cls) -> None:
'''读取缓存文件'''
try:
with open(file=config.cache_path, mode='r', encoding='UTF-8') as file:
json = loads(file.read())
cls._headers = json['headers']
cls._cookies = json['cookies']
except FileNotFoundError:
await cls.init_cache()
except PermissionError:
os.remove(config.cache_path)
await cls.init_cache()
except JSONDecodeError:
os.remove(config.cache_path)
await cls.init_cache()
@classmethod
async def _write_cache(cls) -> None:
'''写入缓存文件'''
try:
with open(file=config.cache_path, mode='r+', encoding='UTF-8') as file:
file.write(
dumps(
{
'headers': cls._headers,
'cookies': cls._cookies
}
)
)
except FileNotFoundError:
await cls.init_cache()
except PermissionError:
os.remove(config.cache_path)
await cls.init_cache()
except JSONDecodeError:
os.remove(config.cache_path)
await cls.init_cache()
@classmethod
async def request(cls, url: str) -> tuple[bool, bool, dict[str, Any]]:
'''请求api'''
try:
async with aiohttp.ClientSession(cookies=cls._cookies) as session:
async with session.get(url, headers=cls._headers) as resp:
data = await resp.json()
return True, data['success'], data
except aiohttp.client_exceptions.ClientConnectorError as error:
logger.error(f'请求错误\n{error}')
return False, False, {}
except aiohttp.client_exceptions.ContentTypeError:
return await cls._anti_cloudflare(url)
@classmethod
async def close_browser(cls) -> None:
'''关闭浏览器对象'''
if isinstance(cls._browser, Browser):
await cls._browser.close()

View File

@@ -1,16 +1,20 @@
from typing import Any from asyncio import gather
from re import I from re import I
from lxml import etree from typing import Any
from pandas import read_html
import aiohttp
import aiohttp
from lxml import etree
from nonebot import on_regex from nonebot import on_regex
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
from nonebot.matcher import Matcher
from nonebot.log import logger from nonebot.log import logger
from nonebot.matcher import Matcher
from pandas import read_html
from ..utils.message_analyzer import handle_bind_message, handle_stats_query_message from ..utils.database import DataBase
from ..utils.sql import query_bind_info, write_bind_info from ..utils.message_analyzer import (
handle_bind_message,
handle_stats_query_message
)
TOPBind = on_regex(pattern=r'^top绑定|^topbind', flags=I, permission=GROUP) TOPBind = on_regex(pattern=r'^top绑定|^topbind', flags=I, permission=GROUP)
TopStats = on_regex(pattern=r'^top查|^topstats', flags=I, permission=GROUP) TopStats = on_regex(pattern=r'^top查|^topstats', flags=I, permission=GROUP)
@@ -32,7 +36,13 @@ async def _(event: MessageEvent, matcher: Matcher):
if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是 if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是
logger.error('获取QQ号失败') logger.error('获取QQ号失败')
await matcher.finish('获取QQ号失败') await matcher.finish('获取QQ号失败')
await matcher.finish(await write_bind_info(qq_number=event.sender.user_id, user=user_name, game_type='TOP')) await matcher.finish(
await DataBase.write_bind_info(
qq_number=event.sender.user_id,
user=user_name,
game_type='TOP'
)
)
@TopStats.handle() @TopStats.handle()
@@ -42,8 +52,8 @@ async def _(event: MessageEvent, matcher: Matcher):
await matcher.finish(decoded_message[1][0]) await matcher.finish(decoded_message[1][0])
elif decoded_message[0] == 'AT': elif decoded_message[0] == 'AT':
if event.is_tome() is True: if event.is_tome() is True:
await matcher.finish(message='不能查询bot的信息') await matcher.finish('不能查询bot的信息')
bind_info = await query_bind_info(qq_number=decoded_message[1][1], game_type='TOP') bind_info = await DataBase.query_bind_info(qq_number=decoded_message[1][1], game_type='TOP')
if bind_info is None: if bind_info is None:
message = '未查询到绑定信息' message = '未查询到绑定信息'
else: else:
@@ -52,14 +62,14 @@ async def _(event: MessageEvent, matcher: Matcher):
if event.sender.user_id is None: if event.sender.user_id is None:
logger.error('获取QQ号失败') logger.error('获取QQ号失败')
await matcher.finish('获取QQ号失败, 请联系bot主人') await matcher.finish('获取QQ号失败, 请联系bot主人')
bind_info = await query_bind_info(qq_number=event.sender.user_id, game_type='TOP') bind_info = await DataBase.query_bind_info(qq_number=event.sender.user_id, game_type='TOP')
if bind_info is None: if bind_info is None:
message = '未查询到绑定信息' message = '未查询到绑定信息'
else: else:
message = (f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await generate_message(bind_info)}') message = (f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await generate_message(bind_info)}')
elif decoded_message[0] == 'Name': elif decoded_message[0] == 'Name':
message = await generate_message(decoded_message[1][1]) message = await generate_message(decoded_message[1][1])
await matcher.finish(message=message) await matcher.finish(message)
async def get_user_data(user_name: str) -> tuple[bool, str]: async def get_user_data(user_name: str) -> tuple[bool, str]:
@@ -68,10 +78,10 @@ async def get_user_data(user_name: str) -> tuple[bool, str]:
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: async with session.get(url) as resp:
return (True, await resp.text()) return True, await resp.text()
except aiohttp.client_exceptions.ClientConnectorError as error: except aiohttp.client_exceptions.ClientConnectorError as error:
logger.error(error) logger.error(error)
return (False, '') return False, ''
async def check_user(user_data: str) -> bool: async def check_user(user_data: str) -> bool:
@@ -153,8 +163,10 @@ async def generate_message(user_name: str) -> str:
return '用户信息请求失败' return '用户信息请求失败'
if await check_user(user_data[1]) is False: if await check_user(user_data[1]) is False:
return '用户不存在' return '用户不存在'
user_name = await get_user_name(user_data[1]) user_name, game_stats = await gather(
game_stats = await get_game_stats(user_data[1]) get_user_name(user_data[1]),
get_game_stats(user_data[1])
)
message = '' message = ''
if game_stats['24H'] and game_stats['All']: if game_stats['24H'] and game_stats['All']:
message += f'用户 {user_name} 24小时内统计数据为: ' message += f'用户 {user_name} 24小时内统计数据为: '

View File

@@ -1,17 +1,20 @@
from typing import Any
from asyncio import gather from asyncio import gather
from re import I from re import I
import aiohttp from typing import Any
import aiohttp
from nonebot import on_regex from nonebot import on_regex
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
from nonebot.matcher import Matcher
from nonebot.log import logger from nonebot.log import logger
from nonebot.matcher import Matcher
from ..utils.message_analyzer import handle_stats_query_message from ..utils.message_analyzer import handle_stats_query_message
TOSStats = on_regex(pattern=r'^tos查|^tostats|^tosstats|^茶服查|^茶服stats', TOSStats = on_regex(
flags=I, permission=GROUP) pattern=r'^tos查|^tostats|^tosstats|^茶服查|^茶服stats',
flags=I,
permission=GROUP
)
@TOSStats.handle() @TOSStats.handle()
@@ -21,13 +24,13 @@ async def _(event: MessageEvent, matcher: Matcher):
await matcher.finish(decoded_message[1][0]) await matcher.finish(decoded_message[1][0])
elif decoded_message[0] == 'AT' or decoded_message[0] == 'QQ': elif decoded_message[0] == 'AT' or decoded_message[0] == 'QQ':
if decoded_message[1][1] == event.self_id: if decoded_message[1][1] == event.self_id:
await matcher.finish(message='不能查询bot的信息') await matcher.finish('不能查询bot的信息')
message = await generate_message(tea_id=decoded_message[1][1]) message = await generate_message(tea_id=decoded_message[1][1])
elif decoded_message[0] == 'ME': elif decoded_message[0] == 'ME':
message = await generate_message(tea_id=event.sender.user_id) message = await generate_message(tea_id=event.sender.user_id)
elif decoded_message[0] == 'Name': elif decoded_message[0] == 'Name':
message = await generate_message(user_name=decoded_message[1][1]) message = await generate_message(user_name=decoded_message[1][1])
await matcher.finish(message=message) await matcher.finish(message)
async def request(url: str) -> tuple[bool, bool, dict[str, Any]]: async def request(url: str) -> tuple[bool, bool, dict[str, Any]]:
@@ -36,15 +39,16 @@ async def request(url: str) -> tuple[bool, bool, dict[str, Any]]:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: async with session.get(url) as resp:
data = await resp.json() data = await resp.json()
return (True, data['success'], data) return True, data['success'], data
except aiohttp.client_exceptions.ClientConnectorError as error: except aiohttp.client_exceptions.ClientConnectorError as error:
logger.error(f'请求错误\n{error}') logger.error(f'请求错误\n{error}')
return (False, False, {}) return False, False, {}
async def get_user_info(user_name: str = None, async def get_user_info(
tea_id: int = None user_name: str = None,
) -> tuple[bool, bool, dict[str, Any]]: tea_id: int = None
) -> tuple[bool, bool, dict[str, Any]]:
'''获取用户信息''' '''获取用户信息'''
if user_name is not None and tea_id is None: if user_name is not None and tea_id is None:
user_data_url = f'https://teatube.cn:8888/getUsernameInfo?username={user_name}' user_data_url = f'https://teatube.cn:8888/getUsernameInfo?username={user_name}'
@@ -52,13 +56,14 @@ async def get_user_info(user_name: str = None,
user_data_url = f'https://teatube.cn:8888/getTeaIdInfo?teaId={tea_id}' user_data_url = f'https://teatube.cn:8888/getTeaIdInfo?teaId={tea_id}'
else: else:
raise ValueError('预期外行为, 请上报GitHub') raise ValueError('预期外行为, 请上报GitHub')
return await request(url=user_data_url) return await request(user_data_url)
async def get_user_data(user_name: str = None, async def get_user_data(
tea_id: int = None, user_name: str = None,
other_parameter: str = '' tea_id: int = None,
) -> tuple[bool, bool, dict[str, Any]]: other_parameter: str = ''
) -> tuple[bool, bool, dict[str, Any]]:
'''获取用户数据''' '''获取用户数据'''
if user_name is not None and tea_id is None: if user_name is not None and tea_id is None:
user_data_url = f'https://teatube.cn:8888/getProfile?id={user_name}{other_parameter}' user_data_url = f'https://teatube.cn:8888/getProfile?id={user_name}{other_parameter}'
@@ -66,7 +71,7 @@ async def get_user_data(user_name: str = None,
user_data_url = f'https://teatube.cn:8888/getProfile?id={tea_id}{other_parameter}' user_data_url = f'https://teatube.cn:8888/getProfile?id={tea_id}{other_parameter}'
else: else:
raise ValueError('预期外行为, 请上报GitHub') raise ValueError('预期外行为, 请上报GitHub')
return await request(url=user_data_url) return await request(user_data_url)
async def get_rank_stats(user_info: dict) -> dict[str, float]: async def get_rank_stats(user_info: dict) -> dict[str, float]:
@@ -133,13 +138,18 @@ async def get_pb_data(user_info: dict) -> dict[str, float | str]:
async def generate_message(user_name: str = None, tea_id: int = None) -> str: async def generate_message(user_name: str = None, tea_id: int = None) -> str:
'''生成消息''' '''生成消息'''
user_info, user_data = await gather(get_user_info(user_name=user_name, tea_id=tea_id), user_info, user_data = await gather(
get_user_data(user_name=user_name, tea_id=tea_id)) get_user_info(user_name=user_name, tea_id=tea_id),
get_user_data(user_name=user_name, tea_id=tea_id)
)
if user_info[0] is False: if user_info[0] is False:
return '用户信息请求失败' return '用户信息请求失败'
if user_info[1] is False: if user_info[1] is False:
return f'用户信息请求错误:\n{user_info[2]["error"]}' return f'用户信息请求错误:\n{user_info[2]["error"]}'
rank_stats, pb_data = await gather(get_rank_stats(user_info[2]), get_pb_data(user_info[2])) rank_stats, pb_data = await gather(
get_rank_stats(user_info[2]),
get_pb_data(user_info[2])
)
message = f'用户 {user_info[2]["data"]["name"]} ({user_info[2]["data"]["teaId"]}) ' message = f'用户 {user_info[2]["data"]["name"]} ({user_info[2]["data"]["teaId"]}) '
if not rank_stats: if not rank_stats:
message += '暂无段位统计数据' message += '暂无段位统计数据'

View File

@@ -1 +1 @@
from . import message_analyzer, sql from . import config, database, message_analyzer

View File

@@ -0,0 +1,7 @@
from pydantic import BaseModel
class Config(BaseModel):
'''配置类'''
cache_path: str = 'cache/nonebot_plugin_tetris_stats/cache'
db_path: str = 'data/nonebot_plugin_tetris_stats/data.db'

View File

@@ -0,0 +1,86 @@
import os
from asyncio import gather
from sqlite3 import Connection, connect
from nonebot import get_driver
from nonebot.log import logger
from .config import Config
driver = get_driver()
config = Config.parse_obj(get_driver().config)
@driver.on_startup
async def _():
await DataBase.init_db()
@driver.on_shutdown
async def _():
await DataBase.close_db()
class DataBase():
'''数据库交互类'''
_db: Connection | None = None
@classmethod
async def init_db(cls) -> Connection:
'''初始化数据库'''
if not os.path.exists(os.path.dirname(config.db_path)):
os.makedirs(os.path.dirname(config.db_path))
cls._db = connect(config.db_path)
cursor = cls._db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS IOBIND
(QQ INTEGER NOT NULL,
USER TEXT NOT NULL)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS TOPBIND
(QQ INTEGER NOT NULL,
USER TEXT NOT NULL)''')
cls._db.commit()
logger.info('数据库初始化完成')
return cls._db
@classmethod
async def _get_db(cls) -> Connection:
'''获取数据库对象'''
return cls._db or await cls.init_db()
@classmethod
async def query_bind_info(cls, qq_number: str | int, game_type: str) -> str | None:
'''查询绑定信息'''
db = await cls._get_db()
cursor = db.cursor()
cursor.execute(
f'SELECT USER FROM {game_type}BIND WHERE QQ = {qq_number}')
user = cursor.fetchone()
if user is None:
return None
return user[0]
@classmethod
async def write_bind_info(cls, qq_number: str | int, user: str, game_type: str) -> str:
'''写入绑定信息'''
bind_info, db = await gather(
cls.query_bind_info(qq_number=qq_number, game_type=game_type),
cls._get_db()
)
cursor = db.cursor()
if bind_info is not None:
cursor.execute(
f'UPDATE {game_type}BIND SET USER = ? WHERE QQ = ?', (user, qq_number))
message = '更新成功'
elif bind_info is None:
cursor.execute(
f'INSERT INTO {game_type}BIND (QQ, USER) VALUES (?, ?)', (qq_number, user))
message = '绑定成功'
db.commit()
return message
@classmethod
async def close_db(cls) -> None:
'''关闭数据库对象'''
if isinstance(cls._db, Connection):
cls._db.close()

View File

@@ -14,7 +14,7 @@ async def handle_bind_message(message: str, game_type: str) -> tuple[str | None,
else: else:
raise ValueError('预期外行为, 请上报GitHub') raise ValueError('预期外行为, 请上报GitHub')
if message == '' or message.isspace(): if message == '' or message.isspace():
return (None, ('用户名为空', None)) return None, ('用户名为空', None)
return await check_name(message, game_type) return await check_name(message, game_type)
@@ -23,11 +23,13 @@ async def handle_stats_query_message(message: str, game_type: str) -> tuple[str
_cmd_aliases = {'IO': ['io查', 'iostats'], _cmd_aliases = {'IO': ['io查', 'iostats'],
'TOS': ['tos查', 'tostats', 'tosstats', '茶服查', '茶服stats'], 'TOS': ['tos查', 'tostats', 'tosstats', '茶服查', '茶服stats'],
'TOP': ['top查', 'topstats']} 'TOP': ['top查', 'topstats']}
_me = ['', '自己', '我等', '卑人', '', '老身', '', '老娘', '本姑娘', '本大爷', '鄙人', '寡人', _me = [
'小生', '贫僧', '本人', '', '', '', '', '', 'me', '洒家', '在下', '', '', '', '自己', '我等', '卑人', '', '老身', '', '老娘', '本姑娘', '本大爷', '鄙人', '',
'本小姐', '老夫', '老子', '', '本尊', '', '拙者', '', '', '自分', '吾輩', '我輩', '', '小生', '贫僧', '本人', '', '', '', '', '', 'me', '洒家', '在下', '', '人家',
'己等', '俺等', '此方', '', '', '劳资', '本宝宝', '', '本喵', 'watashi', 'i', 'myself', '本小姐', '老夫', '老子', '', '本尊', '', '拙者', '', '', '自分', '吾輩', '我輩', '',
'self', 'oneself'] '己等', '俺等', '此方', '', '', '劳资', '本宝宝', '', '本喵', 'watashi', 'i', 'myself',
'self', 'oneself'
]
# 剔除命令前缀 # 剔除命令前缀
for i in _cmd_aliases[game_type]: for i in _cmd_aliases[game_type]:
if match(rf'(?i){i}', message): if match(rf'(?i){i}', message):
@@ -35,18 +37,18 @@ async def handle_stats_query_message(message: str, game_type: str) -> tuple[str
message = message.strip() message = message.strip()
break break
if message == '' or message.isspace(): if message == '' or message.isspace():
return (None, ('用户名为空', None)) return None, ('用户名为空', None)
if message.startswith('[CQ:at,qq='): if message.startswith('[CQ:at,qq='):
try: try:
user = int(str(message).split('[CQ:at,qq=')[1].split(']')[0]) user = int(str(message).split('[CQ:at,qq=')[1].split(']')[0])
except ValueError: except ValueError:
return (None, ('QQ号码不合法', None)) return None, ('QQ号码不合法', None)
else: else:
return ('AT', (None, user)) return 'AT', (None, user)
elif message in _me: elif message in _me:
# 会不会有人叫本姑娘 本大爷这种或许可以成为id的名字呢 # 会不会有人叫本姑娘 本大爷这种或许可以成为id的名字呢
# TODO: 在判断是否可能是查自己的情况的时候 也去判断是否能成立为一个UserName # TODO: 在判断是否可能是查自己的情况的时候 也去判断是否能成立为一个UserName
return ('ME', (None, None)) return 'ME', (None, None)
else: else:
return await check_name(message, game_type) return await check_name(message, game_type)
@@ -55,21 +57,22 @@ async def check_name(name: str, game_type: str) -> tuple[str | None, tuple]:
'''返回值为tuple[gameType, tuple[message, user]]''' '''返回值为tuple[gameType, tuple[message, user]]'''
if game_type == 'IO': if game_type == 'IO':
if match(r'^[a-f0-9]{24}$', name): if match(r'^[a-f0-9]{24}$', name):
return ('ID', (None, name)) return 'ID', (None, name)
if match(r'^[a-zA-Z0-9_-]{3,16}$', name): if match(r'^[a-zA-Z0-9_-]{3,16}$', name):
return ('Name', (None, name.lower())) return 'Name', (None, name.lower())
return (None, ('用户名不合法', None)) return None, ('用户名不合法', None)
if game_type == 'TOP': if game_type == 'TOP':
if match(r'^[a-zA-Z0-9_]{1,16}$', name): if match(r'^[a-zA-Z0-9_]{1,16}$', name):
return ('Name', (None, name)) return 'Name', (None, name)
return (None, ('用户名不合法', None)) return None, ('用户名不合法', None)
if game_type == 'TOS': if game_type == 'TOS':
if (match(r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$', name) if (match(r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$', name)
and name.isdigit() is False and name.isdigit() is False
and 2 <= len(name) <= 18): and 2 <= len(name) <= 18):
# 虽然我也不想这么长 但是似乎确实得这么长 # 虽然我也不想这么长 但是似乎确实得这么长
return ('Name', (None, name)) # TODO 简化正则表达式
return 'Name', (None, name)
if name.isdigit() is True: if name.isdigit() is True:
return ('QQ', (None, name)) return 'QQ', (None, name)
return (None, ('用户名不合法', None)) return None, ('用户名不合法', None)
return (None, ('游戏类型错误', None)) return None, ('游戏类型错误', None)

View File

@@ -1,79 +0,0 @@
from sqlite3 import connect, Connection
import os
from nonebot import get_driver
from nonebot.log import logger
_DB_FILE = 'data/nonebot_plugin_tetris_stats/data.db'
_DB: Connection | None = None
driver = get_driver()
@driver.on_startup
async def _():
'''初始化数据库'''
await init_db()
@driver.on_shutdown
async def _():
if isinstance(_DB, Connection):
await _DB.close()
async def init_db() -> Connection:
'''初始化数据库'''
if not os.path.exists(os.path.dirname(_DB_FILE)):
if os.path.exists('data/nonebot-plugin-tetris-stats'): # 重命名旧的数据库路径
os.rename('data/nonebot-plugin-tetris-stats',
os.path.dirname(_DB_FILE))
else:
os.makedirs(os.path.dirname(_DB_FILE))
global _DB
_DB = connect(_DB_FILE)
cursor = _DB.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS IOBIND
(QQ INTEGER NOT NULL,
USER TEXT NOT NULL)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS TOPBIND
(QQ INTEGER NOT NULL,
USER TEXT NOT NULL)''')
_DB.commit()
logger.info('数据库初始化完成')
return _DB
async def get_db() -> Connection:
'''获取数据库对象'''
return _DB or await init_db()
async def query_bind_info(qq_number: str | int, game_type: str) -> str | None:
'''查询绑定信息'''
db = await get_db()
cursor = db.cursor()
cursor.execute(f'SELECT USER FROM {game_type}BIND WHERE QQ = {qq_number}')
user = cursor.fetchone()
db.commit()
if user is None:
return None
return user[0]
async def write_bind_info(qq_number: str | int, user: str, game_type: str) -> str:
'''写入绑定信息'''
bind_info = await query_bind_info(qq_number, game_type)
db = await get_db()
cursor = db.cursor()
if bind_info is not None:
cursor.execute(
f'UPDATE {game_type}BIND SET USER = ? WHERE QQ = ?', (user, qq_number))
message = '更新成功'
elif bind_info is None:
cursor.execute(
f'INSERT INTO {game_type}BIND (QQ, USER) VALUES (?, ?)', (qq_number, user))
message = '绑定成功'
db.commit()
return message

94
poetry.lock generated
View File

@@ -90,16 +90,24 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]] [[package]]
name = "autopep8" name = "autopep8"
version = "1.6.0" version = "1.7.0"
description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[package.dependencies] [package.dependencies]
pycodestyle = ">=2.8.0" pycodestyle = ">=2.9.1"
toml = "*" toml = "*"
[[package]]
name = "brotli"
version = "1.0.9"
description = "Python bindings for the Brotli compression library"
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.1.0" version = "2.1.0"
@@ -380,7 +388,7 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
[[package]] [[package]]
name = "pandas-stubs" name = "pandas-stubs"
version = "1.4.3.220801" version = "1.4.3.220807"
description = "Type annotations for pandas" description = "Type annotations for pandas"
category = "dev" category = "dev"
optional = false optional = false
@@ -562,7 +570,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.11.1" version = "0.11.2"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "main" category = "main"
optional = false optional = false
@@ -688,7 +696,7 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10,<3.11" python-versions = "^3.10,<3.11"
content-hash = "ee024f6b87812714d3b664289d4a47bf1228e49dc3f797f728a27d50fc169f9f" content-hash = "6548c2344f509aa1b3f45fd5ca39a875f09587b11c05eb8fda5c1da5a76f0eb7"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@@ -792,8 +800,72 @@ attrs = [
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
] ]
autopep8 = [ autopep8 = [
{file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"},
{file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"},
]
brotli = [
{file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
{file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
{file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
{file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
{file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
{file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
{file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
{file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
{file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
{file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
{file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
{file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
{file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
{file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
{file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
{file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
{file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
{file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
{file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
{file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
{file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
{file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
{file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
{file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
{file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
{file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
{file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
{file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
{file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
{file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
{file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
{file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
] ]
charset-normalizer = [ charset-normalizer = [
{file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
@@ -1304,8 +1376,8 @@ pandas = [
{file = "pandas-1.4.3.tar.gz", hash = "sha256:2ff7788468e75917574f080cd4681b27e1a7bf36461fe968b49a87b5a54d007c"}, {file = "pandas-1.4.3.tar.gz", hash = "sha256:2ff7788468e75917574f080cd4681b27e1a7bf36461fe968b49a87b5a54d007c"},
] ]
pandas-stubs = [ pandas-stubs = [
{file = "pandas-stubs-1.4.3.220801.tar.gz", hash = "sha256:16e15308ad4dab35f485c3cccdcec63be92e43395de6809435616d353e826d00"}, {file = "pandas-stubs-1.4.3.220807.tar.gz", hash = "sha256:0048410980f546c07d4ac9ecd8061dc6ecdedb26901730f9439e2f07ce812bec"},
{file = "pandas_stubs-1.4.3.220801-py3-none-any.whl", hash = "sha256:abe78bfebab257cbdf766b4974c92a592a869f5de2393a5077acab239ec30648"}, {file = "pandas_stubs-1.4.3.220807-py3-none-any.whl", hash = "sha256:ecc38adcd26cdbb40fc02a4e5b9a4837db5ac37168d562a6b25fbbcc38d1b861"},
] ]
platformdirs = [ platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
@@ -1440,8 +1512,8 @@ tomli = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
tomlkit = [ tomlkit = [
{file = "tomlkit-0.11.1-py3-none-any.whl", hash = "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5"}, {file = "tomlkit-0.11.2-py3-none-any.whl", hash = "sha256:69e0675671a2eed1c08a53f342c955c4ead5d373a10f756219bf39f3d4f0018a"},
{file = "tomlkit-0.11.1.tar.gz", hash = "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e"}, {file = "tomlkit-0.11.2.tar.gz", hash = "sha256:d1b49c3e460f5910b22d799b13513504acb4f5fcaee01660ee66f07bd45a271c"},
] ]
types-pytz = [ types-pytz = [
{file = "types-pytz-2022.1.2.tar.gz", hash = "sha256:1a8b25c225c5e6bd8468aa9eb45ddd3b337f6716d4072ad0aa4ef1e41478eebc"}, {file = "types-pytz-2022.1.2.tar.gz", hash = "sha256:1a8b25c225c5e6bd8468aa9eb45ddd3b337f6716d4072ad0aa4ef1e41478eebc"},

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot-plugin-tetris-stats" name = "nonebot-plugin-tetris-stats"
version = "0.3.0" version = "0.3.2"
description = "一个基于nonebot2的用于查询TETRIS相关游戏玩家数据的插件" description = "一个基于nonebot2的用于查询TETRIS相关游戏玩家数据的插件"
authors = ["scdhh <wallfjjd@gmail.com>"] authors = ["scdhh <wallfjjd@gmail.com>"]
readme = "README.md" readme = "README.md"
@@ -18,10 +18,11 @@ lxml = "^4.9.1"
pandas = "^1.4.3" pandas = "^1.4.3"
playwright = "^1.24.1" playwright = "^1.24.1"
ujson = "^5.4.0" ujson = "^5.4.0"
Brotli = "^1.0.9"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.971" mypy = "^0.971"
autopep8 = "^1.6.0" autopep8 = "^1.7.0"
pylint = "^2.14.5" pylint = "^2.14.5"
types-ujson = "^5.4.0" types-ujson = "^5.4.0"
lxml-stubs = "^0.4.0" lxml-stubs = "^0.4.0"