Compare commits

...

44 Commits
1.0.2 ... 1.2.2

Author SHA1 Message Date
7ff59cfc01 🔖 1.2.2 2024-05-14 17:09:53 +08:00
498781f376 ✏️ 变量名写错了 2024-05-14 17:09:29 +08:00
a3c00dbd93 🔖 1.2.1 2024-05-14 17:00:33 +08:00
069d5953f9 🐛 修复 TETR.IO User Records 解析失败的bug 2024-05-14 17:00:07 +08:00
3721d92f52 🔇 忘记删 debug 日志了 2024-05-14 16:20:57 +08:00
98b58866e1 🔖 1.2.0 2024-05-14 15:40:05 +08:00
dependabot[bot]
189c3999f7 ⬆️ Bump lxml from 5.2.1 to 5.2.2 (#316)
Bumps [lxml](https://github.com/lxml/lxml) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.2.1...lxml-5.2.2)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 15:39:31 +08:00
dependabot[bot]
a2622d5102 ⬆️ Bump types-pillow from 10.2.0.20240423 to 10.2.0.20240511 (#317)
Bumps [types-pillow](https://github.com/python/typeshed) from 10.2.0.20240423 to 10.2.0.20240511.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-pillow
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 15:04:19 +08:00
呵呵です
c8832bd1c9 ♻️ Refactor (#318)
* 🔧 启用一些 ruff 的新规则

*  添加开发依赖 nonebot-adapter-qq

*  添加依赖 nonebot-plugin-session

*  添加依赖 nonebot-plugin-session-orm

* 🔧 忽略 ruff 规则 ISC001
format 冲突风险

* 🚨 修复 ruff 警报

* ♻️ 重构!

* ♻️ 恢复定时获取 TetraLeague 数据的功能

*  统一处理需要捕获的错误

*  记录用户触发的指令
2024-05-14 15:03:46 +08:00
e6c3a32532 🔖 1.1.5 2024-05-13 04:20:02 +08:00
b3015aaa91 格式化 rating 新增千分位分隔符 2024-05-13 04:19:27 +08:00
dependabot[bot]
abc1038082 ⬆️ Bump ruff from 0.4.3 to 0.4.4 (#315)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.3...v0.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-11 00:44:54 +08:00
dependabot[bot]
dd91455890 ⬆️ Bump viztracer from 0.16.2 to 0.16.3 (#314)
Bumps [viztracer](https://github.com/gaogaotiantian/viztracer) from 0.16.2 to 0.16.3.
- [Release notes](https://github.com/gaogaotiantian/viztracer/releases)
- [Commits](https://github.com/gaogaotiantian/viztracer/compare/0.16.2...0.16.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-11 00:44:30 +08:00
dependabot[bot]
4b17b0b907 ⬆️ Bump nonebot-plugin-alconna from 0.45.3 to 0.45.4 (#311)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.45.3 to 0.45.4.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.45.3...v0.45.4)

---
updated-dependencies:
- dependency-name: nonebot-plugin-alconna
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 12:39:57 +08:00
ac4631d1f3 🔖 1.1.4 2024-05-10 12:20:48 +08:00
b0ee7fe6c7 🐛 修复 TETR.IO 默认头像传参错误 2024-05-10 12:20:25 +08:00
5bcecc0623 🔖 1.1.3 2024-05-10 12:11:19 +08:00
9cf048fce4 新增 更新模板 指令 2024-05-10 12:10:45 +08:00
aff2fa120a 🔖 1.1.2 2024-05-10 11:49:05 +08:00
1c057661c2 🐛 防止历史记录一条都没有的数组越界 2024-05-10 11:48:47 +08:00
83bcd14012 🔖 1.1.1 2024-05-10 11:30:08 +08:00
70f53a2c76 🐛 修复 init_templates 和 mount assets 的运行顺序问题 2024-05-10 11:29:46 +08:00
6df70f621e 🔖 1.1.0 2024-05-10 11:14:38 +08:00
8ba3f3c3f4 🎨 拆分函数 2024-05-10 11:13:39 +08:00
a5c4e7df5c 🐛 修正 TETR.IO Badge 模型定义 2024-05-10 09:46:09 +08:00
66db7a8a28 🚨 判断 cookie 是否拥有对应字段 2024-05-10 09:42:15 +08:00
呵呵です
716e392a3a 使用新版模板 (#313)
* 🔥 删除现有模板

*  自动克隆模板仓库

* 🔥 删除 identicon 相关代码

* 🚚 修改静态文件路径

*  使用新模板进行渲染

*  每次渲染都获取一次模板, 以应对实时更新

*  TETR.IO 绑定图使用新模板

* 🚚 修改网络路径

*  TOP 绑定图使用新模板

*  TOS 绑定图使用新模板

* 🐛 防止截图超时

* 🐛 Pydantic V1 会把 float 转换成 int

* ✏️ 模板字段名写错了

*  兼容 Pydantic V1

*  TETR.IO 查询图使用新模板

* 🐛 在查询的用户没有历史记录时不去查询更多记录
2024-05-10 09:41:05 +08:00
呵呵です
e47f1bb6f9 渲染 历史tr 曲线图 (#312) 2024-05-08 18:26:08 +08:00
03d34c5572 🔖 1.0.4 2024-05-07 17:22:46 +08:00
04b480ef52 🗃️ HistoricalData 添加 user_unique_identifier 字段 2024-05-07 17:21:52 +08:00
5563b01937 Revert " 为使用了 alias 的 pydantic model 设置 populate_by_name"
This reverts commit 17690e673f.
2024-05-07 08:51:55 +08:00
504edb08de 🔖 1.0.3 2024-05-07 08:50:53 +08:00
c283f1ca49 🗃️ 更正 HistoricalData 中的数据 2024-05-07 08:50:29 +08:00
0171953264 修改 PydanticType raise 的 Error 类型 2024-05-07 08:48:33 +08:00
7515daccc7 添加依赖 rich 2024-05-07 08:33:03 +08:00
17690e673f 为使用了 alias 的 pydantic model 设置 populate_by_name 2024-05-07 08:32:39 +08:00
e9b3c30a13 🎨 优化模型定义 2024-05-07 06:47:34 +08:00
42484b9c2c 适配 Pydantic V2 2024-05-07 06:47:34 +08:00
42828f23f6 PydanticType dump model 的时候会使用 by_alias=True 2024-05-07 06:47:34 +08:00
d0af2e83c4 更新 Metadata 的 homepage 2024-05-07 06:47:33 +08:00
5534456b22 ⬇️ 错误的使用了 Python3.11 的新特性 2024-05-07 06:47:25 +08:00
dependabot[bot]
1928506021 ⬆️ Bump nonebot-adapter-satori from 0.11.4 to 0.11.5 (#309)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.11.4 to 0.11.5.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.11.4...v0.11.5)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 06:20:54 +08:00
dependabot[bot]
67da935849 ⬆️ Bump ruff from 0.4.2 to 0.4.3 (#308)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.2...v0.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 06:19:33 +08:00
dependabot[bot]
e1e8743c48 ⬆️ Bump jinja2 from 3.1.3 to 3.1.4 (#307)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 06:17:34 +08:00
125 changed files with 3195 additions and 3875 deletions

View File

@@ -1,10 +1,12 @@
from nonebot import require
from nonebot.plugin import PluginMetadata
require('nonebot_plugin_localstore')
require('nonebot_plugin_orm')
require('nonebot_plugin_alconna')
require('nonebot_plugin_apscheduler')
require('nonebot_plugin_localstore')
require('nonebot_plugin_orm')
require('nonebot_plugin_session')
require('nonebot_plugin_session_orm')
from .config.config import migrations # noqa: E402
@@ -13,7 +15,7 @@ __plugin_meta__ = PluginMetadata(
description='一个用于查询 Tetris 相关游戏玩家数据的插件',
usage='发送 {游戏名} --help 查询使用方法',
type='application',
homepage='https://github.com/shoucandanghehe/nonebot-plugin-tetris-stats',
homepage='https://github.com/A-minos/nonebot-plugin-tetris-stats',
extra={
'orm_version_location': migrations,
},

View File

@@ -8,11 +8,14 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '09d4bb60160d'
down_revision: str | Sequence[str] | None = 'b9d65badc713'
branch_labels: str | Sequence[str] | None = None

View File

@@ -8,11 +8,14 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '0d50142b780f'
down_revision: str | Sequence[str] | None = '09d4bb60160d'
branch_labels: str | Sequence[str] | None = None

View File

@@ -0,0 +1,279 @@
"""Refactor Historical
迁移 ID: 3c25a5a8c050
父迁移: b7fbdafc339a
创建时间: 2024-05-14 09:16:35.193001
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, Any
import sqlalchemy as sa
from alembic import op
from nonebot.log import logger
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn
from sqlalchemy import desc, select
from sqlalchemy.dialects import sqlite
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '3c25a5a8c050'
down_revision: str | Sequence[str] | None = 'b7fbdafc339a'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def migrate_old_data() -> None:
Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind())
OldHistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
TETRIOHistoricalData = Base.classes.nonebot_plugin_tetris_stats_tetriohistoricaldata # noqa: N806
TOSHistoricalData = Base.classes.nonebot_plugin_tetris_stats_toshistoricaldata # noqa: N806
with (
Session(op.get_bind()) as session,
Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),
MofNCompleteColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress,
):
task_id = progress.add_task('[cyan]Migrating:', total=session.query(OldHistoricalData).count())
pointer = 0
while pointer < session.query(OldHistoricalData).order_by(desc(OldHistoricalData.id)).limit(1).one().id:
result = session.scalars(
select(OldHistoricalData)
.where(OldHistoricalData.id > pointer)
.order_by(OldHistoricalData.id)
.limit(100)
).all()
for j in result:
processed_data: dict[str, Any] = loads(j.processed_data)
if j.game_platform == 'IO':
if (data := processed_data.get('user_info')) is not None:
session.add(
TETRIOHistoricalData(
user_unique_identifier=j.user_unique_identifier,
api_type='User Info',
data=dumps(data),
update_time=datetime.fromisoformat(data['cache']['cached_at']),
)
)
if (data := processed_data.get('user_records')) is not None:
session.add(
TETRIOHistoricalData(
user_unique_identifier=j.user_unique_identifier,
api_type='User Records',
data=dumps(data),
update_time=datetime.fromisoformat(data['cache']['cached_at']),
)
)
if j.game_platform == 'TOS' and not j.user_unique_identifier.isdigit():
if (data := processed_data.get('user_info')) is not None:
session.add(
TOSHistoricalData(
user_unique_identifier=j.user_unique_identifier,
api_type='User Info',
data=dumps(data),
update_time=j.finish_time,
)
)
if (data := processed_data.get('user_profile')) is not None:
for v in data.values():
session.add(
TOSHistoricalData(
user_unique_identifier=j.user_unique_identifier,
api_type='User Profile',
data=dumps(v),
update_time=j.finish_time,
)
)
progress.update(task_id, advance=1)
session.commit()
pointer = result[-1].id
logger.success('Migrate successfully')
def upgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'nonebot_plugin_tetris_stats_tetriohistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_unique_identifier', sa.String(length=24), nullable=False),
sa.Column('api_type', sa.String(length=16), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetriohistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type'), ['api_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time'), ['update_time'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
op.create_table(
'nonebot_plugin_tetris_stats_tophistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_unique_identifier', sa.String(length=24), nullable=False),
sa.Column('api_type', sa.String(length=16), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tophistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tophistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_api_type'), ['api_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_update_time'), ['update_time'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
op.create_table(
'nonebot_plugin_tetris_stats_toshistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_unique_identifier', sa.String(length=24), nullable=False),
sa.Column('api_type', sa.String(length=16), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_toshistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_toshistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_api_type'), ['api_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_update_time'), ['update_time'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
op.create_table(
'nonebot_plugin_tetris_stats_triggerhistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('trigger_time', sa.DateTime(), nullable=False),
sa.Column('session_persist_id', sa.Integer(), nullable=False),
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('finish_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_triggerhistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_triggerhistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_triggerhistoricaldata_command_type'),
['command_type'],
unique=False,
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_triggerhistoricaldata_game_platform'),
['game_platform'],
unique=False,
)
migrate_old_data()
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_command_type')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_game_platform')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_source_account')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_source_type')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier')
op.drop_table('nonebot_plugin_tetris_stats_historicaldata')
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'nonebot_plugin_tetris_stats_historicaldata',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('trigger_time', sa.DATETIME(), nullable=False),
sa.Column('bot_platform', sa.VARCHAR(length=32), nullable=True),
sa.Column('bot_account', sa.VARCHAR(), nullable=True),
sa.Column('source_type', sa.VARCHAR(length=32), nullable=True),
sa.Column('source_account', sa.VARCHAR(), nullable=True),
sa.Column('message', sa.BLOB(), nullable=True),
sa.Column('game_platform', sa.VARCHAR(length=32), nullable=False),
sa.Column('command_type', sa.VARCHAR(length=16), nullable=False),
sa.Column('command_args', sqlite.JSON(), nullable=False),
sa.Column('game_user', sqlite.JSON(), nullable=False),
sa.Column('processed_data', sqlite.JSON(), nullable=False),
sa.Column('finish_time', sa.DATETIME(), nullable=False),
sa.Column('user_unique_identifier', sa.VARCHAR(length=32), nullable=False),
sa.PrimaryKeyConstraint('id', name='pk_nonebot_plugin_tetris_stats_historicaldata'),
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier',
['user_unique_identifier'],
unique=False,
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_source_type', ['source_type'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_source_account', ['source_account'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_game_platform', ['game_platform'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_command_type', ['command_type'], unique=False
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_triggerhistoricaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_triggerhistoricaldata_game_platform'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_triggerhistoricaldata_command_type'))
op.drop_table('nonebot_plugin_tetris_stats_triggerhistoricaldata')
with op.batch_alter_table('nonebot_plugin_tetris_stats_toshistoricaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_user_unique_identifier'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_update_time'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_toshistoricaldata_api_type'))
op.drop_table('nonebot_plugin_tetris_stats_toshistoricaldata')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tophistoricaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_user_unique_identifier'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_update_time'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tophistoricaldata_api_type'))
op.drop_table('nonebot_plugin_tetris_stats_tophistoricaldata')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type'))
op.drop_table('nonebot_plugin_tetris_stats_tetriohistoricaldata')
# ### end Alembic commands ###

View File

@@ -5,15 +5,19 @@
创建时间: 2023-11-26 20:15:56.033892
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from alembic import op
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '6c3206f90cc3'
down_revision: str | Sequence[str] | None = '9f6582279ce2'
branch_labels: str | Sequence[str] | None = None

View File

@@ -0,0 +1,96 @@
"""Correct the data in HistoricalData
迁移 ID: 8a91210ce14d
父迁移: 0d50142b780f
创建时间: 2024-05-06 08:16:38.487214
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from alembic import op
from nonebot.log import logger
from sqlalchemy import select
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '8a91210ce14d'
down_revision: str | Sequence[str] | None = '0d50142b780f'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None: # noqa: C901
if name:
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.3':
msg = '本迁移需要1.0.3版本, 请先锁定版本至1.0.3版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot.compat import PYDANTIC_V2, type_validate_json
from pydantic import BaseModel, ValidationError
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseProcessedData # type: ignore[attr-defined]
Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind())
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
if PYDANTIC_V2:
def model_to_json(value: BaseModel) -> str:
return value.model_dump_json(by_alias=True)
else:
def model_to_json(value: BaseModel) -> str:
return value.json(by_alias=True)
models = BaseProcessedData.__subclasses__()
def json_to_model(value: str) -> BaseModel:
for i in models:
try:
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise ValueError
with Session(op.get_bind()) as session:
count = session.query(HistoricalData).count()
with Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),
MofNCompleteColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task_id = progress.add_task('[cyan]Updateing:', total=count)
for i in range(0, count, 100):
for j in session.scalars(
select(HistoricalData).where(HistoricalData.id > i).order_by(HistoricalData.id).limit(100)
):
model = json_to_model(j.processed_data)
j.processed_data = model_to_json(model)
progress.update(task_id, advance=1)
session.commit()
logger.success('Corrected HistoricalData')
def downgrade(name: str = '') -> None:
if name:
return

View File

@@ -5,13 +5,17 @@
创建时间: 2023-11-11 16:24:11.826667
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '9866f53ce44f'
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = ('nonebot_plugin_tetris_stats',)

View File

@@ -5,11 +5,12 @@
创建时间: 2023-11-11 16:51:30.718277
"""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from shutil import copyfile
from typing import TYPE_CHECKING
from alembic import op
from nonebot import get_driver
@@ -18,6 +19,9 @@ from sqlalchemy import Connection, create_engine, inspect, text
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '9cd1647db502'
down_revision: str | Sequence[str] | None = '9866f53ce44f'
branch_labels: str | Sequence[str] | None = None
@@ -80,8 +84,9 @@ def upgrade(name: str = '') -> None:
logger.success('nonebot_plugin_tetris_stats: 跳过迁移')
return
if 'IORANK' not in tables:
logger.warning('nonebot_plugin_tetris_stats: 发现过早版本的数据, 请先更新到 0.4.4 版本')
raise RuntimeError('nonebot_plugin_tetris_stats: 请先安装 0.4.4 版本完成迁移之后再升级')
msg = 'nonebot_plugin_tetris_stats: 请先安装 0.4.4 版本完成迁移之后再升级'
logger.warning(msg)
raise RuntimeError(msg)
logger.info('nonebot_plugin_tetris_stats: 发现来自老版本的数据, 正在迁移...')
migrate_old_data(connection)
db_path.unlink()

View File

@@ -5,15 +5,17 @@
创建时间: 2023-11-21 08:35:50.393246
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import sqlite
from nonebot_plugin_tetris_stats.db.models import PydanticType
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '9f6582279ce2'
down_revision: str | Sequence[str] | None = '9cd1647db502'
@@ -45,8 +47,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(list), nullable=False),
sa.Column('processed_data', PydanticType(list), nullable=False),
sa.Column('game_user', sa.JSON(), nullable=False),
sa.Column('processed_data', sa.JSON(), nullable=False),
sa.Column('finish_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_historicaldata')),
)

View File

@@ -0,0 +1,107 @@
"""Add user_unique_identifier field to HistoricalData
迁移 ID: b7fbdafc339a
父迁移: 8a91210ce14d
创建时间: 2024-05-07 16:55:29.527215
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from nonebot.log import logger
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = 'b7fbdafc339a'
down_revision: str | Sequence[str] | None = '8a91210ce14d'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.4':
msg = '本迁移需要1.0.4版本, 请先锁定版本至1.0.4版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot.compat import type_validate_json
from pydantic import ValidationError
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from sqlalchemy import select
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseUser
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_unique_identifier', sa.String(length=32), nullable=True))
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
Base = automap_base() # noqa: N806
connection = op.get_bind()
Base.prepare(autoload_with=connection)
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
models: list[type[BaseUser]] = BaseUser.__subclasses__()
def json_to_model(value: str) -> BaseUser:
for i in models:
try:
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise ValueError
with Session(op.get_bind()) as session:
count = session.query(HistoricalData).count()
with Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),
MofNCompleteColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task_id = progress.add_task('[cyan]Updateing:', total=count)
for i in range(0, count, 100):
for j in session.scalars(
select(HistoricalData).where(HistoricalData.id > i).order_by(HistoricalData.id).limit(100)
):
model = json_to_model(j.game_user)
try:
j.user_unique_identifier = model.unique_identifier
except ValueError:
session.delete(j)
progress.update(task_id, advance=1)
session.commit()
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.alter_column('user_unique_identifier', existing_type=sa.VARCHAR(length=32), nullable=False)
logger.success('database upgrade success')
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier'))
batch_op.drop_column('user_unique_identifier')
# ### end Alembic commands ###

View File

@@ -5,14 +5,18 @@
创建时间: 2023-12-30 00:27:40.991704
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from alembic import op
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = 'b9d65badc713'
down_revision: str | Sequence[str] | None = '6c3206f90cc3'
branch_labels: str | Sequence[str] | None = None

View File

@@ -1,13 +1,26 @@
from enum import StrEnum, auto
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from enum import Enum, auto
from typing import TYPE_CHECKING, Literal, TypeVar, overload
from nonebot_plugin_orm import AsyncSession
from nonebot.exception import FinishedException
from nonebot.log import logger
from nonebot_plugin_orm import AsyncSession, get_session
from sqlalchemy import select
from ..utils.typing import GameType
from .models import Bind
from ..utils.typing import CommandType, GameType
from .models import Bind, TriggerHistoricalData
UTC = timezone.utc
if TYPE_CHECKING:
from ..game_data_processor.io_data_processor.api.models import TETRIOHistoricalData
from ..game_data_processor.top_data_processor.api.models import TOPHistoricalData
from ..game_data_processor.tos_data_processor.api.models import TOSHistoricalData
class BindStatus(StrEnum):
class BindStatus(Enum):
SUCCESS = auto()
UPDATE = auto()
@@ -55,3 +68,73 @@ async def create_or_update_bind(
message = BindStatus.UPDATE
await session.commit()
return message
T = TypeVar('T', 'TETRIOHistoricalData', 'TOPHistoricalData', 'TOSHistoricalData')
async def anti_duplicate_add(cls: type[T], model: T) -> None:
async with get_session() as session:
result = (
await session.scalars(
select(cls)
.where(cls.update_time == model.update_time)
.where(cls.user_unique_identifier == model.user_unique_identifier)
.where(cls.api_type == model.api_type)
)
).all()
if result:
for i in result:
if i.data == model.data:
logger.debug('Anti duplicate successfully')
return
session.add(model)
await session.commit()
@asynccontextmanager
@overload
async def trigger(
session_persist_id: int,
game_platform: Literal['IO'],
command_type: CommandType | Literal['rank'],
command_args: list[str],
) -> AsyncGenerator:
yield
@asynccontextmanager
@overload
async def trigger(
session_persist_id: int,
game_platform: GameType,
command_type: CommandType,
command_args: list[str],
) -> AsyncGenerator:
yield
@asynccontextmanager
async def trigger(
session_persist_id: int,
game_platform: GameType,
command_type: CommandType | Literal['rank'],
command_args: list[str],
) -> AsyncGenerator:
trigger_time = datetime.now(UTC)
try:
yield
except FinishedException:
async with get_session() as session:
session.add(
TriggerHistoricalData(
trigger_time=trigger_time,
session_persist_id=session_persist_id,
game_platform=game_platform,
command_type=command_type,
command_args=command_args,
finish_time=datetime.now(UTC),
)
)
await session.commit()
raise

View File

@@ -1,16 +1,14 @@
from collections.abc import Callable, Sequence
from datetime import datetime
from typing import Any
from typing import Any, Literal
from nonebot.adapters import Message
from nonebot.compat import type_validate_json
from nonebot.compat import PYDANTIC_V2, type_validate_json
from nonebot_plugin_orm import Model
from pydantic import BaseModel, ValidationError
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
from sqlalchemy import JSON, DateTime, Dialect, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from typing_extensions import override
from ..game_data_processor.schemas import BaseProcessedData, BaseUser
from ..utils.typing import CommandType, GameType
@@ -18,27 +16,45 @@ class PydanticType(TypeDecorator):
impl = JSON
@override
def __init__(self, get_model: Callable[[], Sequence[type[BaseModel]]], *args: Any, **kwargs: Any):
self.get_model = get_model
def __init__(
self,
get_model: Sequence[Callable[[], Sequence[type[BaseModel]]]],
models: set[type[BaseModel]],
*args: Any,
**kwargs: Any,
):
for i in get_model:
models.update(i())
self.models = models
super().__init__(*args, **kwargs)
@override
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str:
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.get_model())):
return value.json() # type: ignore[union-attr]
raise TypeError
if PYDANTIC_V2:
@override
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str:
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.models)):
return value.model_dump_json(by_alias=True) # type: ignore[union-attr]
raise TypeError
else:
@override
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str:
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.models)):
return value.json(by_alias=True) # type: ignore[union-attr]
raise TypeError
@override
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel:
# 将 JSON 转换回 Pydantic 模型实例
if isinstance(value, str | bytes):
for i in self.get_model():
for i in self.models:
try:
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise TypeError
raise ValueError
class Bind(MappedAsDataclass, Model):
@@ -49,19 +65,11 @@ class Bind(MappedAsDataclass, Model):
game_account: Mapped[str]
class HistoricalData(MappedAsDataclass, Model):
class TriggerHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
trigger_time: Mapped[datetime] = mapped_column(DateTime)
bot_platform: Mapped[str | None] = mapped_column(String(32))
bot_account: Mapped[str | None]
source_type: Mapped[str | None] = mapped_column(String(32), index=True)
source_account: Mapped[str | None] = mapped_column(index=True)
message: Mapped[Message | None] = mapped_column(PickleType)
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(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)
session_persist_id: Mapped[int]
game_platform: Mapped[GameType] = mapped_column(String(32), index=True)
command_type: Mapped[CommandType | Literal['rank']] = mapped_column(String(16), index=True)
command_args: Mapped[list[str]] = mapped_column(JSON)
finish_time: Mapped[datetime] = mapped_column(DateTime)

View File

@@ -1,80 +1,20 @@
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any
from nonebot.adapters import Bot
from nonebot.exception import FinishedException
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, AlconnaMatcher
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from nonebot.message import run_postprocessor
from nonebot_plugin_alconna import AlcMatches, AlconnaMatcher, At
from ..utils.exception import MessageFormatError
from ..utils.recorder import Recorder
from ..utils.typing import CommandType, GameType
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
command_type: CommandType
command_args: list[str]
user: User
raw_response: RawResponse
processed_data: ProcessedData
@abstractmethod
def __init__(
self,
event_id: int,
user: User,
command_args: list[str],
) -> None:
self.event_id = event_id
self.user = user
self.command_args = command_args
@property
@abstractmethod
def game_platform(self) -> GameType:
"""游戏平台"""
raise NotImplementedError
@abstractmethod
async def handle_bind(
self,
platform: str,
account: str,
bot_info: UserInfo,
*args: Any, # noqa: ANN401
**kwargs: Any, # noqa: ANN401
) -> UniMessage:
"""处理绑定消息"""
raise NotImplementedError
@abstractmethod
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
raise NotImplementedError
def __del__(self) -> None:
finish_time = datetime.now(tz=UTC)
if Recorder.is_error_event(self.event_id):
Recorder.del_error_event(self.event_id)
return
historical_data = Recorder.get_historical_data(self.event_id)
historical_data.game_platform = self.game_platform
historical_data.command_type = self.command_type
historical_data.command_args = self.command_args
historical_data.game_user = self.user
historical_data.processed_data = self.processed_data
historical_data.finish_time = finish_time
Recorder.update_historical_data(self.event_id, historical_data)
from ..utils.exception import MessageFormatError, NeedCatchError
def add_default_handlers(matcher: type[AlconnaMatcher]) -> None:
@matcher.assign('query')
async def _(bot: Bot, matcher: Matcher, target: At):
if isinstance(target, At) and target.target == bot.self_id:
await matcher.finish('不能查询bot的信息')
@matcher.handle()
async def _(matcher: Matcher, account: MessageFormatError):
await matcher.finish(str(account))
@@ -88,8 +28,8 @@ def add_default_handlers(matcher: type[AlconnaMatcher]) -> None:
)
@matcher.handle()
async def _(matcher: Matcher, other: Any): # noqa: ANN401
await matcher.finish()
def _(other: Any): # noqa: ANN401, ARG001
raise FinishedException
from . import ( # noqa: F401, E402
@@ -97,3 +37,8 @@ from . import ( # noqa: F401, E402
top_data_processor,
tos_data_processor,
)
@run_postprocessor
async def _(matcher: Matcher, exception: NeedCatchError):
await matcher.send(str(exception))

View File

@@ -1,2 +1,3 @@
BIND_COMMAND: list[str] = ['绑定', 'bind']
QUERY_COMMAND: list[str] = ['', '查询', 'query', 'stats']
CANT_VERIFY_MESSAGE = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'

View File

@@ -1,28 +1,22 @@
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from sqlalchemy import func, select
from ...db import query_bind_info
from ...utils.exception import HandleNotFinishedError, NeedCatchError
from ...utils.metrics import get_metrics
from ...utils.platform import get_platform
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE
from .model import IORank
from .processor import Processor, User, identify_user_info
from .typing import Rank
from .api import Player
from .api.typing import Rank
from .constant import USER_ID, USER_NAME
def get_player(user_id_or_name: str) -> Player | MessageFormatError:
if USER_ID.match(user_id_or_name):
return Player(user_id=user_id_or_name, trust=True)
if USER_NAME.match(user_id_or_name):
return Player(user_name=user_id_or_name, trust=True)
return MessageFormatError('用户名/ID不合法')
UTC = timezone.utc
alc = on_alconna(
Alconna(
@@ -32,7 +26,7 @@ alc = on_alconna(
Args(
Arg(
'account',
identify_user_info,
get_player,
notice='IO 用户名 / ID',
flags=[ArgFlag.HIDDEN],
)
@@ -53,7 +47,7 @@ alc = on_alconna(
),
Arg(
'account',
identify_user_info,
get_player,
notice='IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
@@ -84,113 +78,8 @@ alc = on_alconna(
aliases={'IO'},
)
alc.shortcut('fkosk', {'command': 'io查', 'args': ['']})
@alc.assign('bind')
async def _(bot: Bot, event: Event, matcher: Matcher, account: User, bot_info: UserInfo = BotUserInfo()): # noqa: B008
proc = Processor(event_id=id(event), user=account, command_args=[])
try:
await (
await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
proc = Processor(
event_id=id(event),
user=User(ID=bind.game_account),
command_args=[],
)
try:
await (UniMessage(message) + await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(event: Event, matcher: Matcher, account: User):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('rank')
async def _(matcher: Matcher, rank: Rank):
if rank == 'z':
await matcher.finish('暂不支持查询未知段位')
async with get_session() as session:
latest_data = (
await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(1))
).one()
compare_data = (
await session.scalars(
select(IORank)
.where(IORank.rank == rank)
.order_by(
func.abs(
func.julianday(IORank.update_time)
- func.julianday(latest_data.update_time - timedelta(hours=24))
)
)
.limit(1)
)
).one()
message = ''
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.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)
low_pps = get_metrics(pps=latest_data.low_pps[1])
low_vs = get_metrics(vs=latest_data.low_vs[1])
max_pps = get_metrics(pps=latest_data.high_pps[1])
max_vs = get_metrics(vs=latest_data.high_vs[1])
message += (
'\n'
'平均数据:\n'
f"L'PM: {avg.lpm} ( {avg.pps} pps )\n"
f'APM: {avg.apm} ( x{avg.apl} )\n'
f'ADPM: {avg.adpm} ( x{avg.adpl} ) ( {avg.vs}vs )\n'
'\n'
'最低数据:\n'
f"L'PM: {low_pps.lpm} ( {low_pps.pps} pps ) By: {latest_data.low_pps[0]['name'].upper()}\n"
f'APM: {latest_data.low_apm[1]} By: {latest_data.low_apm[0]["name"].upper()}\n'
f'ADPM: {low_vs.adpm} ( {low_vs.vs}vs ) By: {latest_data.low_vs[0]["name"].upper()}\n'
'\n'
'最高数据:\n'
f"L'PM: {max_pps.lpm} ( {max_pps.pps} pps ) By: {latest_data.high_pps[0]['name'].upper()}\n"
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.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
)
await matcher.finish(message)
alc.shortcut('fkosk', {'command': 'io查', 'args': [''], 'fuzzy': False, 'humanized': 'An Easter egg!'})
from . import bind, query, rank # noqa: F401, E402
add_default_handlers(alc)

View File

@@ -0,0 +1,7 @@
from .player import Player
from .schemas.user import User
from .schemas.user_info import UserInfoSuccess
from .schemas.user_records import UserRecordsSuccess
from .tetra_league import full_export as tetra_league_full_export
__all__ = ['Player', 'User', 'UserInfoSuccess', 'UserRecordsSuccess', 'tetra_league_full_export']

View File

@@ -1,10 +1,13 @@
from asyncio import Lock
from datetime import datetime, timezone
from typing import ClassVar
from weakref import WeakValueDictionary
from aiocache import Cache as ACache # type: ignore[import-untyped]
from nonebot.compat import type_validate_json
from nonebot.log import logger
from ...utils.request import Request
from ....utils.request import Request
from .schemas.base import FailedModel, SuccessModel
UTC = timezone.utc
@@ -12,11 +15,15 @@ UTC = timezone.utc
class Cache:
cache = ACache(ACache.MEMORY)
task: ClassVar[WeakValueDictionary[str, Lock]] = WeakValueDictionary()
@classmethod
async def get(cls, url: str) -> bytes:
cached_data = await cls.cache.get(url)
if cached_data is None:
lock = cls.task.setdefault(url, Lock())
async with lock:
if (cached_data := await cls.cache.get(url)) is not None:
logger.debug(f'{url}: Cache hit!')
return cached_data
response_data = await Request.request(url)
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
if isinstance(parsed_data, SuccessModel):
@@ -26,5 +33,3 @@ class Cache:
(parsed_data.cache.cached_until - datetime.now(UTC)).total_seconds(),
)
return response_data
logger.debug(f'{url}: Cache hit!')
return cached_data

View File

@@ -0,0 +1,17 @@
from datetime import datetime
from typing import Literal
from nonebot_plugin_orm import Model
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType
from .schemas.base import SuccessModel
class TETRIOHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True)
api_type: Mapped[Literal['User Info', 'User Records']] = mapped_column(String(16), index=True)
data: Mapped[SuccessModel] = mapped_column(PydanticType(get_model=[SuccessModel.__subclasses__], models=set()))
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)

View File

@@ -0,0 +1,102 @@
from typing import overload
from nonebot.compat import type_validate_json
from ....db import anti_duplicate_add
from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL, USER_ID, USER_NAME
from .cache import Cache
from .models import TETRIOHistoricalData
from .schemas.base import FailedModel
from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess
from .schemas.user_records import UserRecords, UserRecordsSuccess
class Player:
@overload
def __init__(self, *, user_id: str, trust: bool = False): ...
@overload
def __init__(self, *, user_name: str, trust: bool = False): ...
def __init__(self, *, user_id: str | None = None, user_name: str | None = None, trust: bool = False):
self.user_id = user_id
self.user_name = user_name
if not trust:
if self.user_id is not None:
if not USER_ID.match(self.user_id):
msg = 'Invalid user id'
raise ValueError(msg)
elif self.user_name is not None:
if not USER_NAME.match(self.user_name):
msg = 'Invalid user name'
raise ValueError(msg)
else:
msg = 'Invalid user'
raise ValueError(msg)
self.__user: User | None = None
self._user_info: UserInfoSuccess | None = None
self._user_records: UserRecordsSuccess | None = None
@property
def _request_user_parameter(self) -> str:
if self.user_id is not None:
return self.user_id
if self.user_name is not None:
return self.user_name
msg = 'Invalid user'
raise ValueError(msg)
@property
async def user(self) -> User:
if self.__user is None:
user_info = await self.get_info()
self.__user = User(
ID=user_info.data.user.id,
name=user_info.data.user.username,
)
self.user_id = user_info.data.user.id
self.user_name = user_info.data.user.username
return self.__user
async def get_info(self) -> UserInfoSuccess:
"""Get User Info"""
if self._user_info is None:
raw_user_info = await Cache.get(splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}']))
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if isinstance(user_info, FailedModel):
msg = f'用户信息请求错误:\n{user_info.error}'
raise RequestError(msg)
self._user_info = user_info
await anti_duplicate_add(
TETRIOHistoricalData,
TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Info',
data=user_info,
update_time=user_info.cache.cached_at,
),
)
return self._user_info
async def get_records(self) -> UserRecordsSuccess:
"""Get User Records"""
if self._user_records is None:
raw_user_records = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}/', 'records'])
)
user_records: UserRecords = type_validate_json(UserRecords, raw_user_records) # type: ignore[arg-type]
if isinstance(user_records, FailedModel):
msg = f'用户Solo数据请求错误:\n{user_records.error}'
raise RequestError(msg)
self._user_records = user_records
await anti_duplicate_add(
TETRIOHistoricalData,
TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Records',
data=user_records,
update_time=user_records.cache.cached_at,
),
)
return self._user_records

View File

@@ -0,0 +1,59 @@
from pydantic import BaseModel, Field
from ..typing import Rank
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class _User(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
supporter: bool
verified: bool
country: str | None = None
class _League(BaseModel):
gamesplayed: int
gameswon: int
rating: float
rank: Rank
bestrank: Rank
decaying: bool
class ValidLeague(_League):
glicko: float
rd: float
apm: float
pps: float
vs: float
class ValidUser(_User):
league: ValidLeague
class InvalidLeague(_League):
glicko: float | None = None
rd: float | None = None
apm: float | None = None
pps: float | None = None
vs: float | None = None
class InvalidUser(_User):
league: InvalidLeague
class Data(BaseModel):
users: list[ValidUser | InvalidUser]
class TetraLeagueSuccess(BaseSuccessModel):
data: Data
TetraLeague = TetraLeagueSuccess | FailedModel

View File

@@ -0,0 +1,18 @@
from typing import Literal
from typing_extensions import override
from ....schemas import BaseUser
from ...constant import GAME_TYPE
class User(BaseUser):
platform: Literal['IO'] = GAME_TYPE
ID: str
name: str
@property
@override
def unique_identifier(self) -> str:
return self.ID

View File

@@ -0,0 +1,133 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
from ..typing import Rank
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class Badge(BaseModel):
id: str
label: str
group: str | None = None
ts: datetime | Literal[False] | None = None
class MetaLeague(BaseModel):
decaying: bool
class NeverPlayedLeague(MetaLeague):
gamesplayed: Literal[0]
gameswon: Literal[0]
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: None = None
pps: None = None
vs: None = None
class NeverRatedLeague(MetaLeague):
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
gameswon: int
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: float
pps: float
vs: float | None = None
class RatedLeague(MetaLeague):
gamesplayed: int
gameswon: int
rating: float
rank: Rank
bestrank: Rank
standing: int
standing_local: int
next_rank: Rank | None = None
prev_rank: Rank | None = None
next_at: int
prev_at: int
percentile: float
percentile_rank: str
glicko: float
rd: float
apm: float
pps: float
vs: float | None = None
class Discord(BaseModel):
id: str
username: str
class Connections(BaseModel):
discord: Discord | None = None
class Distinguishment(BaseModel):
type: str
class User(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
ts: datetime | None = None
botmaster: str | None = None
badges: list[Badge]
xp: float
gamesplayed: int
gameswon: int
gametime: float
country: str | None = None
badstanding: bool | None = None
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int
verified: bool
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
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 = 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 = None
connections: Connections
friend_count: int | None = None
distinguishment: Distinguishment | None = None
class Data(BaseModel):
user: User
class UserInfoSuccess(BaseSuccessModel):
data: Data
UserInfo = UserInfoSuccess | FailedModel

View File

@@ -0,0 +1,122 @@
from datetime import datetime
from pydantic import BaseModel, Field
from .....utils.typing import Number
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class Time(BaseModel):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int | None = None
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
pentas: int | None = None
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int | None = None
cleared: int
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class EndContext(BaseModel):
seed: Number
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int | None = None
time: Time
score: int
zenlevel: int | None = None
zenprogress: int | None = None
level: int
combo: int
currentcombopower: int | None = None # WTF
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None # WTF * 2
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
final_time: float = Field(..., alias='finalTime')
gametype: str
class _User(BaseModel):
id: str = Field(..., alias='_id')
username: str
class _Record(BaseModel):
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: _User
ts: datetime
ismulti: bool | None = None
class SoloRecord(_Record):
endcontext: EndContext
class MultiRecord(_Record):
endcontext: list[EndContext]
class SoloModeRecord(BaseModel):
record: SoloRecord
rank: int | None = None
class Records(BaseModel):
sprint: SoloModeRecord = Field(..., alias='40l')
blitz: SoloModeRecord
class Zen(BaseModel):
level: int
score: int
class Data(BaseModel):
records: Records
zen: Zen
class UserRecordsSuccess(BaseSuccessModel):
data: Data
UserRecords = UserRecordsSuccess | FailedModel

View File

@@ -0,0 +1,36 @@
from typing import Literal, NamedTuple, overload
from nonebot.compat import type_validate_json
from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL
from .cache import Cache
from .schemas.base import FailedModel
from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess
class FullExport(NamedTuple):
model: TetraLeagueSuccess
original: bytes
@overload
async def full_export(*, with_original: Literal[False]) -> TetraLeagueSuccess: ...
@overload
async def full_export(*, with_original: Literal[True]) -> FullExport: ...
async def full_export(*, with_original: bool) -> TetraLeagueSuccess | FullExport:
full: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type]
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
)
if isinstance(full, FailedModel):
msg = f'排行榜数据请求错误:\n{full.error}'
raise RequestError(msg)
if with_original:
return FullExport(full, data)
return full

View File

@@ -0,0 +1,64 @@
from hashlib import md5
from urllib.parse import urlunparse
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.avatar import get_avatar
from ...utils.host import HostPage, get_self_netloc
from ...utils.platform import get_platform
from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot
from . import alc
from .api import Player
from .constant import GAME_TYPE
@alc.assign('bind')
async def _(bot: Bot, event: Event, account: Player, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='bind',
command_args=[],
):
user = await account.user
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=get_platform(bot),
chat_account=event.get_user_id(),
game_platform=GAME_TYPE,
game_account=user.unique_identifier,
)
user_info = await account.get_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform='TETR.IO',
status='unknown',
user=People(
avatar=f'https://tetr.io/user-content/avatars/{user_info.data.user.id}.jpg?rv={user_info.data.user.avatar_revision}'
if user_info.data.user.avatar_revision is not None
else Avatar(type='identicon', hash=md5(user_info.data.user.id.encode()).hexdigest()), # noqa: S324
name=user_info.data.user.username.upper(),
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='io查我',
),
)
) as page_hash:
await UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
).finish()

View File

@@ -1,9 +1,12 @@
from re import compile
from typing import Literal
from .typing import Rank
from .api.typing import Rank
GAME_TYPE: Literal['IO'] = 'IO'
BASE_URL = 'https://ch.tetr.io/api/'
RANK_PERCENTILE: dict[Rank, float] = {
'x': 1,
'u': 5,
@@ -23,3 +26,9 @@ RANK_PERCENTILE: dict[Rank, float] = {
'd+': 97.5,
'd': 100,
}
TR_MIN = 0
TR_MAX = 25000
USER_ID = compile(r'^[a-f0-9]{24}$')
USER_NAME = compile(r'^[a-zA-Z0-9_-]{3,16}$')

View File

@@ -1,12 +1,10 @@
from datetime import datetime, timezone
from datetime import datetime
from nonebot_plugin_orm import Model
from sqlalchemy import JSON, DateTime, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from .typing import Rank
UTC = timezone.utc
from .api.typing import Rank
class IORank(MappedAsDataclass, Model):

View File

@@ -1,324 +0,0 @@
from asyncio import gather
from collections import defaultdict
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from hashlib import md5, sha512
from math import floor
from re import match
from statistics import mean
from typing import Literal
from urllib.parse import urlunparse
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_alconna.uniseg import UniMessage
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 nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped]
from sqlalchemy import select
from typing_extensions import override
from zstandard import ZstdCompressor
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import render
from ...utils.request import splice_url
from ...utils.retry import retry
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from .cache import Cache
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
from .model import IORank
from .schemas.base import FailedModel
from .schemas.league_all import LeagueAll
from .schemas.league_all import ValidUser as LeagueAllUser
from .schemas.response import ProcessedData, RawResponse
from .schemas.user import User
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague, UserInfo
from .schemas.user_info import SuccessModel as InfoSuccess
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()
def identify_user_info(info: str) -> User | MessageFormatError:
if match(r'^[a-f0-9]{24}$', info):
return User(ID=info)
if match(r'^[a-zA-Z0-9_-]{3,16}$', info):
return User(name=info.lower())
return MessageFormatError('用户名/ID不合法')
class Processor(ProcessorMeta):
user: User
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse()
self.processed_data = ProcessedData()
@property
@override
def game_platform(self) -> Literal['IO']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.get_user()
if self.user.ID is None:
raise # FIXME: 不知道怎么才能把这类型给变过来了
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.ID,
)
bot_avatar = await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg')
user_info = await self.get_user_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'bind.j2.html',
user_avatar=f'https://tetr.io/user-content/avatars/{user_info.data.user.id}.jpg?rv={user_info.data.user.avatar_revision}'
if user_info.data.user.avatar_revision is not None
else f'../../identicon?md5={md5(user_info.data.user.id.encode()).hexdigest()}', # noqa: S324
state='unknown',
bot_avatar=bot_avatar,
game_type=self.game_platform,
user_name=user_info.data.user.username.upper(),
bot_name=bot_info.user_name,
command='io查我',
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
)
)
return message
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
await self.get_user()
user_info, user_records = await gather(self.get_user_info(), self.get_user_records())
user_name = user_info.data.user.username.upper()
league = user_info.data.user.league
sprint = user_records.data.records.sprint
blitz = user_records.data.records.blitz
if isinstance(league, RatedLeague) and league.vs is not None:
if sprint.record is None:
sprint_value = 'N/A'
else:
if not isinstance(sprint.record, SoloRecord):
raise WhatTheFuckError('40L记录不是单人记录')
duration = timedelta(milliseconds=sprint.record.endcontext.final_time).total_seconds()
sprint_value = f'{duration:.1f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.1f}s' # noqa: PLR2004
if blitz.record is None:
blitz_value = 'N/A'
else:
if not isinstance(blitz.record, SoloRecord):
raise WhatTheFuckError('Blitz记录不是单人记录')
blitz_value = f'{blitz.record.endcontext.score:,}'
async with HostPage(
await render(
'data.j2.html',
user_avatar=f'https://tetr.io/user-content/avatars/{user_info.data.user.id}.jpg?rv={user_info.data.user.avatar_revision}'
if user_info.data.user.avatar_revision is not None
else f'../../identicon?md5={md5(user_info.data.user.id.encode()).hexdigest()}', # noqa: S324
user_name=user_name,
user_sign=user_info.data.user.bio,
game_type='TETR.IO',
ranking=round(league.glicko, 2),
rd=round(league.rd, 2),
rank=league.rank,
TR=round(league.rating, 2),
global_rank=league.standing,
lpm=round(lpm := (league.pps * 24), 2),
pps=league.pps,
apm=league.apm,
apl=round(league.apm / lpm, 2),
adpm=round(adpm := (league.vs * 0.6), 2),
adpl=round(adpm / lpm, 2),
vs=league.vs,
sprint=sprint_value,
blitz=blitz_value,
data=[[0, 0]],
split_value=0,
value_max=0,
value_min=0,
offset=0,
app=(app := (league.apm / (60 * league.pps))),
dsps=(dsps := ((league.vs / 100) - (league.apm / 60))),
dspp=(dspp := (dsps / league.pps)),
ci=150 * dspp - 125 * app + 50 * (league.vs / league.apm) - 25,
ge=2 * ((app * dsps) / league.pps),
)
) as page_hash:
return UniMessage.image(
raw=await screenshot(
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
)
)
# call back
ret_message = ''
if isinstance(league, NeverPlayedLeague):
ret_message += f'用户 {user_name} 没有排位统计数据'
else:
if isinstance(league, NeverRatedLeague):
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
else:
if league.rank == 'z':
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
else:
ret_message += (
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
)
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
lpm = league.pps * 24
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/lpm,2)} )'
if league.vs is not None:
adpm = league.vs * 0.6
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
user_records = await self.get_user_records()
sprint = user_records.data.records.sprint
if sprint.record is not None:
if not isinstance(sprint.record, SoloRecord):
raise WhatTheFuckError('40L记录不是单人记录')
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
blitz = user_records.data.records.blitz
if blitz.record is not None:
if not isinstance(blitz.record, SoloRecord):
raise WhatTheFuckError('Blitz记录不是单人记录')
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
return UniMessage(ret_message)
async def get_user(self) -> None:
"""
用于获取 UserName 和 UserID 的函数
"""
if self.user.name is None:
self.user.name = (await self.get_user_info()).data.user.username
if self.user.ID is None:
self.user.ID = (await self.get_user_info()).data.user.id
async def get_user_info(self) -> InfoSuccess:
"""获取用户数据"""
if self.processed_data.user_info is None:
self.raw_response.user_info = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
)
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
if isinstance(user_info, FailedModel):
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
self.processed_data.user_info = user_info
return self.processed_data.user_info
async def get_user_records(self) -> RecordsSuccess:
"""获取Solo数据"""
if self.processed_data.user_records is None:
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 = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
if isinstance(user_records, FailedModel):
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
self.processed_data.user_records = user_records
return self.processed_data.user_records
@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 = type_validate_json(
LeagueAll, # type: ignore[arg-type]
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
)
if isinstance(league_all, FailedModel):
raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
def pps(user: LeagueAllUser) -> float:
return user.league.pps
def apm(user: LeagueAllUser) -> float:
return user.league.apm
def vs(user: LeagueAllUser) -> float:
return user.league.vs
def _min(users: list[LeagueAllUser], field: Callable[[LeagueAllUser], float]) -> LeagueAllUser:
return min(users, key=field)
def _max(users: list[LeagueAllUser], field: Callable[[LeagueAllUser], float]) -> LeagueAllUser:
return max(users, key=field)
def build_extremes_data(
users: list[LeagueAllUser],
field: Callable[[LeagueAllUser], float],
sort: Callable[[list[LeagueAllUser], Callable[[LeagueAllUser], float]], LeagueAllUser],
) -> tuple[dict[str, str], float]:
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:
rank_to_users[i.league.rank].append(i)
rank_info: list[IORank] = []
for rank, percentile in RANK_PERCENTILE.items():
offset = floor((percentile / 100) * len(users)) - 1
tr_line = users[offset].league.rating
rank_users = rank_to_users[rank]
rank_info.append(
IORank(
rank=rank,
tr_line=tr_line,
player_count=len(rank_users),
low_pps=(build_extremes_data(rank_users, pps, _min)),
low_apm=(build_extremes_data(rank_users, apm, _min)),
low_vs=(build_extremes_data(rank_users, vs, _min)),
avg_pps=mean({i.league.pps for i in rank_users}),
avg_apm=mean({i.league.apm for i in rank_users}),
avg_vs=mean({i.league.vs for i in rank_users}),
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:
session.add_all(rank_info)
await session.commit()
@driver.on_startup
async def _() -> None:
async with get_session() as session:
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()

View File

@@ -0,0 +1,296 @@
import contextlib
from asyncio import gather
from datetime import datetime, timedelta, timezone
from hashlib import md5
from math import ceil, floor
from urllib.parse import urlunparse
from zoneinfo import ZoneInfo
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from sqlalchemy import select
from ...db import query_bind_info, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.platform import get_platform
from ...utils.render import TETRIOInfo, render
from ...utils.render.schemas.base import Avatar
from ...utils.render.schemas.tetrio_info import Data, Radar, Ranking, TetraLeague, TetraLeagueHistory
from ...utils.render.schemas.tetrio_info import User as TemplateUser
from ...utils.screenshot import screenshot
from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE
from . import alc
from .api import Player, User, UserInfoSuccess
from .api.models import TETRIOHistoricalData
from .api.schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague
from .api.schemas.user_records import SoloModeRecord, SoloRecord
from .constant import GAME_TYPE, TR_MAX, TR_MIN
UTC = timezone.utc
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True)
user, user_info, user_records = await gather(player.user, player.get_info(), player.get_records())
sprint = user_records.data.records.sprint
blitz = user_records.data.records.blitz
with contextlib.suppress(TypeError):
message += UniMessage.image(raw=await make_query_image(user, user_info, sprint.record, blitz.record))
await message.finish()
message += make_query_text(user_info, sprint, blitz)
await message.finish()
@alc.assign('query')
async def _(account: Player, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
user, user_info, user_records = await gather(account.user, account.get_info(), account.get_records())
sprint = user_records.data.records.sprint
blitz = user_records.data.records.blitz
with contextlib.suppress(TypeError):
await UniMessage.image(raw=await make_query_image(user, user_info, sprint.record, blitz.record)).finish()
await make_query_text(user_info, sprint, blitz).finish()
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
value_max = 10 * ceil(max(values) / 10)
value_min = 10 * floor(min(values) / 10)
return value_max, value_min
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
offset = 0
overflow = 0
while True:
if (new_max_value := value_max + offset + overflow) > TR_MAX:
overflow -= 1
continue
if (new_min_value := value_min - offset + overflow) < TR_MIN:
overflow += 1
continue
if ((new_max_value - new_min_value) / 40).is_integer():
split_value = int((value_max + offset - (value_min - offset)) / 4)
break
offset += 1
return split_value, offset + overflow
def get_specified_point(
previous_point: Data,
behind_point: Data,
point_time: datetime,
) -> Data:
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
Args:
previous_point (Data): 前面的数据点
behind_point (Data): 后面的数据点
point_time (datetime): 要推算的点的位置
Returns:
Data: 要推算的点的数据
"""
# 求两个点的斜率
slope = (behind_point.tr - previous_point.tr) / (
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
)
return Data(
record_at=point_time,
tr=previous_point.tr + slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
)
async def query_historical_data(user: User, user_info: UserInfoSuccess) -> list[Data]:
today = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
forward = timedelta(days=9)
start_time = (today - forward).astimezone(UTC)
async with get_session() as session:
historical_data = (
await session.scalars(
select(TETRIOHistoricalData)
.where(TETRIOHistoricalData.update_time >= start_time)
.where(TETRIOHistoricalData.user_unique_identifier == user.unique_identifier)
.where(TETRIOHistoricalData.api_type == 'User Info')
)
).all()
if historical_data:
extra = (
await session.scalars(
select(TETRIOHistoricalData)
.where(TETRIOHistoricalData.user_unique_identifier == user.unique_identifier)
.where(TETRIOHistoricalData.api_type == 'User Info')
.order_by(TETRIOHistoricalData.id.desc())
.where(TETRIOHistoricalData.id < min([i.id for i in historical_data]))
.limit(1)
)
).one_or_none()
if extra is not None:
historical_data = list(historical_data)
historical_data.append(extra)
if not historical_data:
return [
Data(record_at=today - forward, tr=user_info.data.user.league.rating),
Data(record_at=today.replace(microsecond=1000), tr=user_info.data.user.league.rating),
]
histories = [
Data(
record_at=i.update_time.astimezone(ZoneInfo('Asia/Shanghai')),
tr=i.data.data.user.league.rating,
)
for i in historical_data
if isinstance(i.data, UserInfoSuccess) and isinstance(i.data.data.user.league, RatedLeague)
]
# 按照时间排序
histories = sorted(histories, key=lambda x: x.record_at)
for index, value in enumerate(histories):
# 在历史记录里找有没有今天0点后的数据
if value.record_at > today:
histories = histories[:index] + [
get_specified_point(histories[index - 1], histories[index], today.replace(microsecond=1000))
]
break
else:
histories.append(
get_specified_point(
histories[-1],
Data(record_at=user_info.cache.cached_at, tr=user_info.data.user.league.rating),
today.replace(microsecond=1000),
)
)
if histories[0].record_at < (today - forward):
histories[0] = get_specified_point(
histories[0],
histories[1],
today - forward,
)
else:
histories.insert(0, Data(record_at=today - forward, tr=histories[0].tr))
return histories
async def make_query_image(
user: User, user_info: UserInfoSuccess, sprint: SoloRecord | None, blitz: SoloRecord | None
) -> bytes:
league = user_info.data.user.league
if not isinstance(league, RatedLeague) or league.vs is None:
raise TypeError
user_name = user_info.data.user.username.upper()
histories = await query_historical_data(user, user_info)
value_max, value_min = get_value_bounds([i.tr for i in histories])
split_value, offset = get_split(value_max, value_min)
if sprint is not None:
duration = timedelta(milliseconds=sprint.endcontext.final_time).total_seconds()
sprint_value = f'{duration:.1f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.1f}s' # noqa: PLR2004
else:
sprint_value = 'N/A'
blitz_value = f'{blitz.endcontext.score:,}' if blitz is not None else 'N/A'
async with HostPage(
await render(
'tetrio/info',
TETRIOInfo(
user=TemplateUser(
avatar=f'https://tetr.io/user-content/avatars/{user_info.data.user.id}.jpg?rv={user_info.data.user.avatar_revision}'
if user_info.data.user.avatar_revision is not None
else Avatar(
type='identicon',
hash=md5(user_info.data.user.id.encode()).hexdigest(), # noqa: S324
),
name=user_name,
bio=user_info.data.user.bio,
),
ranking=Ranking(
rating=round(league.glicko, 2),
rd=round(league.rd, 2),
),
tetra_league=TetraLeague(
rank=league.rank,
tr=round(league.rating, 2),
global_rank=league.standing,
pps=league.pps,
lpm=round(lpm := (league.pps * 24), 2),
apm=league.apm,
apl=round(league.apm / lpm, 2),
vs=league.vs,
adpm=round(adpm := (league.vs * 0.6), 2),
adpl=round(adpm / lpm, 2),
),
tetra_league_history=TetraLeagueHistory(
data=histories,
split_interval=split_value,
min_tr=value_min,
max_tr=value_max,
offset=offset,
),
radar=Radar(
app=(app := (league.apm / (60 * league.pps))),
dsps=(dsps := ((league.vs / 100) - (league.apm / 60))),
dspp=(dspp := (dsps / league.pps)),
ci=150 * dspp - 125 * app + 50 * (league.vs / league.apm) - 25,
ge=2 * ((app * dsps) / league.pps),
),
sprint=sprint_value,
blitz=blitz_value,
),
)
) as page_hash:
return await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
def make_query_text(user_info: UserInfoSuccess, sprint: SoloModeRecord, blitz: SoloModeRecord) -> UniMessage:
league = user_info.data.user.league
user_name = user_info.data.user.username.upper()
message = ''
if isinstance(league, NeverPlayedLeague):
message += f'用户 {user_name} 没有排位统计数据'
else:
if isinstance(league, NeverRatedLeague):
message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
else:
if league.rank == 'z':
message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
else:
message += f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
lpm = league.pps * 24
message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
message += f'\nAPM: {league.apm} ( x{round(league.apm/lpm,2)} )'
if league.vs is not None:
adpm = league.vs * 0.6
message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
if sprint.record is not None:
message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
if blitz.record is not None:
message += f'\nBlitz: {blitz.record.endcontext.score}'
message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
return UniMessage(message)

View File

@@ -0,0 +1,174 @@
from collections import defaultdict
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from hashlib import sha512
from math import floor
from statistics import mean
from zoneinfo import ZoneInfo
from aiofiles import open
from nonebot import get_driver
from nonebot.compat import model_dump
from nonebot.matcher import Matcher
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 nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from sqlalchemy import func, select
from zstandard import ZstdCompressor
from ...db import trigger
from ...utils.exception import RequestError
from ...utils.metrics import get_metrics
from ...utils.retry import retry
from . import alc
from .api.schemas.base import FailedModel
from .api.schemas.tetra_league import ValidUser
from .api.schemas.user import User
from .api.tetra_league import full_export
from .api.typing import Rank
from .constant import GAME_TYPE, RANK_PERCENTILE
from .model import IORank
UTC = timezone.utc
driver = get_driver()
@alc.assign('rank')
async def _(matcher: Matcher, rank: Rank, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='rank',
command_args=[],
):
if rank == 'z':
await matcher.finish('暂不支持查询未知段位')
async with get_session() as session:
latest_data = (
await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(1))
).one()
compare_data = (
await session.scalars(
select(IORank)
.where(IORank.rank == rank)
.order_by(
func.abs(
func.julianday(IORank.update_time)
- func.julianday(latest_data.update_time - timedelta(hours=24))
)
)
.limit(1)
)
).one()
message = ''
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.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)
low_pps = get_metrics(pps=latest_data.low_pps[1])
low_vs = get_metrics(vs=latest_data.low_vs[1])
max_pps = get_metrics(pps=latest_data.high_pps[1])
max_vs = get_metrics(vs=latest_data.high_vs[1])
message += (
'\n'
'平均数据:\n'
f"L'PM: {avg.lpm} ( {avg.pps} pps )\n"
f'APM: {avg.apm} ( x{avg.apl} )\n'
f'ADPM: {avg.adpm} ( x{avg.adpl} ) ( {avg.vs}vs )\n'
'\n'
'最低数据:\n'
f"L'PM: {low_pps.lpm} ( {low_pps.pps} pps ) By: {latest_data.low_pps[0]['name'].upper()}\n"
f'APM: {latest_data.low_apm[1]} By: {latest_data.low_apm[0]["name"].upper()}\n'
f'ADPM: {low_vs.adpm} ( {low_vs.vs}vs ) By: {latest_data.low_vs[0]["name"].upper()}\n'
'\n'
'最高数据:\n'
f"L'PM: {max_pps.lpm} ( {max_pps.pps} pps ) By: {latest_data.high_pps[0]['name'].upper()}\n"
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.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
)
await matcher.finish(message)
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
async def get_tetra_league_data() -> None:
league, original = await full_export(with_original=True)
if isinstance(league, FailedModel):
msg = f'排行榜数据请求错误:\n{league.error}'
raise RequestError(msg)
def pps(user: ValidUser) -> float:
return user.league.pps
def apm(user: ValidUser) -> float:
return user.league.apm
def vs(user: ValidUser) -> float:
return user.league.vs
def _min(users: list[ValidUser], field: Callable[[ValidUser], float]) -> ValidUser:
return min(users, key=field)
def _max(users: list[ValidUser], field: Callable[[ValidUser], float]) -> ValidUser:
return max(users, key=field)
def build_extremes_data(
users: list[ValidUser],
field: Callable[[ValidUser], float],
sort: Callable[[list[ValidUser], Callable[[ValidUser], float]], ValidUser],
) -> tuple[dict[str, str], float]:
user = sort(users, field)
return model_dump(User(ID=user.id, name=user.username)), field(user)
data_hash: str | None = await run_sync((await run_sync(sha512)(original)).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)(original))
users = [i for i in league.data.users if isinstance(i, ValidUser)]
rank_to_users: defaultdict[Rank, list[ValidUser]] = defaultdict(list)
for i in users:
rank_to_users[i.league.rank].append(i)
rank_info: list[IORank] = []
for rank, percentile in RANK_PERCENTILE.items():
offset = floor((percentile / 100) * len(users)) - 1
tr_line = users[offset].league.rating
rank_users = rank_to_users[rank]
rank_info.append(
IORank(
rank=rank,
tr_line=tr_line,
player_count=len(rank_users),
low_pps=(build_extremes_data(rank_users, pps, _min)),
low_apm=(build_extremes_data(rank_users, apm, _min)),
low_vs=(build_extremes_data(rank_users, vs, _min)),
avg_pps=mean({i.league.pps for i in rank_users}),
avg_apm=mean({i.league.apm for i in rank_users}),
avg_vs=mean({i.league.vs for i in rank_users}),
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.cache.cached_at,
file_hash=data_hash,
)
)
async with get_session() as session:
session.add_all(rank_info)
await session.commit()
@driver.on_startup
async def _() -> None:
async with get_session() as session:
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_tetra_league_data()

View File

@@ -1,63 +0,0 @@
from pydantic import BaseModel, Field
from ..typing import Rank
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class SuccessModel(BaseSuccessModel):
class Data(BaseModel):
class ValidUser(BaseModel):
class League(BaseModel):
gamesplayed: int
gameswon: int
rating: float
glicko: float
rd: float
rank: Rank
bestrank: Rank
apm: float
pps: float
vs: float
decaying: bool
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool
verified: bool
country: str | None = None
class InvalidUser(BaseModel):
class League(BaseModel):
gamesplayed: int
gameswon: int
rating: float
glicko: float | None = None
rd: float | None = None
rank: Rank
bestrank: Rank
apm: float | None = None
pps: float | None = None
vs: float | None = None
decaying: bool
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool
verified: bool
country: str | None
users: list[ValidUser | InvalidUser]
data: Data
LeagueAll = SuccessModel | FailedModel
ValidUser = SuccessModel.Data.ValidUser
InvalidUser = SuccessModel.Data.InvalidUser

View File

@@ -1,21 +0,0 @@
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

View File

@@ -1,17 +0,0 @@
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
@property
def unique_identifier(self) -> str:
if self.ID is None:
raise ValueError('不完整的User!')
return self.ID

View File

@@ -1,125 +0,0 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
from ..typing import Rank
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class SuccessModel(BaseSuccessModel):
class Data(BaseModel):
class User(BaseModel):
class Badge(BaseModel):
id: str
label: str
ts: datetime | None = None
class NeverPlayedLeague(BaseModel):
gamesplayed: Literal[0]
gameswon: Literal[0]
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: None = Field(None)
pps: None = Field(None)
vs: None = Field(None)
decaying: bool
class NeverRatedLeague(BaseModel):
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
gameswon: int
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: float
pps: float
vs: float
decaying: bool
class RatedLeague(BaseModel):
gamesplayed: int
gameswon: int
rating: float
rank: Rank
bestrank: Rank
standing: int
standing_local: int
next_rank: Rank | None = None
prev_rank: Rank | None = None
next_at: int
prev_at: int
percentile: float
percentile_rank: str
glicko: float
rd: float
apm: float
pps: float
vs: float | None = None
decaying: bool
class Connections(BaseModel):
class Discord(BaseModel):
id: str
username: str
discord: Discord | None = None
class Distinguishment(BaseModel):
type: str
id: str = Field(..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
ts: datetime | None = None
botmaster: str | None = None
badges: list[Badge]
xp: float
gamesplayed: int
gameswon: int
gametime: float
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 = 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 = 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 = None
connections: Connections
friend_count: int | None = None
distinguishment: Distinguishment | None = None
user: User
data: Data
NeverPlayedLeague = SuccessModel.Data.User.NeverPlayedLeague
NeverRatedLeague = SuccessModel.Data.User.NeverRatedLeague
RatedLeague = SuccessModel.Data.User.RatedLeague
UserInfo = SuccessModel | FailedModel

View File

@@ -1,124 +0,0 @@
from datetime import datetime
from pydantic import BaseModel, Field
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class EndContext(BaseModel):
class Time(BaseModel):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int | None = None
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
pentas: int | None = None
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int | None = None
cleared: int
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
seed: int
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int | None = None
time: Time
score: int
zenlevel: int | None = None
zenprogress: int | None = None
level: int
combo: int
currentcombopower: int | None = None # WTF
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None # WTF * 2
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
final_time: float = Field(..., alias='finalTime')
gametype: str
class BaseModeRecord(BaseModel):
class SoloRecord(BaseModel):
class User(BaseModel):
id: str = Field(..., alias='_id')
username: str
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: User
ts: datetime
ismulti: bool | None = None
endcontext: EndContext
class MultiRecord(BaseModel):
class User(BaseModel):
id: str = Field(..., alias='_id')
username: str
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: User
ts: datetime
ismulti: bool | None = None
endcontext: list[EndContext]
record: SoloRecord | MultiRecord | None = None
rank: int | None = None
class SuccessModel(BaseSuccessModel):
class Data(BaseModel):
class Records(BaseModel):
class Sprint(BaseModeRecord): ...
class Blitz(BaseModeRecord): ...
sprint: Sprint = Field(..., alias='40l')
blitz: Blitz
class Zen(BaseModel):
level: int
score: int
records: Records
zen: Zen
data: Data
SoloRecord = BaseModeRecord.SoloRecord
MultiRecord = BaseModeRecord.MultiRecord
UserRecords = SuccessModel | FailedModel

View File

@@ -21,11 +21,3 @@ class BaseUser(ABC, Base):
@abstractmethod
def unique_identifier(self) -> str:
raise NotImplementedError
class BaseRawResponse(Base):
"""原始请求数据"""
class BaseProcessedData(Base):
"""处理/验证后的数据"""

View File

@@ -1,19 +1,19 @@
from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
from ...db import query_bind_info
from ...utils.exception import HandleNotFinishedError, NeedCatchError
from ...utils.platform import get_platform
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE
from .processor import Processor, User, identify_user_info
from .api import Player
from .constant import USER_NAME
def get_player(name: str) -> Player | MessageFormatError:
if USER_NAME.match(name):
return Player(user_name=name, trust=True)
return MessageFormatError('用户名/ID不合法')
alc = on_alconna(
Alconna(
@@ -23,7 +23,7 @@ alc = on_alconna(
Args(
Arg(
'account',
identify_user_info,
get_player,
notice='TOP 用户名',
flags=[ArgFlag.HIDDEN],
)
@@ -44,7 +44,7 @@ alc = on_alconna(
),
Arg(
'account',
identify_user_info | Me | At,
get_player,
notice='TOP 用户名',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
@@ -67,68 +67,6 @@ alc = on_alconna(
aliases={'TOP'},
)
@alc.assign('bind')
async def _( # noqa: PLR0913
bot: Bot,
event: Event,
matcher: Matcher,
account: User,
bot_info: UserInfo = BotUserInfo(), # noqa: B008
user_info: UserInfo = EventUserInfo(), # noqa: B008
):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await (
await proc.handle_bind(
platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info, user_info=user_info
)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
proc = Processor(
event_id=id(event),
user=User(name=bind.game_account),
command_args=[],
)
try:
await (UniMessage(message) + await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(event: Event, matcher: Matcher, account: User):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
from . import bind, query # noqa: E402, F401
add_default_handlers(alc)

View File

@@ -0,0 +1,3 @@
from .player import Player
__all__ = ['Player']

View File

@@ -0,0 +1,17 @@
from datetime import datetime
from typing import Literal
from nonebot_plugin_orm import Model
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType
from .schemas.user_profile import UserProfile
class TOPHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True)
api_type: Mapped[Literal['User Profile']] = mapped_column(String(16), index=True)
data: Mapped[UserProfile] = mapped_column(PydanticType(get_model=[], models={UserProfile}))
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)

View File

@@ -0,0 +1,71 @@
from contextlib import suppress
from datetime import datetime, timezone
from io import StringIO
from urllib.parse import urlencode
from lxml import etree
from pandas import read_html
from ....db import anti_duplicate_add
from ....utils.request import Request, splice_url
from ..constant import BASE_URL, USER_NAME
from .models import TOPHistoricalData
from .schemas.user import User
from .schemas.user_profile import Data, UserProfile
UTC = timezone.utc
class Player:
def __init__(self, *, user_name: str, trust: bool = False) -> None:
self.user_name = user_name
if not trust and not USER_NAME.match(self.user_name):
msg = 'Invalid user name'
raise ValueError(msg)
self.__user: User | None = None
self._user_profile: UserProfile | None = None
@property
async def user(self) -> User:
if self.__user is None:
profile = await self.get_profile()
self.__user = User(user_name=profile.user_name)
return self.__user
async def get_profile(self) -> UserProfile:
"""获取用户信息"""
if self._user_profile is None:
url = splice_url([BASE_URL, 'profile.php', f'?{urlencode({"user":self.user_name})}'])
raw_user_profile = await Request.request(url, is_json=False)
self._user_profile = self._parse_profile(raw_user_profile)
await anti_duplicate_add(
TOPHistoricalData,
TOPHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Profile',
data=self._user_profile,
update_time=datetime.now(tz=UTC),
),
)
return self._user_profile
def _parse_profile(self, original_user_profile: bytes) -> UserProfile:
html = etree.HTML(original_user_profile)
user_name = html.xpath('//div[@class="mycontent"]/h1/text()')[0].replace("'s profile", '')
today = None
with suppress(ValueError):
today = Data(
lpm=float(str(html.xpath('//div[@class="mycontent"]/text()[3]')[0]).replace('lpm:', '').strip()),
apm=float(str(html.xpath('//div[@class="mycontent"]/text()[4]')[0]).replace('apm:', '').strip()),
)
table = StringIO(
etree.tostring(
html.xpath('//div[@class="mycontent"]/table[@class="mytable"]')[0],
encoding='utf-8',
).decode()
)
dataframe = read_html(table, encoding='utf-8', header=0)[0]
total: list[Data] = []
for _, value in dataframe.iterrows():
total.append(Data(lpm=value['lpm'], apm=value['apm']))
return UserProfile(user_name=user_name, today=today, total=total)

View File

@@ -0,0 +1,17 @@
from typing import Literal
from typing_extensions import override
from ....schemas import BaseUser
from ...constant import GAME_TYPE
class User(BaseUser):
platform: Literal['TOP'] = GAME_TYPE
user_name: str
@property
@override
def unique_identifier(self) -> str:
return self.user_name

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
class Data(BaseModel):
lpm: float
apm: float
class UserProfile(BaseModel):
user_name: str
today: Data | None
total: list[Data] | None

View File

@@ -0,0 +1,66 @@
from urllib.parse import urlunparse
from nonebot.adapters import Bot
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.avatar import get_avatar
from ...utils.host import HostPage, get_self_netloc
from ...utils.platform import get_platform
from ...utils.render import Bind, render
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
from . import alc
from .api import Player
from .constant import GAME_TYPE
@alc.assign('bind')
async def _(
bot: Bot,
account: Player,
event_session: EventSession,
bot_info: UserInfo = BotUserInfo(), # noqa: B008
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='bind',
command_args=[],
):
user = await account.user
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=get_platform(bot),
chat_account=event_user_info.user_id,
game_platform=GAME_TYPE,
game_account=user.unique_identifier,
)
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform=GAME_TYPE,
status='unknown',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None),
name=user.user_name,
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='top查我',
),
)
) as page_hash:
await UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
).finish()

View File

@@ -1,4 +1,8 @@
from re import compile
from typing import Literal
GAME_TYPE: Literal['TOP'] = 'TOP'
BASE_URL = 'http://tetrisonline.pl/top/'
USER_NAME = compile(r'^[a-zA-Z0-9_]{1,16}$')

View File

@@ -1,162 +0,0 @@
from contextlib import suppress
from dataclasses import dataclass
from io import StringIO
from re import match
from typing import Literal
from urllib.parse import urlencode, urlunparse
from lxml import etree
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from pandas import read_html
from typing_extensions import override
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import render
from ...utils.request import Request, splice_url
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from ..schemas import BaseUser
from .constant import BASE_URL, GAME_TYPE
from .schemas.response import ProcessedData, RawResponse
class User(BaseUser):
platform: Literal['TOP'] = GAME_TYPE
name: str
@property
@override
def unique_identifier(self) -> str:
return self.name
@dataclass
class Data:
lpm: float
apm: float
@dataclass
class GameData:
day: Data | None
total: Data | None
def identify_user_info(info: str) -> User | MessageFormatError:
if match(r'^[a-zA-Z0-9_]{1,16}$', info):
return User(name=info)
return MessageFormatError('用户名不合法')
class Processor(ProcessorMeta):
user: User
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse()
self.processed_data = ProcessedData()
@property
@override
def game_platform(self) -> Literal['TOP']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: UserInfo, user_info: UserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.check_user()
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.name,
)
bot_avatar = await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg')
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'bind.j2.html',
user_avatar='../../static/static/logo/top.ico',
state='unknown',
bot_avatar=bot_avatar,
game_type=self.game_platform,
user_name=(await self.get_user_name()).upper(),
bot_name=bot_info.user_name,
command='top查我',
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
)
)
return message
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
await self.check_user()
game_data = await self.get_game_data()
message = ''
if game_data.day is not None:
message += f'用户 {self.user.name} 24小时内统计数据为: '
message += f"\nL'PM: {round(game_data.day.lpm,2)} ( {round(game_data.day.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.day.apm,2)} ( x{round(game_data.day.apm/game_data.day.lpm,2)} )'
else:
message += f'用户 {self.user.name} 暂无24小时内统计数据'
if game_data.total is not None:
message += '\n历史统计数据为: '
message += f"\nL'PM: {round(game_data.total.lpm,2)} ( {round(game_data.total.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.total.apm,2)} ( x{round(game_data.total.apm/game_data.total.lpm,2)} )'
else:
message += '\n暂无历史统计数据'
return UniMessage(message)
async def get_user_profile(self) -> str:
"""获取用户信息"""
if self.processed_data.user_profile is None:
url = splice_url([BASE_URL, 'profile.php', f'?{urlencode({"user":self.user.name})}'])
self.raw_response.user_profile = await Request.request(url, is_json=False)
self.processed_data.user_profile = self.raw_response.user_profile.decode()
return self.processed_data.user_profile
async def check_user(self) -> None:
if 'user not found!' in await self.get_user_profile():
raise RequestError('用户不存在!')
async def get_user_name(self) -> str:
"""获取用户名"""
data = etree.HTML(await self.get_user_profile()).xpath('//div[@class="mycontent"]/h1/text()')
return data[0].replace("'s profile", '')
async def get_game_data(self) -> GameData:
"""获取游戏统计数据"""
html = etree.HTML(await self.get_user_profile())
day = None
with suppress(ValueError):
day = Data(
lpm=float(str(html.xpath('//div[@class="mycontent"]/text()[3]')[0]).replace('lpm:', '').strip()),
apm=float(str(html.xpath('//div[@class="mycontent"]/text()[4]')[0]).replace('apm:', '').strip()),
)
table = StringIO(
etree.tostring(
html.xpath('//div[@class="mycontent"]/table[@class="mytable"]')[0],
encoding='utf-8',
).decode()
)
dataframe = read_html(table, encoding='utf-8', header=0)[0]
total = Data(lpm=dataframe['lpm'].mean(), apm=dataframe['apm'].mean()) if len(dataframe) != 0 else None
return GameData(day=day, total=total)

View File

@@ -0,0 +1,73 @@
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from ...db import query_bind_info, trigger
from ...utils.metrics import get_metrics
from ...utils.platform import get_platform
from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE
from . import alc
from .api import Player
from .api.schemas.user_profile import UserProfile
from .constant import GAME_TYPE
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE
await (message + make_query_text(await Player(user_name=bind.game_account, trust=True).get_profile())).finish()
@alc.assign('query')
async def _(account: Player, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
await (make_query_text(await account.get_profile())).finish()
def make_query_text(profile: UserProfile) -> UniMessage:
message = ''
if profile.today is not None:
today = get_metrics(lpm=profile.today.lpm, apm=profile.today.apm)
message += f'用户 {profile.user_name} 24小时内统计数据为: '
message += f"\nL'PM: {today.lpm} ( {today.pps} pps )"
message += f'\nAPM: {today.apm} ( x{today.apl} )'
else:
message += f'用户 {profile.user_name} 暂无24小时内统计数据'
if profile.total is not None:
total_lpm = total_apm = 0.0
for value in profile.total:
total_lpm += value.lpm
total_apm += value.apm
num = len(profile.total)
total = get_metrics(lpm=total_lpm / num, apm=total_apm / num)
message += '\n历史统计数据为: '
message += f"\nL'PM: {total.lpm} ( {total.pps} pps )"
message += f'\nAPM: {total.apm} ( x{total.apl} )'
else:
message += '\n暂无历史统计数据'
return UniMessage(message)

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,21 +1,24 @@
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
from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from ...db import query_bind_info
from ...utils.exception import HandleNotFinishedError, NeedCatchError, RequestError
from ...utils.platform import get_platform
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE
from .processor import Processor, User, identify_user_info
from .api import Player
from .constant import USER_NAME
def get_player(teaid_or_name: str) -> Player | MessageFormatError:
if (
teaid_or_name.startswith(('onebot-', 'qqguild-', 'kook-', 'discord-'))
and teaid_or_name.split('-', maxsplit=1)[1].isdigit()
):
return Player(teaid=teaid_or_name, trust=True)
if USER_NAME.match(teaid_or_name) and not teaid_or_name.isdigit() and 2 <= len(teaid_or_name) <= 18: # noqa: PLR2004
return Player(user_name=teaid_or_name, trust=True)
return MessageFormatError('用户名/ID不合法')
alc = on_alconna(
Alconna(
@@ -25,7 +28,7 @@ alc = on_alconna(
Args(
Arg(
'account',
identify_user_info,
get_player,
notice='茶服 用户名 / TeaID',
flags=[ArgFlag.HIDDEN],
)
@@ -46,7 +49,7 @@ alc = on_alconna(
),
Arg(
'account',
identify_user_info,
get_player,
notice='茶服 用户名 / TeaID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
@@ -71,118 +74,6 @@ alc = on_alconna(
)
async def finish_special_query(matcher: Matcher, proc: Processor) -> NoReturn:
try:
await (await proc.handle_query()).finish()
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: 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=f'onebot-{target.target}' if isinstance(target, At) else f'onebot-{event.get_user_id()}'),
command_args=[],
)
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
@alc.assign('bind')
async def _(bot: Bot, event: Event, matcher: Matcher, account: User, bot_info: UserInfo = BotUserInfo()): # noqa: B008
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await (
await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
proc = Processor(
event_id=id(event),
user=User(teaid=bind.game_account),
command_args=[],
)
try:
await (UniMessage(message) + await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query')
async def _(event: Event, matcher: Matcher, account: User):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
from . import bind, query # noqa: E402, F401
add_default_handlers(alc)

View File

@@ -0,0 +1,3 @@
from .player import Player
__all__ = ['Player']

View File

@@ -0,0 +1,20 @@
from datetime import datetime
from typing import Literal
from nonebot_plugin_orm import Model
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType
from .schemas.user_info import UserInfoSuccess
from .schemas.user_profile import UserProfile
class TOSHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True)
api_type: Mapped[Literal['User Info', 'User Profile']] = mapped_column(String(16), index=True)
data: Mapped[UserInfoSuccess | UserProfile] = mapped_column(
PydanticType(get_model=[], models={UserInfoSuccess, UserProfile})
)
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)

View File

@@ -0,0 +1,128 @@
from datetime import datetime, timezone
from typing import overload
from urllib.parse import urlencode
from httpx import TimeoutException
from nonebot.compat import type_validate_json
from ....db import anti_duplicate_add
from ....utils.exception import RequestError
from ....utils.request import Request, splice_url
from ..constant import BASE_URL, USER_NAME
from .models import TOSHistoricalData
from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess
from .schemas.user_profile import UserProfile
UTC = timezone.utc
class Player:
@overload
def __init__(self, *, teaid: str, trust: bool = False): ...
@overload
def __init__(self, *, user_name: str, trust: bool = False): ...
def __init__(self, *, teaid: str | None = None, user_name: str | None = None, trust: bool = False):
self.teaid = teaid
self.user_name = user_name
if not trust:
if self.teaid is not None:
if (
not self.teaid.startswith(('onebot-', 'qqguild-', 'kook-', 'discord-'))
or not self.teaid.split('-', maxsplit=1)[1].isdigit()
):
msg = 'Invalid teaid'
raise ValueError(msg)
elif self.user_name is not None:
if not USER_NAME.match(self.user_name) or self.user_name.isdigit() or 2 > len(self.user_name) > 18: # noqa: PLR2004
msg = 'Invalid user name'
raise ValueError(msg)
else:
msg = 'Invalid user'
raise ValueError(msg)
self.__user: User | None = None
self._user_info: UserInfoSuccess | None = None
self._user_profile: dict[str, UserProfile] = {}
@property
async def user(self) -> User:
if self.__user is None:
user_info = await self.get_info()
self.__user = User(teaid=user_info.data.teaid, name=user_info.data.name)
self.teaid = user_info.data.teaid
self.user_name = user_info.data.name
return self.__user
async def get_info(self) -> UserInfoSuccess:
"""获取用户信息"""
if self._user_info is None:
if self.teaid is not None:
url = [
splice_url(
[
i,
'getTeaIdInfo',
f'?{urlencode({"teaId":self.teaid})}',
]
)
for i in BASE_URL
]
else:
url = [
splice_url(
[
i,
'getUsernameInfo',
f'?{urlencode({"username":self.user_name})}',
]
)
for i in BASE_URL
]
raw_user_info = await Request.failover_request(url, failover_code=[502], failover_exc=(TimeoutException,))
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if not isinstance(user_info, UserInfoSuccess):
msg = f'用户信息请求错误:\n{user_info.error}'
raise RequestError(msg)
self._user_info = user_info
await anti_duplicate_add(
TOSHistoricalData,
TOSHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Info',
data=user_info,
update_time=datetime.now(UTC),
),
)
return self._user_info
async def get_profile(self, other_parameter: dict[str, str | bytes] | None = None) -> UserProfile:
"""获取用户数据"""
if other_parameter is None:
other_parameter = {}
params = urlencode(dict(sorted(other_parameter.items())))
if self._user_profile.get(params) is None:
raw_user_profile = await Request.failover_request(
[
splice_url(
[
i,
'getProfile',
f'?{urlencode({"id":self.teaid or self.user_name,**other_parameter})}',
]
)
for i in BASE_URL
],
failover_code=[502],
failover_exc=(TimeoutException,),
)
self._user_profile[params] = type_validate_json(UserProfile, raw_user_profile)
await anti_duplicate_add(
TOSHistoricalData,
TOSHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Profile',
data=self._user_profile[params],
update_time=datetime.now(UTC),
),
)
return self._user_profile[params]

View File

@@ -0,0 +1,18 @@
from typing import Literal
from typing_extensions import override
from ....schemas import BaseUser
from ...constant import GAME_TYPE
class User(BaseUser):
platform: Literal['TOS'] = GAME_TYPE
teaid: str
name: str
@property
@override
def unique_identifier(self) -> str:
return self.teaid

View File

@@ -0,0 +1,89 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class PeriodMatch(BaseModel):
name: str
teaid: str = Field(..., alias='teaId')
rating: str
rd: str
start_time: datetime = Field(..., alias='startTime')
end_time: datetime = Field(..., alias='endTime')
win: str
lose: str
score: str
class UserDataTotalItem(BaseModel):
time_map: str = Field(..., alias='timeMap')
pieces_map: str = Field(..., alias='piecesMap')
clear_lines_map: str = Field(..., alias='clearLinesMap')
attacks_map: str = Field(..., alias='attacksMap')
dig_map: str = Field(..., alias='digMap')
send_map: str = Field(..., alias='sendMap')
rise_map: str = Field(..., alias='riseMap')
offset_map: str = Field(..., alias='offsetMap')
receive_map: str = Field(..., alias='receiveMap')
games_map: str = Field(..., alias='gamesMap')
tetris_map: str = Field(..., alias='tetrisMap')
combo_map: str = Field(..., alias='comboMap')
tspin_map: str = Field(..., alias='tspinMap')
b2b_map: str = Field(..., alias='b2bMap')
perfect_clear_map: str = Field(..., alias='perfectClearMap')
time_no_map: str = Field(..., alias='timeNoMap')
pieces_no_map: str = Field(..., alias='piecesNoMap')
clear_lines_no_map: str = Field(..., alias='clearLinesNoMap')
attacks_no_map: str = Field(..., alias='attacksNoMap')
dig_no_map: str = Field(..., alias='digNoMap')
send_no_map: str = Field(..., alias='sendNoMap')
rise_no_map: str = Field(..., alias='riseNoMap')
offset_no_map: str = Field(..., alias='offsetNoMap')
receive_no_map: str = Field(..., alias='receiveNoMap')
games_no_map: str = Field(..., alias='gamesNoMap')
tetris_no_map: str = Field(..., alias='tetrisNoMap')
combo_no_map: str = Field(..., alias='comboNoMap')
tspin_no_map: str = Field(..., alias='tspinNoMap')
b2b_no_map: str = Field(..., alias='b2bNoMap')
perfect_clear_no_map: str = Field(..., alias='perfectClearNoMap')
class Data(BaseModel):
teaid: str = Field(..., alias='teaId')
name: str
total_exp: str = Field(..., alias='totalExp')
ranking: str
ranked_games: str = Field(..., alias='rankedGames')
rating_now: str = Field(..., alias='ratingNow')
rd_now: str = Field(..., alias='rdNow')
vol_now: str = Field(..., alias='volNow')
rating_last: str = Field(..., alias='ratingLast')
rd_last: str = Field(..., alias='rdLast')
vol_last: str = Field(..., alias='volLast')
period_matches: list[PeriodMatch] = Field(..., alias='periodMatches')
user_data_total: list[UserDataTotalItem] = Field(..., alias='userDataTotal')
ranking_items: str = Field(..., alias='rankingItems')
ranking_game_items: str = Field(..., alias='rankingGameItems')
training_level: str = Field(..., alias='trainingLevel')
training_wins: str = Field(..., alias='trainingWins')
pb_sprint: str = Field(..., alias='PBSprint')
pb_marathon: str = Field(..., alias='PBMarathon')
pb_challenge: str = Field(..., alias='PBChallenge')
register_date: datetime = Field(..., alias='registerDate')
last_login_date: datetime = Field(..., alias='lastLoginDate')
class UserInfoSuccess(BaseModel):
code: int
success: Literal[True]
data: Data
class FailedModel(BaseModel):
code: int
success: Literal[False]
error: str
UserInfo = UserInfoSuccess | FailedModel

View File

@@ -0,0 +1,34 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
class Data(BaseModel):
idmultiplayergameresult: int
iduser: str
teaid: str
time: int
clear_lines: int
attack: int
send: int
offset: int
receive: int
rise: int
dig: int
pieces: int
max_combo: int
pc_count: int
place: int
num_players: int
fumen_code: Literal['0', '1'] # wtf
rule_set: str
garbage: str
idmultiplayergame: int
datetime: datetime
class UserProfile(BaseModel):
code: int
success: bool
data: list[Data]

View File

@@ -0,0 +1,66 @@
from urllib.parse import urlunparse
from nonebot.adapters import Bot
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.avatar import get_avatar
from ...utils.host import HostPage, get_self_netloc
from ...utils.platform import get_platform
from ...utils.render import Bind, render
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
from . import alc
from .api import Player
from .constant import GAME_TYPE
@alc.assign('bind')
async def _(
bot: Bot,
account: Player,
event_session: EventSession,
bot_info: UserInfo = BotUserInfo(), # noqa: B008
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='bind',
command_args=[],
):
user = await account.user
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=get_platform(bot),
chat_account=event_user_info.user_id,
game_platform=GAME_TYPE,
game_account=user.unique_identifier,
)
user_info = await account.get_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform=GAME_TYPE,
status='unknown',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None), name=user_info.data.name
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_remark or bot_info.user_displayname or bot_info.user_name,
),
command='茶服查我',
),
)
) as page_hash:
await UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
).finish()

View File

@@ -1,6 +1,8 @@
from re import compile
from typing import Literal
GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = {
'https://teatube.cn:8888/',
'http://cafuuchino1.studio26f.org:19970',
@@ -8,3 +10,7 @@ BASE_URL = {
'http://cafuuchino3.studio26f.org:19970',
'http://cafuuchino4.studio26f.org:19970',
}
USER_NAME = compile(
r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$'
)

View File

@@ -1,253 +0,0 @@
from dataclasses import dataclass
from re import match
from typing import Literal
from urllib.parse import urlencode, urlunparse
from httpx import TimeoutException
from nonebot.compat import type_validate_json
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped]
from typing_extensions import override
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import render
from ...utils.request import Request, splice_url
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from ..schemas import BaseUser
from .constant import BASE_URL, GAME_TYPE
from .schemas.response import ProcessedData, RawResponse
from .schemas.user_info import SuccessModel as InfoSuccess
from .schemas.user_info import UserInfo
from .schemas.user_profile import UserProfile
class User(BaseUser):
platform: Literal['TOS'] = GAME_TYPE
teaid: str | None = None
name: str | None = None
@property
@override
def unique_identifier(self) -> str:
if self.teaid is None:
raise ValueError('不完整的User!')
return self.teaid
@dataclass
class GameData:
num: int
pps: float
lpm: float
apm: float
adpm: float
apl: float
adpl: float
vs: float
def identify_user_info(info: str) -> User | MessageFormatError:
if (
match(
r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$',
info,
)
and info.isdigit() is False
and 2 <= len(info) <= 18 # noqa: PLR2004
):
return User(name=info)
if info.startswith(('onebot-', 'qqguild-', 'kook-', 'discord-')) and info.split('-', maxsplit=1)[1].isdigit():
return User(teaid=info)
return MessageFormatError('用户名/QQ号不合法')
class Processor(ProcessorMeta):
user: User
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse(user_profile={})
self.processed_data = ProcessedData(user_profile={})
@property
@override
def game_platform(self) -> Literal['TOS']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.get_user()
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.unique_identifier,
)
bot_avatar = await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg')
user_info = await self.get_user_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'bind.j2.html',
user_avatar='../../static/static/logo/tos.ico',
state='unknown',
bot_avatar=bot_avatar,
game_type=self.game_platform,
user_name=user_info.data.name.upper(),
bot_name=bot_info.user_name,
command='茶服查我',
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
)
)
return message
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
await self.get_user()
user_info = (await self.get_user_info()).data
message = f'用户 {user_info.name} ({user_info.teaid}) '
if user_info.ranked_games == '0':
message += '暂无段位统计数据'
else:
message += f', 段位分 {round(float(user_info.rating_now),2)}±{round(float(user_info.rd_now),2)} ({round(float(user_info.vol_now),2)}) '
game_data = await self.get_game_data()
if game_data is None:
message += ', 暂无游戏数据'
else:
message += f', 最近 {game_data.num} 局数据'
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' 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 UniMessage(message)
async def get_user(self) -> None:
"""
用于获取 UserName 和 UserID 的函数
"""
if self.user.name is None:
self.user.name = (await self.get_user_info()).data.name
if self.user.teaid is None:
self.user.teaid = (await self.get_user_info()).data.teaid
async def get_user_info(self) -> InfoSuccess:
"""获取用户信息"""
if self.processed_data.user_info is None:
if self.user.teaid is not None:
url = [
splice_url(
[
i,
'getTeaIdInfo',
f'?{urlencode({"teaId":self.user.teaid})}',
]
)
for i in BASE_URL
]
else:
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
return self.processed_data.user_info
async def get_user_profile(self, other_parameter: dict[str, str | bytes] | None = None) -> UserProfile:
"""获取用户数据"""
if other_parameter is None:
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.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] = type_validate_json(
UserProfile, self.raw_response.user_profile[params]
)
return self.processed_data.user_profile[params]
async def get_game_data(self) -> GameData | None:
"""获取游戏数据"""
user_profile = await self.get_user_profile()
if user_profile.data == []:
return None
weighted_total_lpm = weighted_total_apm = weighted_total_adpm = 0.0
total_time = 0.0
num = 0
for i in user_profile.data:
# 排除单人局和时间为0的游戏
# 茶: 不计算没挖掘的局, 即使apm和lpm也如此
if i.num_players == 1 or i.time == 0 or i.dig is None:
continue
# 加权计算
time = i.time / 1000
lpm = 24 * (i.pieces / time)
apm = (i.attack / time) * 60
adpm = ((i.attack + i.dig) / time) * 60
weighted_total_lpm += lpm * time
weighted_total_apm += apm * time
weighted_total_adpm += adpm * time
total_time += time
num += 1
if num == 50: # noqa: PLR2004 # TODO: 将查询局数作为可选命令参数
break
if num == 0:
return None
# TODO: 如果有效局数不满50, 没有无dig信息的局, 且userData['data']内有50个局, 则继续往前获取信息
lpm = weighted_total_lpm / total_time
apm = weighted_total_apm / total_time
adpm = weighted_total_adpm / total_time
return GameData(
num=num,
pps=round(lpm / 24, 2),
lpm=round(lpm, 2),
apm=round(apm, 2),
adpm=round(adpm, 2),
apl=round((apm / lpm), 2),
adpl=round((adpm / lpm), 2),
vs=round((adpm / 60 * 100), 2),
)

View File

@@ -0,0 +1,170 @@
from asyncio import gather
from dataclasses import dataclass
from typing import Literal
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from ...db import query_bind_info, trigger
from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics
from ...utils.platform import get_platform
from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE
from . import alc
from .api import Player
from .constant import GAME_TYPE
def add_special_handlers(
teaid_prefix: Literal['onebot-', 'kook-', 'discord-', 'qqguild-'], match_event: type[Event]
) -> None:
@alc.assign('query')
async def _(event: Event, target: At | Me, event_session: EventSession):
if isinstance(event, match_event):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
await (
await make_query_text(
Player(
teaid=f'{teaid_prefix}{target.target}'
if isinstance(target, At)
else f'{teaid_prefix}{event.get_user_id()}',
trust=True,
)
)
).finish()
try:
from nonebot.adapters.onebot.v11 import MessageEvent as OB11MessageEvent
add_special_handlers('onebot-', OB11MessageEvent)
except ImportError:
pass
try:
from nonebot.adapters.qq.event import GuildMessageEvent as QQGuildMessageEvent
from nonebot.adapters.qq.event import QQMessageEvent
add_special_handlers('qqguild-', QQGuildMessageEvent)
add_special_handlers('onebot-', QQMessageEvent)
except ImportError:
pass
try:
from nonebot.adapters.kaiheila.event import MessageEvent as KookMessageEvent
add_special_handlers('kook-', KookMessageEvent)
except ImportError:
pass
try:
from nonebot.adapters.discord import MessageEvent as DiscordMessageEvent
add_special_handlers('discord-', DiscordMessageEvent)
except ImportError:
pass
@alc.assign('query')
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
chat_platform=get_platform(bot),
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
game_platform=GAME_TYPE,
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE
await (message + await make_query_text(Player(teaid=bind.game_account, trust=True))).finish()
@alc.assign('query')
async def _(account: Player, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[],
):
await (await make_query_text(account)).finish()
@dataclass
class GameData:
game_num: int
metrics: TetrisMetricsProWithLPMADPM
async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
"""获取游戏数据"""
user_profile = await player.get_profile()
if user_profile.data == []:
return None
weighted_total_lpm = weighted_total_apm = weighted_total_adpm = total_time = 0.0
num = 0
for i in user_profile.data:
# 排除单人局和时间为0的游戏
# 茶: 不计算没挖掘的局, 即使apm和lpm也如此
if i.num_players == 1 or i.time == 0 or i.dig is None:
continue
# 加权计算
time = i.time / 1000
lpm = 24 * (i.pieces / time)
apm = (i.attack / time) * 60
adpm = ((i.attack + i.dig) / time) * 60
weighted_total_lpm += lpm * time
weighted_total_apm += apm * time
weighted_total_adpm += adpm * time
total_time += time
num += 1
if num >= query_num:
break
if num == 0:
return None
# TODO: 如果有效局数小于 {查询数} , 并且没有无dig信息的局, 且 user_profile.data 内有{请求数}个局, 则继续往前获取信息
metrics = get_metrics(
lpm=weighted_total_lpm / total_time, apm=weighted_total_apm / total_time, adpm=weighted_total_adpm / total_time
)
lpm = weighted_total_lpm / total_time
apm = weighted_total_apm / total_time
adpm = weighted_total_adpm / total_time
return GameData(game_num=num, metrics=metrics)
async def make_query_text(player: Player) -> UniMessage:
user_info, game_data = await gather(player.get_info(), get_game_data(player))
user_data = user_info.data
message = f'用户 {user_data.name} ({user_data.teaid}) '
if user_data.ranked_games == '0':
message += '暂无段位统计数据'
else:
message += f', 段位分 {round(float(user_data.rating_now),2)}±{round(float(user_data.rd_now),2)} ({round(float(user_data.vol_now),2)}) '
if game_data is None:
message += ', 暂无游戏数据'
else:
message += f', 最近 {game_data.game_num} 局数据'
message += f"\nL'PM: {game_data.metrics.lpm} ( {game_data.metrics.pps} pps )"
message += f'\nAPM: {game_data.metrics.apm} ( x{game_data.metrics.apl} )'
message += f'\nADPM: {game_data.metrics.adpm} ( x{game_data.metrics.adpl} ) ( {game_data.metrics.vs}vs )'
message += f'\n40L: {float(user_data.pb_sprint)/1000:.2f}s' if user_data.pb_sprint != '2147483647' else ''
message += f'\nMarathon: {user_data.pb_marathon}' if user_data.pb_marathon != '0' else ''
message += f'\nChallenge: {user_data.pb_challenge}' if user_data.pb_challenge != '0' else ''
return UniMessage(message)

View File

@@ -1,20 +0,0 @@
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

View File

@@ -1,86 +0,0 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class SuccessModel(BaseModel):
class Data(BaseModel):
class PeriodMatch(BaseModel):
name: str
teaid: str = Field(..., alias='teaId')
rating: str
rd: str
start_time: datetime = Field(..., alias='startTime')
end_time: datetime = Field(..., alias='endTime')
win: str
lose: str
score: str
class UserDataTotalItem(BaseModel):
time_map: str = Field(..., alias='timeMap')
pieces_map: str = Field(..., alias='piecesMap')
clear_lines_map: str = Field(..., alias='clearLinesMap')
attacks_map: str = Field(..., alias='attacksMap')
dig_map: str = Field(..., alias='digMap')
send_map: str = Field(..., alias='sendMap')
rise_map: str = Field(..., alias='riseMap')
offset_map: str = Field(..., alias='offsetMap')
receive_map: str = Field(..., alias='receiveMap')
games_map: str = Field(..., alias='gamesMap')
tetris_map: str = Field(..., alias='tetrisMap')
combo_map: str = Field(..., alias='comboMap')
tspin_map: str = Field(..., alias='tspinMap')
b2b_map: str = Field(..., alias='b2bMap')
perfect_clear_map: str = Field(..., alias='perfectClearMap')
time_no_map: str = Field(..., alias='timeNoMap')
pieces_no_map: str = Field(..., alias='piecesNoMap')
clear_lines_no_map: str = Field(..., alias='clearLinesNoMap')
attacks_no_map: str = Field(..., alias='attacksNoMap')
dig_no_map: str = Field(..., alias='digNoMap')
send_no_map: str = Field(..., alias='sendNoMap')
rise_no_map: str = Field(..., alias='riseNoMap')
offset_no_map: str = Field(..., alias='offsetNoMap')
receive_no_map: str = Field(..., alias='receiveNoMap')
games_no_map: str = Field(..., alias='gamesNoMap')
tetris_no_map: str = Field(..., alias='tetrisNoMap')
combo_no_map: str = Field(..., alias='comboNoMap')
tspin_no_map: str = Field(..., alias='tspinNoMap')
b2b_no_map: str = Field(..., alias='b2bNoMap')
perfect_clear_no_map: str = Field(..., alias='perfectClearNoMap')
teaid: str = Field(..., alias='teaId')
name: str
total_exp: str = Field(..., alias='totalExp')
ranking: str
ranked_games: str = Field(..., alias='rankedGames')
rating_now: str = Field(..., alias='ratingNow')
rd_now: str = Field(..., alias='rdNow')
vol_now: str = Field(..., alias='volNow')
rating_last: str = Field(..., alias='ratingLast')
rd_last: str = Field(..., alias='rdLast')
vol_last: str = Field(..., alias='volLast')
period_matches: list[PeriodMatch] = Field(..., alias='periodMatches')
user_data_total: list[UserDataTotalItem] = Field(..., alias='userDataTotal')
ranking_items: str = Field(..., alias='rankingItems')
ranking_game_items: str = Field(..., alias='rankingGameItems')
training_level: str = Field(..., alias='trainingLevel')
training_wins: str = Field(..., alias='trainingWins')
pb_sprint: str = Field(..., alias='PBSprint')
pb_marathon: str = Field(..., alias='PBMarathon')
pb_challenge: str = Field(..., alias='PBChallenge')
register_date: datetime = Field(..., alias='registerDate')
last_login_date: datetime = Field(..., alias='lastLoginDate')
code: int
success: Literal[True]
data: Data
class FailedModel(BaseModel):
code: int
success: Literal[False]
error: str
UserInfo = SuccessModel | FailedModel

View File

@@ -1,33 +0,0 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
class UserProfile(BaseModel):
class Data(BaseModel):
idmultiplayergameresult: int
iduser: str
teaid: str
time: int
clear_lines: int
attack: int
send: int
offset: int
receive: int
rise: int
dig: int
pieces: int
max_combo: int
pc_count: int
place: int
num_players: int
fumen_code: Literal['0', '1'] # wtf
rule_set: str
garbage: str
idmultiplayergame: int
datetime: datetime
code: int
success: bool
data: list[Data]

View File

@@ -1,3 +0,0 @@
from pathlib import Path
path = Path(__file__).absolute().parent

View File

@@ -1,43 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="../../static/css/bind.css" rel="stylesheet" />
</head>
<body>
<div id="background">
<div id="main-content">
<div id="bind-subject">
<div id="bind-icons">
<img id="user-avatar" src="{{ user_avatar }}" />
<img id="state" src="../../static/static/bind/{{ state }}.svg" />
<img id="bot-avatar" src="{{ bot_avatar }}" />
</div>
<div id="command-result">
已将您在
<p id="game-type">{{ game_type }}</p>
上的账号
<br />
<p id="user-name">{{ user_name }}</p>
<br />
{% if state == 'success' %} 成功验证并绑定至
<p id="bot-name">{{ bot_name }}.</p>
{% elif state == 'unverified'%} 绑定至
<p id="bot-name">{{ bot_name }}</p>
, 但尚未通过验证. {% elif state == 'unknown' %} 绑定至
<p id="bot-name">{{ bot_name }}</p>
,<br />但是
<p id="bot-name">{{ bot_name }}</p>
暂时无法验证您的身份. {% elif state == 'unlink' %} 成功从
<p id="bot-name">{{ bot_name }}</p>
解绑. {% endif %}
</div>
</div>
<div id="extra-info">您可以输入 “{{ command }}” 命令来查找您在该平台上的统计数据.</div>
</div>
</div>
</body>
</html>

View File

@@ -1,118 +0,0 @@
@font-face {
font-family: 'SourceHanSansSC-VF';
src: url('../static/fonts/SourceHanSans/SourceHanSansSC-VF.otf.woff2') format('woff2');
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'CabinetGrotesk-Variable';
src: url('../static/fonts/CabinetGrotesk/CabinetGrotesk-Variable.woff2') format('woff2');
font-display: swap;
font-style: normal;
}
* {
margin: 0;
padding: 0;
}
#background {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 30px;
gap: 10px;
width: 444px;
background: #f1f1f1;
font-family: 'CabinetGrotesk-Variable', 'SourceHanSansSC-VF';
}
#main-content {
display: flex;
flex-direction: column;
margin: 0 auto;
padding: 0px;
gap: 15px;
}
#bind-subject {
display: flex;
flex-direction: column;
align-items: center;
padding: 0px;
gap: 30px;
}
#bind-icons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0px;
gap: 32px;
}
#user-avatar {
width: 96px;
height: 96px;
filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22));
border-radius: 20px;
}
#state {
width: 128px;
height: 56px;
}
#bot-avatar {
width: 96px;
height: 96px;
filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22));
border-radius: 20px;
}
#command-result {
font-weight: 350;
font-size: 25px;
line-height: 36.2px;
text-align: center;
color: #000000;
}
#game-type {
display: inline;
font-weight: 800;
line-height: 31px;
}
#user-name {
display: inline;
font-weight: 800;
line-height: 31px;
white-space: nowrap;
text-overflow: ellipsis;
}
#bot-name {
display: inline;
font-weight: 400;
line-height: 31px;
}
#extra-info {
width: 324px;
margin: 0 auto;
font-weight: 400;
font-size: 16px;
line-height: 23px;
text-align: center;
color: #52525c;
}

View File

@@ -1,368 +0,0 @@
@font-face {
font-family: 'CabinetGrotesk-Variable';
src: url('../static/fonts/CabinetGrotesk/CabinetGrotesk-Variable.woff2') format('woff2');
}
@font-face {
font-family: 'SourceHanSansSC-VF';
src: url('../static/fonts/SourceHanSans/SourceHanSansSC-VF.otf.woff2') format('woff2');
}
* {
margin: 0;
padding: 0;
}
.flex-gap {
flex: 1;
}
.big-title {
margin-left: 25px;
margin-top: 22px;
font-weight: 900;
font-size: 35px;
line-height: 43px;
color: #000000;
}
.box-shadow {
box-shadow: 0px 9px 25px rgba(0, 0, 0, 0.15);
}
.chart-shadow {
box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.3);
}
.box-rounded-corners {
border-radius: 30px;
}
.small-data-box {
position: relative;
width: 275px;
height: 125px;
}
.big-data-value {
position: absolute;
left: 24px;
top: 52px;
font-weight: 500;
font-size: 45px;
line-height: 56px;
}
.small-data-value {
position: absolute;
top: 79px;
right: 25px;
font-weight: 500;
font-size: 15px;
line-height: 19px;
text-align: right;
}
.main-content {
display: flex;
flex-direction: column;
width: 625px;
background: #f1f1f1;
font-family: 'CabinetGrotesk-Variable', 'SourceHanSansSC-VF';
}
.account-box {
display: flex;
flex-direction: column;
}
.info-box {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.user-info-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 25px;
gap: 10px;
width: 275px;
height: 275px;
background: #fafafa;
box-sizing: border-box;
}
.user-avatar {
width: 125px;
height: 125px;
filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22));
border-radius: 65px;
}
.user-name {
font-weight: 800;
font-size: 25px;
line-height: 31px;
color: #000000;
}
.user-sign {
width: 225px;
height: 66px;
font-weight: 400;
font-size: 18px;
line-height: 22px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
color: #000000;
}
.game-info-box {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 25px;
gap: 10px;
width: 275px;
height: 275px;
background: #fafafa;
box-sizing: border-box;
}
.game-type-box {
display: flex;
flex-direction: column;
}
.game-logo {
width: 60px;
height: 60px;
border-radius: 10px;
}
.game-name {
font-weight: 800;
font-size: 30px;
line-height: 37px;
color: #000000;
}
.game-info-dividing-line {
width: 225px;
border: 1px solid #bababa;
transform: rotate(0.25deg);
}
.ranking-info-box {
display: flex;
flex-direction: column;
}
.ranking-title {
font-weight: 800;
font-size: 25px;
line-height: 31px;
color: #000000;
}
.ranking {
font-weight: 400;
font-size: 50px;
line-height: 120%;
color: #000000;
}
.rd {
margin-top: -16px;
font-weight: 300;
font-size: 30px;
line-height: 120%;
color: #000000;
}
#TR-curve-chart {
align-self: center;
margin-top: 25px;
width: 575px;
height: 275px;
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%);
}
.TR-title {
position: absolute;
margin-left: 24px;
margin-top: 19px;
font-weight: 800;
font-size: 25px;
line-height: 31px;
white-space: nowrap;
color: #fafafa;
}
.rank-icon {
position: absolute;
margin-left: 27px;
margin-top: 90px;
width: 50px;
height: 50px;
}
.TR {
position: absolute;
margin-left: 24px;
margin-top: 143px;
font-weight: 800;
font-size: 45px;
line-height: 120%;
color: #fafafa;
}
.multiplayer-box {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 14px;
}
.multiplayer-data-box {
display: flex;
flex-direction: column;
}
.multiplayer-data {
margin-top: 25px;
}
.multiplayer-data:first-child {
margin-top: 0px;
}
.lpm-box {
background-image: url('../static/data/LPM.svg');
}
.lpm-value {
color: #4d7d0f;
}
.pps-value {
color: #4d7d0f;
}
.apm-box {
background-image: url('../static/data/APM.svg');
}
.apm-value {
color: #b5530a;
}
.apl-value {
color: #b5530a;
}
.adpm-box {
background-image: url('../static/data/ADPM.svg');
}
.adpm-value {
color: #235db4;
}
.vs-value {
top: 62px;
color: #4779c6;
}
.adpl-value {
color: #4779c6;
}
.radar-chart-box {
display: flex;
flex-direction: column;
}
.radar-background {
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%),
linear-gradient(222.34deg, #4f9dff 11.97%, #2563ea 89.73%);
}
#radar-chart {
width: 275px;
height: 275px;
}
.radar-description {
text-align: left;
padding-left: 20px;
padding-top: 15px;
font-size: 12px;
color: #fafafa;
box-sizing: border-box;
}
.singleplayer-box {
display: flex;
flex-direction: row;
align-content: space-between;
margin-top: 14px;
}
.sprint-box {
background-image: url('../static/data/40L.svg');
}
.blitz-box {
background-image: url('../static/data/Blitz.svg');
}
.sprint-value {
color: #b42323;
}
.blitz-value {
color: #8e23b4;
}
.footer {
margin-top: 20px;
margin-bottom: 20px;
font-size: 30px;
font-weight: 750;
line-height: 120%;
text-align: center;
}

View File

@@ -1,361 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="../../static/css/data.css" rel="stylesheet" />
</head>
<body>
<div class="main-content">
<span class="big-title">Account&Rankings</span>
<div class="account-box">
<div class="info-box">
<div class="flex-gap"></div>
<div class="box-shadow box-rounded-corners user-info-box">
<div class="flex-gap"></div>
<img class="user-avatar" src="{{user_avatar}}" />
<div class="flex-gap"></div>
<div class="user-name">{{user_name}}</div>
<div class="flex-gap"></div>
{% if user_sign is not none %}
<div class="user-sign">“{{user_sign}}”</div>
{% endif %}
</div>
<div class="flex-gap"></div>
<div class="box-shadow box-rounded-corners game-info-box">
<div class="game-type-box">
<img class="game-logo" src="../../static/static/logo/{{game_type}}.svg" />
<span class="game-name">{{game_type}}</span>
</div>
<div class="game-info-dividing-line"></div>
<div class="ranking-info-box">
<span class="ranking-title">Ranking</span>
<span class="ranking">{{ranking}}</span>
<span class="rd">±{{rd}}</span>
</div>
</div>
<div class="flex-gap"></div>
</div>
{# <div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
<span class="TR-title">Tetra Rating (TR)</span>
<img class="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
<span class="TR" style="display: flex; align-items: flex-end"
>{{TR}}&nbsp;
<p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p>
</span>
</div> #}
</div>
<span class="big-title">Multiplayer Stats</span>
<div class="multiplayer-box">
<div class="flex-gap"></div>
<div class="multiplayer-data-box">
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners lpm-box">
<span class="big-data-value lpm-value">{{lpm}}</span>
<span class="small-data-value pps-value">{{pps}} pps</span>
</div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners apm-box">
<span class="big-data-value apm-value">{{apm}}</span>
<span class="small-data-value apl-value">x{{apl}}</span>
</div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners adpm-box">
<span class="big-data-value adpm-value">{{adpm}}</span>
<span class="small-data-value adpl-value">x{{adpl}}</span>
<span class="small-data-value vs-value">{{vs}} vs</span>
</div>
</div>
<div class="flex-gap"></div>
<div class="radar-chart-box">
<div class="chart-shadow box-rounded-corners radar-background" id="radar-chart"></div>
<div class="flex-gap"></div>
<div class="chart-shadow box-rounded-corners small-data-box radar-background radar-description"><p style="font-size: 18px;display: inline;">tips: </p><br />DSPS 每秒挖掘<br />DSPP 每块挖掘<br />CI 奶酪指数<br />GE 垃圾利用率</div>
</div>
<div class="flex-gap"></div>
</div>
<span class="big-title">Singleplayer Stats</span>
<div class="singleplayer-box">
<div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners sprint-box">
<span class="big-data-value sprint-value">{{sprint}}</span>
</div>
<div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners blitz-box">
<span class="big-data-value blitz-value">{{blitz}}</span>
</div>
<div class="flex-gap"></div>
</div>
<div class="footer">Powered by<br />Nonebot2 x nonebot-plugin-tetris-stats</div>
</div>
</body>
<script src="../../static/js/echarts.js"></script>
<script>
var data = {{data}}
// 曲线图
var lineChartDom = document.getElementById('TR-curve-chart');
var lineChart = echarts.init(lineChartDom, null, { renderer: 'svg' });
var option;
/** @type EChartsOption */
option = {
animation: false,
grid: {
left: '-5%',
bottom: '17%',
width: '90%',
height: '70%',
},
xAxis: {
type: 'time',
minInterval: 3600 * 48 * 1000,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
axisLabel: {
formatter: function (value, index) {
var date = new Date(value);
var lst;
var ret;
function format_date() {
return new Intl.DateTimeFormat('en-US', {
month: '2-digit',
day: '2-digit',
})
.format(date)
.split('/');
}
switch (index) {
case 0:
case 6:
ret = '';
break;
default:
lst = format_date();
if (index === 5) {
ret = '{last_month|' + lst[0] + '}\n{last_day|' + lst[1] + '}';
break;
}
ret = '{month|' + lst[0] + '}\n{day|' + lst[1] + '}';
}
return ret;
},
rich: {
month: {
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 13,
fontWeight: '400',
color: 'rgba(255, 255, 255, 0.6)',
},
day: {
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 20,
fontWeight: '800',
color: 'rgba(255, 255, 255, 0.6)',
},
last_month: {
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 13,
fontWeight: '400',
color: '#373533',
backgroundColor: '#FAFAFA',
borderRadius: 6,
padding: [-10, 0, 10, 0],
width: 36,
height: 37,
lineHeight: 32,
},
last_day: {
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 20,
fontWeight: '800',
color: '#373533',
padding: [-18, 0, 0, 0],
lineHeight: 0,
},
},
},
zlevel: 1,
},
yAxis: {
type: 'value',
interval: {{split_value}},
position: 'right',
splitLine: {
show: false,
},
axisLine: {
show: false,
},
axisLabel: {
align: 'right',
formatter: function (value, index) {
return '{value|' + value.toLocaleString() + '}';
},
rich: {
value: {
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 15,
fontWeight: '500',
color: 'rgba(255, 255, 255, 0.6)',
},
},
},
offset: 70,
max: {{value_max+offset}},
min: {{value_min-offset}},
},
series: [
{
// 10天的数据最后一天只要第一条 (时间戳最少要多1ms)
data: data,
type: 'line',
smooth: true,
symbol: function (value, params) {
if (params.dataIndex === data.length - 1) {
return 'image://../../static/static/data/point.svg';
}
return 'none';
},
symbolSize: 75,
symbolOffset: [0.79, 0],
lineStyle: {
color: '#FAFAFA99',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(250, 250, 250, 0.3)',
},
{
offset: 1,
color: 'rgba(250, 250, 250, 0)',
},
],
global: false,
},
},
markLine: {
data: [
{
xAxis: 'max',
y: 300,
},
],
label: {
show: false,
},
lineStyle: {
color: '#FAFAFA',
width: 3,
type: 'dashed',
cap: 'round',
},
symbol: 'none',
animation: false,
},
z: 5,
},
],
};
option && lineChart.setOption(option);
</script>
<script>
// 雷达图
var radarChartDom = document.getElementById('radar-chart');
var radarChart = echarts.init(radarChartDom, null, { renderer: 'svg' });
var option;
option = {
animation: false,
radar: [
{
indicator: [
{ name: 'PPS' },
{ name: 'APP', nameRotate: 60 },
{ name: 'DSPS', nameRotate: -60 },
{ name: 'DSPP' },
{ name: 'CI', nameRotate: 60 },
{ name: 'GE', nameRotate: -60 },
],
center: ['50%', '50%'],
radius: '65%',
startAngle: 90,
splitNumber: 4,
shape: 'circle',
silent: true,
axisName: {
color: '#FAFAFA',
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 15,
fontWeight: '800',
},
splitArea: {
show: false,
},
axisLine: {
lineStyle: {
color: 'rgba(250, 250, 250, 0.3)',
},
},
axisLabel: {
show: true,
rotate: 0,
margin: -1,
fontFamily: 'CabinetGrotesk-Variable',
fontSize: 7,
fontWeight: '800',
color: '#FFFFFF',
},
splitLine: {
lineStyle: {
color: 'rgba(250, 250, 250, 0.3)',
},
},
},
],
series: [
{
type: 'radar',
symbol: 'none',
label: {
show: true,
},
emphasis: {
disabled: true,
},
lineStyle: {
color: '#FAFAFA',
width: 2.5,
shadowBlur: 20,
shadowColor: 'rgba(250, 250, 250, 1)',
},
areaStyle: {
color: 'rgba(250, 250, 250, 0.45)',
},
data: [
{
value: [{{pps}}, {{app}}, {{dsps}}, {{dspp}}, {{ci}}, {{ge}}],
},
],
},
],
};
option && radarChart.setOption(option);
</script>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,205 +0,0 @@
/**
* Identicon.js 2.3.3
* http://github.com/stewartlord/identicon.js
*
* PNGLib required for PNG output
* http://www.xarg.org/download/pnglib.js
*
* Copyright 2018, Stewart Lord
* Released under the BSD license
* http://www.opensource.org/licenses/bsd-license.php
*/
(function() {
var PNGlib;
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
PNGlib = require('./pnglib');
} else {
PNGlib = window.PNGlib;
}
var Identicon = function(hash, options){
if (typeof(hash) !== 'string' || hash.length < 15) {
throw 'A hash of at least 15 characters is required.';
}
this.defaults = {
background: [240, 240, 240, 255],
margin: 0.08,
size: 64,
saturation: 0.7,
brightness: 0.5,
format: 'png'
};
this.options = typeof(options) === 'object' ? options : this.defaults;
// backward compatibility with old constructor (hash, size, margin)
if (typeof(arguments[1]) === 'number') { this.options.size = arguments[1]; }
if (arguments[2]) { this.options.margin = arguments[2]; }
this.hash = hash
this.background = this.options.background || this.defaults.background;
this.size = this.options.size || this.defaults.size;
this.format = this.options.format || this.defaults.format;
this.margin = this.options.margin !== undefined ? this.options.margin : this.defaults.margin;
// foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness
var hue = parseInt(this.hash.substr(-7), 16) / 0xfffffff;
var saturation = this.options.saturation || this.defaults.saturation;
var brightness = this.options.brightness || this.defaults.brightness;
this.foreground = this.options.foreground || this.hsl2rgb(hue, saturation, brightness);
};
Identicon.prototype = {
background: null,
foreground: null,
hash: null,
margin: null,
size: null,
format: null,
image: function(){
return this.isSvg()
? new Svg(this.size, this.foreground, this.background)
: new PNGlib(this.size, this.size, 256);
},
render: function(){
var image = this.image(),
size = this.size,
baseMargin = Math.floor(size * this.margin),
cell = Math.floor((size - (baseMargin * 2)) / 5),
margin = Math.floor((size - cell * 5) / 2),
bg = image.color.apply(image, this.background),
fg = image.color.apply(image, this.foreground);
// the first 15 characters of the hash control the pixels (even/odd)
// they are drawn down the middle first, then mirrored outwards
var i, color;
for (i = 0; i < 15; i++) {
color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg;
if (i < 5) {
this.rectangle(2 * cell + margin, i * cell + margin, cell, cell, color, image);
} else if (i < 10) {
this.rectangle(1 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
this.rectangle(3 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
} else if (i < 15) {
this.rectangle(0 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
this.rectangle(4 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
}
}
return image;
},
rectangle: function(x, y, w, h, color, image){
if (this.isSvg()) {
image.rectangles.push({x: x, y: y, w: w, h: h, color: color});
} else {
var i, j;
for (i = x; i < x + w; i++) {
for (j = y; j < y + h; j++) {
image.buffer[image.index(i, j)] = color;
}
}
}
},
// adapted from: https://gist.github.com/aemkei/1325937
hsl2rgb: function(h, s, b){
h *= 6;
s = [
b += s *= b < .5 ? b : 1 - b,
b - h % 1 * s * 2,
b -= s *= 2,
b,
b + h % 1 * s,
b + s
];
return[
s[ ~~h % 6 ] * 255, // red
s[ (h|16) % 6 ] * 255, // green
s[ (h|8) % 6 ] * 255 // blue
];
},
toString: function(raw){
// backward compatibility with old toString, default to base64
if (raw) {
return this.render().getDump();
} else {
return this.render().getBase64();
}
},
isSvg: function(){
return this.format.match(/svg/i)
}
};
var Svg = function(size, foreground, background){
this.size = size;
this.foreground = this.color.apply(this, foreground);
this.background = this.color.apply(this, background);
this.rectangles = [];
};
Svg.prototype = {
size: null,
foreground: null,
background: null,
rectangles: null,
color: function(r, g, b, a){
var values = [r, g, b].map(Math.round);
values.push((a >= 0) && (a <= 255) ? a/255 : 1);
return 'rgba(' + values.join(',') + ')';
},
getDump: function(){
var i,
xml,
rect,
fg = this.foreground,
bg = this.background,
stroke = this.size * 0.005;
xml = "<svg xmlns='http://www.w3.org/2000/svg'"
+ " width='" + this.size + "' height='" + this.size + "'"
+ " style='background-color:" + bg + ";'>"
+ "<g style='fill:" + fg + "; stroke:" + fg + "; stroke-width:" + stroke + ";'>";
for (i = 0; i < this.rectangles.length; i++) {
rect = this.rectangles[i];
if (rect.color == bg) continue;
xml += "<rect "
+ " x='" + rect.x + "'"
+ " y='" + rect.y + "'"
+ " width='" + rect.w + "'"
+ " height='" + rect.h + "'"
+ "/>";
}
xml += "</g></svg>"
return xml;
},
getBase64: function(){
if ('function' === typeof btoa) {
return btoa(this.getDump());
} else if (Buffer) {
return new Buffer(this.getDump(), 'binary').toString('base64');
} else {
throw 'Cannot generate base64 output';
}
}
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Identicon;
} else {
window.Identicon = Identicon;
}
})();

View File

@@ -1,20 +0,0 @@
<svg width="132" height="56" viewBox="0 0 132 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5C1.17157 10.5 0.5 11.1716 0.5 12C0.5 12.8284 1.17157 13.5 2 13.5V10.5ZM131.061 13.0607C131.646 12.4749 131.646 11.5251 131.061 10.9393L121.515 1.3934C120.929 0.807611 119.979 0.807611 119.393 1.3934C118.808 1.97919 118.808 2.92893 119.393 3.51472L127.879 12L119.393 20.4853C118.808 21.0711 118.808 22.0208 119.393 22.6066C119.979 23.1924 120.929 23.1924 121.515 22.6066L131.061 13.0607ZM4.95385 13.5C5.78227 13.5 6.45385 12.8284 6.45385 12C6.45385 11.1716 5.78227 10.5 4.95385 10.5V13.5ZM11.8462 10.5C11.0177 10.5 10.3462 11.1716 10.3462 12C10.3462 12.8284 11.0177 13.5 11.8462 13.5V10.5ZM17.7538 13.5C18.5823 13.5 19.2538 12.8284 19.2538 12C19.2538 11.1716 18.5823 10.5 17.7538 10.5V13.5ZM24.6462 10.5C23.8177 10.5 23.1462 11.1716 23.1462 12C23.1462 12.8284 23.8177 13.5 24.6462 13.5V10.5ZM30.5538 13.5C31.3823 13.5 32.0538 12.8284 32.0538 12C32.0538 11.1716 31.3823 10.5 30.5538 10.5V13.5ZM37.4462 10.5C36.6177 10.5 35.9462 11.1716 35.9462 12C35.9462 12.8284 36.6177 13.5 37.4462 13.5V10.5ZM43.3538 13.5C44.1823 13.5 44.8538 12.8284 44.8538 12C44.8538 11.1716 44.1823 10.5 43.3538 10.5V13.5ZM50.2462 10.5C49.4177 10.5 48.7462 11.1716 48.7462 12C48.7462 12.8284 49.4177 13.5 50.2462 13.5V10.5ZM56.1538 13.5C56.9823 13.5 57.6538 12.8284 57.6538 12C57.6538 11.1716 56.9823 10.5 56.1538 10.5V13.5ZM63.0462 10.5C62.2177 10.5 61.5462 11.1716 61.5462 12C61.5462 12.8284 62.2177 13.5 63.0462 13.5V10.5ZM68.9538 13.5C69.7823 13.5 70.4538 12.8284 70.4538 12C70.4538 11.1716 69.7823 10.5 68.9538 10.5V13.5ZM75.8462 10.5C75.0177 10.5 74.3462 11.1716 74.3462 12C74.3462 12.8284 75.0177 13.5 75.8462 13.5V10.5ZM81.7539 13.5C82.5823 13.5 83.2539 12.8284 83.2539 12C83.2539 11.1716 82.5823 10.5 81.7539 10.5V13.5ZM88.6462 10.5C87.8177 10.5 87.1462 11.1716 87.1462 12C87.1462 12.8284 87.8177 13.5 88.6462 13.5V10.5ZM94.5539 13.5C95.3823 13.5 96.0539 12.8284 96.0539 12C96.0539 11.1716 95.3823 10.5 94.5539 10.5V13.5ZM101.446 10.5C100.618 10.5 99.9462 11.1716 99.9462 12C99.9462 12.8284 100.618 13.5 101.446 13.5V10.5ZM107.354 13.5C108.182 13.5 108.854 12.8284 108.854 12C108.854 11.1716 108.182 10.5 107.354 10.5V13.5ZM114.246 10.5C113.418 10.5 112.746 11.1716 112.746 12C112.746 12.8284 113.418 13.5 114.246 13.5V10.5ZM120.154 13.5C120.982 13.5 121.654 12.8284 121.654 12C121.654 11.1716 120.982 10.5 120.154 10.5V13.5ZM127.046 10.5C126.218 10.5 125.546 11.1716 125.546 12C125.546 12.8284 126.218 13.5 127.046 13.5V10.5ZM2 13.5H4.95385V10.5H2V13.5ZM11.8462 13.5H17.7538V10.5H11.8462V13.5ZM24.6462 13.5H30.5538V10.5H24.6462V13.5ZM37.4462 13.5H43.3538V10.5H37.4462V13.5ZM50.2462 13.5H56.1538V10.5H50.2462V13.5ZM63.0462 13.5H68.9538V10.5H63.0462V13.5ZM75.8462 13.5H81.7539V10.5H75.8462V13.5ZM88.6462 13.5H94.5539V10.5H88.6462V13.5ZM101.446 13.5H107.354V10.5H101.446V13.5ZM114.246 13.5H120.154V10.5H114.246V13.5ZM127.046 13.5H130V10.5H127.046V13.5Z" fill="#A1A1AB"/>
<path d="M130 45.5C130.828 45.5 131.5 44.8284 131.5 44C131.5 43.1716 130.828 42.5 130 42.5V45.5ZM0.939331 42.9393C0.353546 43.5251 0.353546 44.4749 0.939331 45.0607L10.4853 54.6066C11.0711 55.1924 12.0208 55.1924 12.6066 54.6066C13.1924 54.0208 13.1924 53.0711 12.6066 52.4853L4.12132 44L12.6066 35.5147C13.1924 34.9289 13.1924 33.9792 12.6066 33.3934C12.0208 32.8076 11.0711 32.8076 10.4853 33.3934L0.939331 42.9393ZM127.046 42.5C126.218 42.5 125.546 43.1716 125.546 44C125.546 44.8284 126.218 45.5 127.046 45.5V42.5ZM120.154 45.5C120.982 45.5 121.654 44.8284 121.654 44C121.654 43.1716 120.982 42.5 120.154 42.5V45.5ZM114.246 42.5C113.418 42.5 112.746 43.1716 112.746 44C112.746 44.8284 113.418 45.5 114.246 45.5V42.5ZM107.354 45.5C108.182 45.5 108.854 44.8284 108.854 44C108.854 43.1716 108.182 42.5 107.354 42.5V45.5ZM101.446 42.5C100.618 42.5 99.9462 43.1716 99.9462 44C99.9462 44.8284 100.618 45.5 101.446 45.5V42.5ZM94.5538 45.5C95.3823 45.5 96.0538 44.8284 96.0538 44C96.0538 43.1716 95.3823 42.5 94.5538 42.5V45.5ZM88.6462 42.5C87.8177 42.5 87.1462 43.1716 87.1462 44C87.1462 44.8284 87.8177 45.5 88.6462 45.5V42.5ZM81.7538 45.5C82.5823 45.5 83.2538 44.8284 83.2538 44C83.2538 43.1716 82.5823 42.5 81.7538 42.5V45.5ZM75.8462 42.5C75.0177 42.5 74.3462 43.1716 74.3462 44C74.3462 44.8284 75.0177 45.5 75.8462 45.5V42.5ZM68.9538 45.5C69.7823 45.5 70.4538 44.8284 70.4538 44C70.4538 43.1716 69.7823 42.5 68.9538 42.5V45.5ZM63.0462 42.5C62.2177 42.5 61.5462 43.1716 61.5462 44C61.5462 44.8284 62.2177 45.5 63.0462 45.5V42.5ZM56.1538 45.5C56.9823 45.5 57.6538 44.8284 57.6538 44C57.6538 43.1716 56.9823 42.5 56.1538 42.5V45.5ZM50.2461 42.5C49.4177 42.5 48.7461 43.1716 48.7461 44C48.7461 44.8284 49.4177 45.5 50.2461 45.5V42.5ZM43.3538 45.5C44.1823 45.5 44.8538 44.8284 44.8538 44C44.8538 43.1716 44.1823 42.5 43.3538 42.5V45.5ZM37.4461 42.5C36.6177 42.5 35.9461 43.1716 35.9461 44C35.9461 44.8284 36.6177 45.5 37.4461 45.5V42.5ZM30.5538 45.5C31.3823 45.5 32.0538 44.8284 32.0538 44C32.0538 43.1716 31.3823 42.5 30.5538 42.5V45.5ZM24.6461 42.5C23.8177 42.5 23.1461 43.1716 23.1461 44C23.1461 44.8284 23.8177 45.5 24.6461 45.5V42.5ZM17.7538 45.5C18.5823 45.5 19.2538 44.8284 19.2538 44C19.2538 43.1716 18.5823 42.5 17.7538 42.5V45.5ZM11.8461 42.5C11.0177 42.5 10.3461 43.1716 10.3461 44C10.3461 44.8284 11.0177 45.5 11.8461 45.5V42.5ZM4.95383 45.5C5.78225 45.5 6.45383 44.8284 6.45383 44C6.45383 43.1716 5.78225 42.5 4.95383 42.5V45.5ZM130 42.5H127.046V45.5H130V42.5ZM120.154 42.5H114.246V45.5H120.154V42.5ZM107.354 42.5H101.446V45.5H107.354V42.5ZM94.5538 42.5H88.6462V45.5H94.5538V42.5ZM81.7538 42.5H75.8462V45.5H81.7538V42.5ZM68.9538 42.5H63.0462V45.5H68.9538V42.5ZM56.1538 42.5H50.2461V45.5H56.1538V42.5ZM43.3538 42.5H37.4461V45.5H43.3538V42.5ZM30.5538 42.5H24.6461V45.5H30.5538V42.5ZM17.7538 42.5H11.8461V45.5H17.7538V42.5ZM4.95383 42.5H2V45.5H4.95383V42.5Z" fill="#A1A1AB"/>
<circle cx="66" cy="28" r="28" fill="#F1F1F1"/>
<g filter="url(#filter0_d_503_299)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M66 48C77.0457 48 86 39.0457 86 28C86 16.9543 77.0457 8 66 8C54.9543 8 46 16.9543 46 28C46 39.0457 54.9543 48 66 48ZM59.6667 37.0667L66.2667 30.4667L72.8667 37.0667C73.1778 37.3778 73.5444 37.5333 73.9667 37.5333C74.3889 37.5333 74.7556 37.3778 75.0667 37.0667C75.3778 36.7556 75.5333 36.3889 75.5333 35.9667C75.5333 35.5444 75.3778 35.1778 75.0667 34.8667L68.4667 28.2667L75.0667 21.6667C75.3778 21.3556 75.5333 20.9889 75.5333 20.5667C75.5333 20.1444 75.3778 19.7778 75.0667 19.4667C74.7556 19.1556 74.3889 19 73.9667 19C73.5444 19 73.1778 19.1556 72.8667 19.4667L66.2667 26.0667L59.6667 19.4667C59.3556 19.1556 58.9889 19 58.5667 19C58.1444 19 57.7778 19.1556 57.4667 19.4667C57.1556 19.7778 57 20.1444 57 20.5667C57 20.9889 57.1556 21.3556 57.4667 21.6667L64.0667 28.2667L57.4667 34.8667C57.1556 35.1778 57 35.5444 57 35.9667C57 36.3889 57.1556 36.7556 57.4667 37.0667C57.7778 37.3778 58.1444 37.5333 58.5667 37.5333C58.9889 37.5333 59.3556 37.3778 59.6667 37.0667Z" fill="#F04444"/>
</g>
<defs>
<filter id="filter0_d_503_299" x="42" y="7" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_299"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_299" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,20 +0,0 @@
<svg width="132" height="56" viewBox="0 0 132 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5C1.17157 10.5 0.5 11.1716 0.5 12C0.5 12.8284 1.17157 13.5 2 13.5V10.5ZM131.061 13.0607C131.646 12.4749 131.646 11.5251 131.061 10.9393L121.515 1.3934C120.929 0.807611 119.979 0.807611 119.393 1.3934C118.808 1.97919 118.808 2.92893 119.393 3.51472L127.879 12L119.393 20.4853C118.808 21.0711 118.808 22.0208 119.393 22.6066C119.979 23.1924 120.929 23.1924 121.515 22.6066L131.061 13.0607ZM2 13.5H130V10.5H2V13.5Z" fill="#A1A1AB"/>
<path d="M130 45.5C130.828 45.5 131.5 44.8284 131.5 44C131.5 43.1716 130.828 42.5 130 42.5V45.5ZM0.939331 42.9393C0.353546 43.5251 0.353546 44.4749 0.939331 45.0607L10.4853 54.6066C11.0711 55.1924 12.0208 55.1924 12.6066 54.6066C13.1924 54.0208 13.1924 53.0711 12.6066 52.4853L4.12132 44L12.6066 35.5147C13.1924 34.9289 13.1924 33.9792 12.6066 33.3934C12.0208 32.8076 11.0711 32.8076 10.4853 33.3934L0.939331 42.9393ZM130 42.5L2 42.5V45.5L130 45.5V42.5Z" fill="#A1A1AB"/>
<circle cx="66" cy="28" r="28" fill="#F1F1F1"/>
<g filter="url(#filter0_d_503_333)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M66 48C77.0457 48 86 39.0457 86 28C86 16.9543 77.0457 8 66 8C54.9543 8 46 16.9543 46 28C46 39.0457 54.9543 48 66 48ZM62.0168 35.7834C62.2057 35.8611 62.4001 35.9 62.6001 35.9C62.8001 35.9 62.9945 35.8611 63.1834 35.7834C63.3723 35.7056 63.5334 35.5889 63.6668 35.4334L76.4334 22.6667C76.7445 22.3778 76.9001 22.0167 76.9001 21.5834C76.9001 21.15 76.7445 20.7778 76.4334 20.4667C76.1445 20.1556 75.789 20.0056 75.3668 20.0167C74.9445 20.0278 74.5779 20.1778 74.2668 20.4667L62.6001 32.1334L57.7334 27.2334C57.4223 26.9445 57.0501 26.8 56.6168 26.8C56.1834 26.8 55.8223 26.9445 55.5334 27.2334C55.2445 27.5222 55.1001 27.8889 55.1001 28.3334C55.1001 28.7778 55.2445 29.1445 55.5334 29.4334L61.5668 35.4334C61.6779 35.5889 61.8279 35.7056 62.0168 35.7834Z" fill="#23C55E"/>
</g>
<defs>
<filter id="filter0_d_503_333" x="42" y="7" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_333"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_333" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,20 +0,0 @@
<svg width="132" height="56" viewBox="0 0 132 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5C1.17157 10.5 0.5 11.1716 0.5 12C0.5 12.8284 1.17157 13.5 2 13.5V10.5ZM131.061 13.0607C131.646 12.4749 131.646 11.5251 131.061 10.9393L121.515 1.3934C120.929 0.807611 119.979 0.807611 119.393 1.3934C118.808 1.97919 118.808 2.92893 119.393 3.51472L127.879 12L119.393 20.4853C118.808 21.0711 118.808 22.0208 119.393 22.6066C119.979 23.1924 120.929 23.1924 121.515 22.6066L131.061 13.0607ZM2 13.5H130V10.5H2V13.5Z" fill="#A1A1AB"/>
<path d="M130 45.5C130.828 45.5 131.5 44.8284 131.5 44C131.5 43.1716 130.828 42.5 130 42.5V45.5ZM0.939331 42.9393C0.353546 43.5251 0.353546 44.4749 0.939331 45.0607L10.4853 54.6066C11.0711 55.1924 12.0208 55.1924 12.6066 54.6066C13.1924 54.0208 13.1924 53.0711 12.6066 52.4853L4.12132 44L12.6066 35.5147C13.1924 34.9289 13.1924 33.9792 12.6066 33.3934C12.0208 32.8076 11.0711 32.8076 10.4853 33.3934L0.939331 42.9393ZM130 42.5L2 42.5V45.5L130 45.5V42.5Z" fill="#A1A1AB"/>
<circle cx="66" cy="28" r="28" fill="#F1F1F1"/>
<g filter="url(#filter0_d_1756_38)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M66 47.9998C77.0457 47.9998 86 39.0455 86 27.9998C86 16.9541 77.0457 7.99976 66 7.99976C54.9543 7.99976 46 16.9541 46 27.9998C46 39.0455 54.9543 47.9998 66 47.9998ZM63.555 32.7332H67.3209V32.0227C67.3209 31.2885 67.4749 30.6608 67.7828 30.1398C68.1144 29.595 68.5288 29.1095 69.0262 28.6832C69.5473 28.2331 70.092 27.8068 70.6605 27.4042C71.2289 26.9779 71.7618 26.516 72.2592 26.0186C72.7802 25.5212 73.1947 24.9528 73.5026 24.3133C73.8342 23.6502 74 22.8686 74 21.9685C74 19.9317 73.325 18.4277 71.975 17.4566C70.6486 16.4855 68.8486 16 66.5748 16C64.7985 16 63.2353 16.3316 61.8853 16.9948C60.5589 17.6579 59.5405 18.5698 58.83 19.7303C58.1431 20.8909 57.8826 22.2291 58.0484 23.7449L61.4234 26.1607C61.2339 24.716 61.3287 23.5199 61.7076 22.5725C62.1103 21.6251 62.7142 20.9264 63.5195 20.4764C64.3248 20.0027 65.2485 19.7659 66.2906 19.7659C67.4749 19.7659 68.3275 20.0146 68.8486 20.5119C69.3933 20.9856 69.6657 21.6014 69.6657 22.3593C69.6657 22.9751 69.5117 23.508 69.2038 23.9581C68.9196 24.4081 68.5407 24.8225 68.067 25.2015C67.617 25.5805 67.1314 25.9713 66.6104 26.3739C66.0893 26.7765 65.5919 27.2147 65.1182 27.6884C64.6682 28.1621 64.2893 28.7305 63.9814 29.3937C63.6972 30.0332 63.555 30.8029 63.555 31.703V32.7332ZM63.1287 40.1229H67.6762V35.078H63.1287V40.1229Z" fill="#E9B308"/>
</g>
<defs>
<filter id="filter0_d_1756_38" x="42" y="6.99976" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1756_38"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1756_38" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,20 +0,0 @@
<svg width="132" height="56" viewBox="0 0 132 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5C1.17157 10.5 0.5 11.1716 0.5 12C0.5 12.8284 1.17157 13.5 2 13.5V10.5ZM131.061 13.0607C131.646 12.4749 131.646 11.5251 131.061 10.9393L121.515 1.3934C120.929 0.807611 119.979 0.807611 119.393 1.3934C118.808 1.97919 118.808 2.92893 119.393 3.51472L127.879 12L119.393 20.4853C118.808 21.0711 118.808 22.0208 119.393 22.6066C119.979 23.1924 120.929 23.1924 121.515 22.6066L131.061 13.0607ZM4.95385 13.5C5.78227 13.5 6.45385 12.8284 6.45385 12C6.45385 11.1716 5.78227 10.5 4.95385 10.5V13.5ZM11.8462 10.5C11.0177 10.5 10.3462 11.1716 10.3462 12C10.3462 12.8284 11.0177 13.5 11.8462 13.5V10.5ZM17.7538 13.5C18.5823 13.5 19.2538 12.8284 19.2538 12C19.2538 11.1716 18.5823 10.5 17.7538 10.5V13.5ZM24.6462 10.5C23.8177 10.5 23.1462 11.1716 23.1462 12C23.1462 12.8284 23.8177 13.5 24.6462 13.5V10.5ZM30.5538 13.5C31.3823 13.5 32.0538 12.8284 32.0538 12C32.0538 11.1716 31.3823 10.5 30.5538 10.5V13.5ZM37.4462 10.5C36.6177 10.5 35.9462 11.1716 35.9462 12C35.9462 12.8284 36.6177 13.5 37.4462 13.5V10.5ZM43.3538 13.5C44.1823 13.5 44.8538 12.8284 44.8538 12C44.8538 11.1716 44.1823 10.5 43.3538 10.5V13.5ZM50.2462 10.5C49.4177 10.5 48.7462 11.1716 48.7462 12C48.7462 12.8284 49.4177 13.5 50.2462 13.5V10.5ZM56.1538 13.5C56.9823 13.5 57.6538 12.8284 57.6538 12C57.6538 11.1716 56.9823 10.5 56.1538 10.5V13.5ZM63.0462 10.5C62.2177 10.5 61.5462 11.1716 61.5462 12C61.5462 12.8284 62.2177 13.5 63.0462 13.5V10.5ZM68.9538 13.5C69.7823 13.5 70.4538 12.8284 70.4538 12C70.4538 11.1716 69.7823 10.5 68.9538 10.5V13.5ZM75.8462 10.5C75.0177 10.5 74.3462 11.1716 74.3462 12C74.3462 12.8284 75.0177 13.5 75.8462 13.5V10.5ZM81.7539 13.5C82.5823 13.5 83.2539 12.8284 83.2539 12C83.2539 11.1716 82.5823 10.5 81.7539 10.5V13.5ZM88.6462 10.5C87.8177 10.5 87.1462 11.1716 87.1462 12C87.1462 12.8284 87.8177 13.5 88.6462 13.5V10.5ZM94.5539 13.5C95.3823 13.5 96.0539 12.8284 96.0539 12C96.0539 11.1716 95.3823 10.5 94.5539 10.5V13.5ZM101.446 10.5C100.618 10.5 99.9462 11.1716 99.9462 12C99.9462 12.8284 100.618 13.5 101.446 13.5V10.5ZM107.354 13.5C108.182 13.5 108.854 12.8284 108.854 12C108.854 11.1716 108.182 10.5 107.354 10.5V13.5ZM114.246 10.5C113.418 10.5 112.746 11.1716 112.746 12C112.746 12.8284 113.418 13.5 114.246 13.5V10.5ZM120.154 13.5C120.982 13.5 121.654 12.8284 121.654 12C121.654 11.1716 120.982 10.5 120.154 10.5V13.5ZM127.046 10.5C126.218 10.5 125.546 11.1716 125.546 12C125.546 12.8284 126.218 13.5 127.046 13.5V10.5ZM2 13.5H4.95385V10.5H2V13.5ZM11.8462 13.5H17.7538V10.5H11.8462V13.5ZM24.6462 13.5H30.5538V10.5H24.6462V13.5ZM37.4462 13.5H43.3538V10.5H37.4462V13.5ZM50.2462 13.5H56.1538V10.5H50.2462V13.5ZM63.0462 13.5H68.9538V10.5H63.0462V13.5ZM75.8462 13.5H81.7539V10.5H75.8462V13.5ZM88.6462 13.5H94.5539V10.5H88.6462V13.5ZM101.446 13.5H107.354V10.5H101.446V13.5ZM114.246 13.5H120.154V10.5H114.246V13.5ZM127.046 13.5H130V10.5H127.046V13.5Z" fill="#A1A1AB"/>
<path d="M130 45.5C130.828 45.5 131.5 44.8284 131.5 44C131.5 43.1716 130.828 42.5 130 42.5V45.5ZM0.939331 42.9393C0.353546 43.5251 0.353546 44.4749 0.939331 45.0607L10.4853 54.6066C11.0711 55.1924 12.0208 55.1924 12.6066 54.6066C13.1924 54.0208 13.1924 53.0711 12.6066 52.4853L4.12132 44L12.6066 35.5147C13.1924 34.9289 13.1924 33.9792 12.6066 33.3934C12.0208 32.8076 11.0711 32.8076 10.4853 33.3934L0.939331 42.9393ZM127.046 42.5C126.218 42.5 125.546 43.1716 125.546 44C125.546 44.8284 126.218 45.5 127.046 45.5V42.5ZM120.154 45.5C120.982 45.5 121.654 44.8284 121.654 44C121.654 43.1716 120.982 42.5 120.154 42.5V45.5ZM114.246 42.5C113.418 42.5 112.746 43.1716 112.746 44C112.746 44.8284 113.418 45.5 114.246 45.5V42.5ZM107.354 45.5C108.182 45.5 108.854 44.8284 108.854 44C108.854 43.1716 108.182 42.5 107.354 42.5V45.5ZM101.446 42.5C100.618 42.5 99.9462 43.1716 99.9462 44C99.9462 44.8284 100.618 45.5 101.446 45.5V42.5ZM94.5538 45.5C95.3823 45.5 96.0538 44.8284 96.0538 44C96.0538 43.1716 95.3823 42.5 94.5538 42.5V45.5ZM88.6462 42.5C87.8177 42.5 87.1462 43.1716 87.1462 44C87.1462 44.8284 87.8177 45.5 88.6462 45.5V42.5ZM81.7538 45.5C82.5823 45.5 83.2538 44.8284 83.2538 44C83.2538 43.1716 82.5823 42.5 81.7538 42.5V45.5ZM75.8462 42.5C75.0177 42.5 74.3462 43.1716 74.3462 44C74.3462 44.8284 75.0177 45.5 75.8462 45.5V42.5ZM68.9538 45.5C69.7823 45.5 70.4538 44.8284 70.4538 44C70.4538 43.1716 69.7823 42.5 68.9538 42.5V45.5ZM63.0462 42.5C62.2177 42.5 61.5462 43.1716 61.5462 44C61.5462 44.8284 62.2177 45.5 63.0462 45.5V42.5ZM56.1538 45.5C56.9823 45.5 57.6538 44.8284 57.6538 44C57.6538 43.1716 56.9823 42.5 56.1538 42.5V45.5ZM50.2461 42.5C49.4177 42.5 48.7461 43.1716 48.7461 44C48.7461 44.8284 49.4177 45.5 50.2461 45.5V42.5ZM43.3538 45.5C44.1823 45.5 44.8538 44.8284 44.8538 44C44.8538 43.1716 44.1823 42.5 43.3538 42.5V45.5ZM37.4461 42.5C36.6177 42.5 35.9461 43.1716 35.9461 44C35.9461 44.8284 36.6177 45.5 37.4461 45.5V42.5ZM30.5538 45.5C31.3823 45.5 32.0538 44.8284 32.0538 44C32.0538 43.1716 31.3823 42.5 30.5538 42.5V45.5ZM24.6461 42.5C23.8177 42.5 23.1461 43.1716 23.1461 44C23.1461 44.8284 23.8177 45.5 24.6461 45.5V42.5ZM17.7538 45.5C18.5823 45.5 19.2538 44.8284 19.2538 44C19.2538 43.1716 18.5823 42.5 17.7538 42.5V45.5ZM11.8461 42.5C11.0177 42.5 10.3461 43.1716 10.3461 44C10.3461 44.8284 11.0177 45.5 11.8461 45.5V42.5ZM4.95383 45.5C5.78225 45.5 6.45383 44.8284 6.45383 44C6.45383 43.1716 5.78225 42.5 4.95383 42.5V45.5ZM130 42.5H127.046V45.5H130V42.5ZM120.154 42.5H114.246V45.5H120.154V42.5ZM107.354 42.5H101.446V45.5H107.354V42.5ZM94.5538 42.5H88.6462V45.5H94.5538V42.5ZM81.7538 42.5H75.8462V45.5H81.7538V42.5ZM68.9538 42.5H63.0462V45.5H68.9538V42.5ZM56.1538 42.5H50.2461V45.5H56.1538V42.5ZM43.3538 42.5H37.4461V45.5H43.3538V42.5ZM30.5538 42.5H24.6461V45.5H30.5538V42.5ZM17.7538 42.5H11.8461V45.5H17.7538V42.5ZM4.95383 42.5H2V45.5H4.95383V42.5Z" fill="#A1A1AB"/>
<circle cx="66" cy="28" r="28" fill="#F1F1F1"/>
<g filter="url(#filter0_d_503_316)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M66 48C77.0457 48 86 39.0457 86 28C86 16.9543 77.0457 8 66 8C54.9543 8 46 16.9543 46 28C46 39.0457 54.9543 48 66 48ZM67.4585 27.0375L69.4418 29.05C69.714 29.05 69.9522 28.9479 70.1564 28.7438C70.3605 28.5396 70.4626 28.3014 70.4626 28.0292C70.4626 27.757 70.3605 27.5236 70.1564 27.3292C69.9522 27.1348 69.714 27.0375 69.4418 27.0375H67.4585ZM72.1835 31.8209L74.3418 33.9209C75.4112 33.4542 76.3251 32.6959 77.0835 31.6459C77.8418 30.5959 78.221 29.4098 78.221 28.0875C78.221 26.2403 77.589 24.6799 76.3251 23.4063C75.0612 22.1327 73.496 21.4959 71.6293 21.4959H68.8293C68.4404 21.4959 68.1147 21.632 67.8522 21.9042C67.5897 22.1764 67.4585 22.507 67.4585 22.8959C67.4585 23.2848 67.5897 23.6104 67.8522 23.8729C68.1147 24.1354 68.4404 24.2667 68.8293 24.2667H71.6585C72.7474 24.2667 73.6515 24.6264 74.371 25.3459C75.0904 26.0653 75.4501 26.9792 75.4501 28.0875C75.4501 29.0014 75.1487 29.8084 74.546 30.5084C73.9432 31.2084 73.1557 31.6459 72.1835 31.8209ZM63.9863 29.05L74.546 39.5792C74.7599 39.8125 75.0078 39.9292 75.2897 39.9292C75.5717 39.9292 75.8196 39.8125 76.0335 39.5792C76.2474 39.3653 76.3543 39.1271 76.3543 38.8646C76.3543 38.6021 76.2474 38.3639 76.0335 38.15L55.821 17.9375C55.5876 17.7236 55.3349 17.6167 55.0626 17.6167C54.7904 17.6167 54.5474 17.7236 54.3335 17.9375C54.1001 18.1709 53.9883 18.4236 53.998 18.6959C54.0078 18.9681 54.1196 19.2111 54.3335 19.425L57.1687 22.2521C56.3256 22.7064 55.609 23.3294 55.0189 24.1209C54.1925 25.2292 53.7793 26.5223 53.7793 28C53.7793 29.8473 54.4112 31.4028 55.6751 32.6667C56.939 33.9306 58.5043 34.5625 60.371 34.5625H63.6085C63.9974 34.5625 64.3279 34.4313 64.6001 34.1688C64.8724 33.9063 65.0085 33.5806 65.0085 33.1917C65.0085 32.8028 64.8724 32.4771 64.6001 32.2146C64.3279 31.9521 63.9974 31.8209 63.6085 31.8209H60.371C59.2626 31.8209 58.3487 31.4611 57.6293 30.7417C56.9099 30.0223 56.5501 29.1084 56.5501 28C56.5501 26.8917 56.9099 25.9778 57.6293 25.2584C58.0882 24.7995 58.6262 24.4869 59.2433 24.3207L62.0223 27.0916C61.9526 27.1327 61.8883 27.1827 61.8293 27.2417C61.6349 27.4361 61.5376 27.6889 61.5376 28C61.5376 28.3111 61.6397 28.5639 61.8439 28.7584C62.048 28.9528 62.296 29.05 62.5876 29.05H63.9863Z" fill="#71717B"/>
</g>
<defs>
<filter id="filter0_d_503_316" x="42" y="7" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_316"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_316" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,20 +0,0 @@
<svg width="132" height="56" viewBox="0 0 132 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5C1.17157 10.5 0.5 11.1716 0.5 12C0.5 12.8284 1.17157 13.5 2 13.5V10.5ZM131.061 13.0607C131.646 12.4749 131.646 11.5251 131.061 10.9393L121.515 1.3934C120.929 0.807611 119.979 0.807611 119.393 1.3934C118.808 1.97919 118.808 2.92893 119.393 3.51472L127.879 12L119.393 20.4853C118.808 21.0711 118.808 22.0208 119.393 22.6066C119.979 23.1924 120.929 23.1924 121.515 22.6066L131.061 13.0607ZM2 13.5H130V10.5H2V13.5Z" fill="#A1A1AB"/>
<path d="M130 45.5C130.828 45.5 131.5 44.8284 131.5 44C131.5 43.1716 130.828 42.5 130 42.5V45.5ZM0.939331 42.9393C0.353546 43.5251 0.353546 44.4749 0.939331 45.0607L10.4853 54.6066C11.0711 55.1924 12.0208 55.1924 12.6066 54.6066C13.1924 54.0208 13.1924 53.0711 12.6066 52.4853L4.12132 44L12.6066 35.5147C13.1924 34.9289 13.1924 33.9792 12.6066 33.3934C12.0208 32.8076 11.0711 32.8076 10.4853 33.3934L0.939331 42.9393ZM130 42.5L2 42.5V45.5L130 45.5V42.5Z" fill="#A1A1AB"/>
<circle cx="66" cy="28" r="28" fill="#F1F1F1"/>
<g filter="url(#filter0_d_505_398)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M66 48C77.0457 48 86 39.0457 86 28C86 16.9543 77.0457 8 66 8C54.9543 8 46 16.9543 46 28C46 39.0457 54.9543 48 66 48ZM67.5 17.75C67.5 18.7165 66.7165 19.5 65.75 19.5C64.7835 19.5 64 18.7165 64 17.75C64 16.7835 64.7835 16 65.75 16C66.7165 16 67.5 16.7835 67.5 17.75ZM65.75 23C66.5784 23 67.25 23.6716 67.25 24.5V38.5C67.25 39.3284 66.5784 40 65.75 40C64.9216 40 64.25 39.3284 64.25 38.5V24.5C64.25 23.6716 64.9216 23 65.75 23Z" fill="#E9B308"/>
</g>
<defs>
<filter id="filter0_d_505_398" x="42" y="7" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_505_398"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_505_398" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,27 +0,0 @@
<svg width="275" height="125" viewBox="0 0 275 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_937_158)">
<rect width="275" height="125" rx="30" fill="url(#paint0_linear_937_158)"/>
<g style="mix-blend-mode:overlay">
<mask id="mask0_937_158" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="128" y="-28" width="195" height="195">
<rect x="128" y="13.1033" width="158.811" height="158.811" transform="rotate(-15 128 13.1033)" fill="url(#paint1_radial_937_158)"/>
</mask>
<g mask="url(#mask0_937_158)">
<path d="M190.662 16.8646L187.237 4.08129L225.587 -6.19454L229.012 6.58877L190.662 16.8646ZM222.285 83.7475L235.068 80.3222L224.792 41.9723L212.009 45.3976L222.285 83.7475ZM242.377 133.168C234.494 135.28 226.657 135.755 218.864 134.591C211.074 133.422 203.87 130.927 197.252 127.105C190.633 123.284 184.871 118.293 179.963 112.132C175.059 105.965 171.551 98.94 169.439 91.0569C167.327 83.1739 166.852 75.3361 168.016 67.5435C169.185 59.754 171.68 52.55 175.501 45.9315C179.323 39.313 184.315 33.5522 190.477 28.6493C196.643 23.7409 203.667 20.2306 211.55 18.1183C218.155 16.3486 224.779 15.7155 231.421 16.2191C238.064 16.7226 244.591 18.2848 251.002 20.9056L257.553 9.55965L268.899 16.1103L262.348 27.4563C267.824 31.698 272.44 36.5694 276.198 42.0706C279.955 47.5718 282.719 53.6248 284.489 60.2295C286.601 68.1125 287.075 75.9503 285.911 83.7429C284.742 91.5324 282.247 98.7364 278.426 105.355C274.605 111.973 269.614 117.736 263.452 122.644C257.285 127.548 250.261 131.056 242.377 133.168ZM238.952 120.385C251.309 117.074 260.685 109.88 267.08 98.8043C273.475 87.7285 275.016 76.0119 271.705 63.6547C268.394 51.2975 261.201 41.9216 250.125 35.527C239.049 29.1323 227.332 27.5905 214.975 30.9016C202.618 34.2127 193.242 41.4062 186.847 52.4821C180.453 63.5579 178.911 75.2745 182.222 87.6317C185.533 99.9889 192.727 109.365 203.803 115.759C214.878 122.154 226.595 123.696 238.952 120.385Z" fill="#1C1B1F"/>
</g>
</g>
<path d="M39.175 38.55H24.65V35.775L32.6 25.25H33.65V29.175H33.25L28.45 35.45H39.175V38.55ZM36.6 42H33.025V25.25H36.6V42ZM47.8797 42.225C46.2797 42.225 44.9297 41.8583 43.8297 41.125C42.7297 40.375 41.888 39.35 41.3047 38.05C40.738 36.75 40.4547 35.2667 40.4547 33.6C40.4547 31.9333 40.738 30.4583 41.3047 29.175C41.888 27.875 42.7297 26.8583 43.8297 26.125C44.9297 25.375 46.2797 25 47.8797 25C49.463 25 50.8047 25.375 51.9047 26.125C53.0214 26.8583 53.863 27.875 54.4297 29.175C54.9964 30.4583 55.2797 31.9333 55.2797 33.6C55.2797 35.2667 54.9964 36.75 54.4297 38.05C53.863 39.35 53.0214 40.375 51.9047 41.125C50.8047 41.8583 49.463 42.225 47.8797 42.225ZM47.8797 38.75C48.7464 38.75 49.4464 38.5417 49.9797 38.125C50.513 37.7083 50.9047 37.1083 51.1547 36.325C51.4047 35.5417 51.5297 34.6333 51.5297 33.6C51.5297 32.55 51.4047 31.6417 51.1547 30.875C50.9047 30.1083 50.513 29.5167 49.9797 29.1C49.4464 28.6667 48.7464 28.45 47.8797 28.45C47.013 28.45 46.313 28.6667 45.7797 29.1C45.2464 29.5167 44.8547 30.1083 44.6047 30.875C44.3547 31.6417 44.2297 32.55 44.2297 33.6C44.2297 34.6333 44.3547 35.5417 44.6047 36.325C44.8547 37.1083 45.2464 37.7083 45.7797 38.125C46.313 38.5417 47.013 38.75 47.8797 38.75ZM61.2248 42H57.6498V25.25H61.2248V42ZM69.5998 42H58.8998V38.825H69.5998V42Z" fill="#B42323"/>
</g>
<defs>
<linearGradient id="paint0_linear_937_158" x1="50.5" y1="-12" x2="244" y2="144.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC7C7"/>
<stop offset="1" stop-color="#FA9C9C"/>
</linearGradient>
<radialGradient id="paint1_radial_937_158" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(207.405 92.5088) rotate(90) scale(84.3683)">
<stop offset="0.208333" stop-color="white" stop-opacity="0.78"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<clipPath id="clip0_937_158">
<rect width="275" height="125" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,33 +0,0 @@
<svg width="275" height="125" viewBox="0 0 275 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_601_13)">
<rect width="275" height="125" rx="30" fill="url(#paint0_linear_601_13)"/>
<g style="mix-blend-mode:overlay" opacity="0.5">
<mask id="mask0_601_13" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="92" y="-32" width="209" height="208">
<rect x="92.9521" y="12.3839" width="169.169" height="169.169" transform="rotate(-15 92.9521 12.3839)" fill="url(#paint1_radial_601_13)"/>
</mask>
<g mask="url(#mask0_601_13)">
<rect x="201.286" y="49.032" width="42.2923" height="42.2923" transform="rotate(-15 201.286 49.032)" fill="black"/>
<rect x="115.499" y="72.0188" width="88.8138" height="42.2923" transform="rotate(-15 115.499 72.0188)" fill="url(#paint2_linear_601_13)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M204.462 106.925C216.305 116.473 229.328 121.132 243.531 120.903C255.945 114 264.896 103.454 270.383 89.2622C275.866 75.0756 276.667 60.738 272.785 46.2494L263.437 11.3625L212.882 6.51932L171.522 35.9911L175.681 51.5145L187.171 48.436L185.118 40.7764L216.138 18.6725L254.055 22.3049L261.295 49.328C264.386 60.8617 263.94 72.2191 259.958 83.4002C255.976 94.5814 249.427 103.079 240.313 108.892C229.512 108.415 219.593 104.33 210.553 96.6381C209.983 96.1531 209.425 95.6605 208.879 95.1602L195.757 98.6763C198.37 101.605 201.271 104.355 204.462 106.925Z" fill="black"/>
</g>
</g>
<path d="M27.925 42H24.125L30.4 25.25H35L41.275 42H37.35L33.65 31.65L32.65 28.1L31.65 31.65L27.925 42ZM37.125 38.275H27.625V35.25H37.125V38.275ZM48.8486 42H43.3236V38.825H48.5486C49.582 38.825 50.432 38.6333 51.0986 38.25C51.782 37.85 52.2903 37.2667 52.6236 36.5C52.957 35.7167 53.1236 34.7583 53.1236 33.625C53.1236 32.475 52.9486 31.5167 52.5986 30.75C52.2653 29.9833 51.757 29.4083 51.0736 29.025C50.3903 28.625 49.532 28.425 48.4986 28.425H43.3236V25.25H48.7986C50.4986 25.25 51.9486 25.6 53.1486 26.3C54.3653 27 55.2903 27.975 55.9236 29.225C56.557 30.475 56.8736 31.9417 56.8736 33.625C56.8736 35.2917 56.557 36.7583 55.9236 38.025C55.2903 39.275 54.3736 40.25 53.1736 40.95C51.9903 41.65 50.5486 42 48.8486 42ZM45.6486 42H42.0736V25.25H45.6486V42ZM65.9914 37.15H60.4414V33.975H65.8664C66.4831 33.975 67.0247 33.8833 67.4914 33.7C67.9747 33.5167 68.3497 33.225 68.6164 32.825C68.8997 32.425 69.0414 31.9 69.0414 31.25C69.0414 30.55 68.8997 30 68.6164 29.6C68.3497 29.1833 67.9747 28.8833 67.4914 28.7C67.0247 28.5167 66.4831 28.425 65.8664 28.425H60.4414V25.25H65.9914C67.2747 25.25 68.4247 25.4583 69.4414 25.875C70.4747 26.2917 71.2914 26.9417 71.8914 27.825C72.4914 28.6917 72.7914 29.8333 72.7914 31.25C72.7914 32.6333 72.4914 33.7583 71.8914 34.625C71.3081 35.4917 70.4997 36.1333 69.4664 36.55C68.4497 36.95 67.2914 37.15 65.9914 37.15ZM62.6164 42H59.0414V25.25H62.6164V42ZM78.1682 42H74.5932V25.25H79.3182L82.0432 31.475L83.6182 36.45L85.1932 31.475L87.9182 25.25H92.6432V42H89.0682V35.725L89.5682 28.975L87.5682 34.55L85.4932 39.1H81.7432L79.6682 34.55L77.6432 28.975L78.1682 35.725V42Z" fill="#235DB4"/>
</g>
<defs>
<linearGradient id="paint0_linear_601_13" x1="50.5" y1="-12" x2="244" y2="144.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#C7DAFF"/>
<stop offset="1" stop-color="#9CBCFA"/>
</linearGradient>
<radialGradient id="paint1_radial_601_13" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(177.537 96.9684) rotate(90) scale(89.8711)">
<stop offset="0.208333" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint2_linear_601_13" x1="115.499" y1="72.0188" x2="204.313" y2="72.0188" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-opacity="0.4"/>
</linearGradient>
<clipPath id="clip0_601_13">
<rect width="275" height="125" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,44 +0,0 @@
<svg width="275" height="125" viewBox="0 0 275 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_601_29)">
<rect width="275" height="125" rx="30" fill="url(#paint0_linear_601_29)"/>
<path d="M27.925 42H24.125L30.4 25.25H35L41.275 42H37.35L33.65 31.65L32.65 28.1L31.65 31.65L27.925 42ZM37.125 38.275H27.625V35.25H37.125V38.275ZM49.0236 37.15H43.4736V33.975H48.8986C49.5153 33.975 50.057 33.8833 50.5236 33.7C51.007 33.5167 51.382 33.225 51.6486 32.825C51.932 32.425 52.0736 31.9 52.0736 31.25C52.0736 30.55 51.932 30 51.6486 29.6C51.382 29.1833 51.007 28.8833 50.5236 28.7C50.057 28.5167 49.5153 28.425 48.8986 28.425H43.4736V25.25H49.0236C50.307 25.25 51.457 25.4583 52.4736 25.875C53.507 26.2917 54.3236 26.9417 54.9236 27.825C55.5236 28.6917 55.8236 29.8333 55.8236 31.25C55.8236 32.6333 55.5236 33.7583 54.9236 34.625C54.3403 35.4917 53.532 36.1333 52.4986 36.55C51.482 36.95 50.3236 37.15 49.0236 37.15ZM45.6486 42H42.0736V25.25H45.6486V42ZM61.2004 42H57.6254V25.25H62.3504L65.0754 31.475L66.6504 36.45L68.2254 31.475L70.9504 25.25H75.6754V42H72.1004V35.725L72.6004 28.975L70.6004 34.55L68.5254 39.1H64.7754L62.7004 34.55L60.6754 28.975L61.2004 35.725V42Z" fill="#B5530A"/>
<g style="mix-blend-mode:overlay" opacity="0.8">
<mask id="mask0_601_29" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="122" y="-15" width="154" height="154">
<rect x="122" y="18" width="125" height="125" transform="rotate(-15 122 18)" fill="url(#paint1_radial_601_29)"/>
</mask>
<g mask="url(#mask0_601_29)">
<rect x="178.355" y="10.9877" width="31.25" height="31.25" transform="rotate(-15 178.355 10.9877)" fill="black"/>
<rect x="217.141" y="41.0355" width="31.25" height="31.25" transform="rotate(-15 217.141 41.0355)" fill="black"/>
<rect x="181.974" y="90.8991" width="31.25" height="31.25" transform="rotate(-15 181.974 90.8991)" fill="black"/>
<rect x="124.022" y="25.5463" width="56.25" height="31.25" transform="rotate(-15 124.022 25.5463)" fill="url(#paint2_linear_601_29)"/>
<rect x="144.242" y="101.009" width="39.0625" height="31.25" transform="rotate(-15 144.242 101.009)" fill="url(#paint3_linear_601_29)"/>
<rect x="134.132" y="63.2778" width="85.9375" height="31.25" transform="rotate(-15 134.132 63.2778)" fill="url(#paint4_linear_601_29)"/>
</g>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_601_29" x1="50.5" y1="-12" x2="244" y2="144.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEAC7"/>
<stop offset="1" stop-color="#FACF9C"/>
</linearGradient>
<radialGradient id="paint1_radial_601_29" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(184.5 80.5) rotate(90) scale(66.4062)">
<stop offset="0.208333" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint2_linear_601_29" x1="124.022" y1="25.5463" x2="180.272" y2="25.5463" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint3_linear_601_29" x1="144.242" y1="101.009" x2="183.305" y2="101.009" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint4_linear_601_29" x1="134.132" y1="63.2778" x2="220.07" y2="63.2778" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-opacity="0.4"/>
</linearGradient>
<clipPath id="clip0_601_29">
<rect width="275" height="125" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,27 +0,0 @@
<svg width="275" height="125" viewBox="0 0 275 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_937_164)">
<rect width="275" height="125" rx="30" fill="url(#paint0_linear_937_164)"/>
<g style="mix-blend-mode:overlay" opacity="0.5">
<mask id="mask0_937_164" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="120" y="-47" width="225" height="225">
<rect x="120" y="0.515015" width="183.584" height="183.584" transform="rotate(-15 120 0.515015)" fill="url(#paint1_radial_937_164)"/>
</mask>
<g mask="url(#mask0_937_164)">
<path d="M235.464 135.879L228.994 82.1788L203.133 89.1081C201.286 89.6031 200.014 89.416 199.316 88.547C198.624 87.6767 198.539 86.2475 199.062 84.2595L221.993 -3.0564L229.382 -5.03619L235.852 48.6645L261.712 41.7352C263.56 41.2402 264.829 41.4279 265.522 42.2983C266.219 43.1673 266.306 44.5958 265.783 46.5838L242.852 133.9L235.464 135.879Z" fill="#1C1B1F"/>
</g>
</g>
<path d="M33.475 42H26.95V38.825H32.95C33.9 38.825 34.5917 38.6667 35.025 38.35C35.4583 38.0333 35.675 37.5333 35.675 36.85C35.675 36.4 35.5667 36.0333 35.35 35.75C35.15 35.4667 34.85 35.2583 34.45 35.125C34.05 34.975 33.5667 34.9 33 34.9H26.95V32.025H32.675C33.1417 32.025 33.5417 31.9667 33.875 31.85C34.225 31.7333 34.4917 31.55 34.675 31.3C34.875 31.0333 34.975 30.6833 34.975 30.25C34.975 29.7833 34.875 29.425 34.675 29.175C34.4917 28.9083 34.225 28.7167 33.875 28.6C33.525 28.4833 33.1083 28.425 32.625 28.425H26.95V25.25H33.575C34.8083 25.25 35.8 25.4417 36.55 25.825C37.3167 26.1917 37.875 26.7 38.225 27.35C38.575 28 38.75 28.7167 38.75 29.5C38.75 30.2167 38.6 30.825 38.3 31.325C38.0167 31.8083 37.6333 32.2 37.15 32.5C36.6667 32.7833 36.1167 33 35.5 33.15C34.8833 33.2833 34.25 33.3667 33.6 33.4C35.0167 33.45 36.1417 33.6583 36.975 34.025C37.825 34.3917 38.4333 34.8917 38.8 35.525C39.1667 36.1417 39.35 36.8417 39.35 37.625C39.35 38.675 39.1083 39.5167 38.625 40.15C38.1417 40.7833 37.4583 41.25 36.575 41.55C35.6917 41.85 34.6583 42 33.475 42ZM28.925 42H25.35V25.25H28.925V42ZM44.9871 42H41.4121V25.25H44.9871V42ZM51.0906 28.3H47.5156V25.25H51.0906V28.3ZM51.0906 42H47.5156V29.75H51.0906V42ZM58.4691 42.25C57.5691 42.25 56.8191 42.0833 56.2191 41.75C55.6358 41.4167 55.2025 40.9583 54.9191 40.375C54.6358 39.7917 54.4941 39.125 54.4941 38.375V28.3L58.0691 26.425V37.725C58.0691 38.225 58.1775 38.575 58.3941 38.775C58.6275 38.975 58.9191 39.075 59.2691 39.075C59.6525 39.075 60.0025 38.95 60.3191 38.7C60.6525 38.4333 60.9108 38.125 61.0941 37.775L62.1191 40.7C61.8525 41.0333 61.4108 41.375 60.7941 41.725C60.1775 42.075 59.4025 42.25 58.4691 42.25ZM61.4691 32.475H52.6691V29.75H61.4691V32.475ZM73.9848 42H62.7848V39.375L67.0348 34.725L69.6598 32.475L66.0348 32.65H63.1348V29.75H73.7348V32.375L69.4098 36.8L66.6348 39.25L70.4098 39.1H73.9848V42Z" fill="#8E23B4"/>
</g>
<defs>
<linearGradient id="paint0_linear_937_164" x1="50.5" y1="-12" x2="244" y2="144.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#EAC7FF"/>
<stop offset="1" stop-color="#D19CFA"/>
</linearGradient>
<radialGradient id="paint1_radial_937_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(211.792 92.3071) rotate(90) scale(97.5291)">
<stop offset="0.208333" stop-color="white" stop-opacity="0.78"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<clipPath id="clip0_937_164">
<rect width="275" height="125" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,36 +0,0 @@
<svg width="275" height="125" viewBox="0 0 275 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_601_57)">
<rect width="275" height="125" rx="30" fill="url(#paint0_linear_601_57)"/>
<g style="mix-blend-mode:overlay">
<mask id="mask0_601_57" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="122" y="-15" width="154" height="154">
<rect x="122" y="18" width="125" height="125" transform="rotate(-15 122 18)" fill="url(#paint1_radial_601_57)"/>
</mask>
<g mask="url(#mask0_601_57)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M190.033 96.8278L146.264 108.556L154.352 138.741L213.213 122.969L209.169 107.876L194.077 111.92L190.033 96.8278ZM275.093 106.388L231.324 118.116L223.236 87.9309L267.005 76.2032L275.093 106.388Z" fill="black"/>
<rect x="138.176" y="78.3704" width="45.3125" height="31.25" transform="rotate(-15 138.176 78.3704)" fill="black"/>
<rect x="215.148" y="57.7457" width="45.3125" height="31.25" transform="rotate(-15 215.148 57.7457)" fill="black"/>
<rect x="167.278" y="5.86786" width="31.25" height="62.5" transform="rotate(-15 167.278 5.86786)" fill="url(#paint2_linear_601_57)"/>
<rect x="198.547" y="62.1942" width="15.625" height="31.25" transform="rotate(-15 198.547 62.1942)" fill="black"/>
<rect x="179.41" y="51.1456" width="15.625" height="31.25" transform="rotate(-15 179.41 51.1456)" fill="black"/>
</g>
</g>
<path d="M28.925 42H25.35V25.25H28.925V42ZM37.3 42H26.6V38.825H37.3V42ZM37.685 32.425L36.985 30.625C37.4516 30.6083 37.8433 30.525 38.16 30.375C38.4933 30.225 38.66 29.9167 38.66 29.45V29.125H36.935V25.25H40.735V29.225C40.735 30.1917 40.4766 30.9417 39.96 31.475C39.46 32.0083 38.7016 32.325 37.685 32.425ZM49.8781 37.15H44.3281V33.975H49.7531C50.3698 33.975 50.9115 33.8833 51.3781 33.7C51.8615 33.5167 52.2365 33.225 52.5031 32.825C52.7865 32.425 52.9281 31.9 52.9281 31.25C52.9281 30.55 52.7865 30 52.5031 29.6C52.2365 29.1833 51.8615 28.8833 51.3781 28.7C50.9115 28.5167 50.3698 28.425 49.7531 28.425H44.3281V25.25H49.8781C51.1615 25.25 52.3115 25.4583 53.3281 25.875C54.3615 26.2917 55.1781 26.9417 55.7781 27.825C56.3781 28.6917 56.6781 29.8333 56.6781 31.25C56.6781 32.6333 56.3781 33.7583 55.7781 34.625C55.1948 35.4917 54.3865 36.1333 53.3531 36.55C52.3365 36.95 51.1781 37.15 49.8781 37.15ZM46.5031 42H42.9281V25.25H46.5031V42ZM62.0549 42H58.4799V25.25H63.2049L65.9299 31.475L67.5049 36.45L69.0799 31.475L71.8049 25.25H76.5299V42H72.9549V35.725L73.4549 28.975L71.4549 34.55L69.3799 39.1H65.6299L63.5549 34.55L61.5299 28.975L62.0549 35.725V42Z" fill="#4D7D0F"/>
</g>
<defs>
<linearGradient id="paint0_linear_601_57" x1="50.5" y1="-12" x2="244" y2="144.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#EBFFC7"/>
<stop offset="1" stop-color="#D8FA9C"/>
</linearGradient>
<radialGradient id="paint1_radial_601_57" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(184.5 80.5) rotate(90) scale(66.4062)">
<stop offset="0.208333" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint2_linear_601_57" x1="182.903" y1="5.86786" x2="182.903" y2="68.3679" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-opacity="0.4"/>
</linearGradient>
<clipPath id="clip0_601_57">
<rect width="275" height="125" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,30 +0,0 @@
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_ddd_936_155)">
<circle cx="44" cy="44" r="10" fill="#4F9DFF"/>
<circle cx="44" cy="44" r="8.5" stroke="#FAFAFA" stroke-width="3"/>
</g>
<defs>
<filter id="filter0_ddd_936_155" x="0" y="0" width="88" height="88" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="12"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.309804 0 0 0 0 0.615686 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_936_155"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="7"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.309804 0 0 0 0 0.615686 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_936_155" result="effect2_dropShadow_936_155"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="17"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.309804 0 0 0 0 0.615686 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_936_155" result="effect3_dropShadow_936_155"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_936_155" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,58 +0,0 @@
Fontshare EULA
---—---------------------------------—------------------------------
Free Font - End User License Agreement (FF EULA)
---—---------------------------------—------------------------------
Notice to User
Indian Type Foundry designs, produces and distributes font software as digital fonts to end users worldwide. In addition to commercial fonts that are available for a fee, ITF also offers several fonts which can be used free of charge. The free fonts are distributed through a dedicated platform called www.fontshare.com (“Fontshare”) to end users worldwide. These free fonts are subject to this legally binding EULA between the Indian Type Foundry (“Indian Type Foundry” or “Licensor”) and you (“Licensee”). 
You acknowledge that the Font Software and designs embodied therein are protected by the copyright, other intellectual property rights and industrial property rights and by international treaties. They are and remain at all times the intellectual property of the Indian Type Foundry.
In addition to direct download, Fontshare also offers these free fonts via Fonthsare API using a code. In this case, the Font Software is delivered directly from the servers used by Indian Type Foundry to the Licensee's website, without the Licensee having to download the Font Software.
By downloading, accessing the API, installing, storing, copying or using one of any Font Software, you agree to the following terms. 
Definitions
“Font Software” refers to the set of computer files or programs released under this license that instructs your computer to display and/or print each letters, characters, typographic designs, ornament and so forth. Font Software includes all bitmap and vector representations of fonts and typographic representations and embellishments created by or derived from the Font Software. 
“Original Version” refers to the Font Software as distributed by the Indian Type Foundry as the copyright holder. 
“Derivative Work” refers to the pictorial representation of the font created by the Font Software, including typographic characters such as letters, numerals, ornaments, symbols, or punctuation and special characters.
01. Grant of License
You are hereby granted a non-exclusive, non-assignable, non-transferrable, terminable license to access, download and use the Font Software for your personal or commercial use for an unlimited period of time for free of charge. 
You may use the font Software in any media (including Print, Web, Mobile, Digital, Apps, ePub, Broadcasting and OEM) at any scale, at any location worldwide. 
You may use the Font Software to create logos and other graphic elements, images on any surface, vector files or other scalable drawings and static images. 
You may use the Font Software on any number of devices (computer, tablet, phone). The number of output devices (Printers) is not restricted. 
You may make only such reasonable number of back-up copies suitable to your permitted use. 
You may but are not required to identify Indian Type Foundry Fonts in your work credits. 
02. Limitations of usage
You may not modify, edit, adapt, translate, reverse engineer, decompile or disassemble, alter or otherwise copy the Font Software or the designs embodied therein in whole or in part, without the prior written consent of the Licensor. 
The Fonts may not - beyond the permitted copies and the uses defined herein - be distributed, duplicated, loaned, resold or licensed in any way, whether by lending, donating or give otherwise to a person or entity. This includes the distribution of the Fonts by e-mail, on USB sticks, CD-ROMs, or other media, uploading them in a public server or making the fonts available on peer-to-peer networks. A passing on to external designers or service providers (design agencies, repro studios, printers, etc.) is also not permitted. 
You are not allowed to transmit the Font Software over the Internet in font serving or for font replacement by means of technologies such as but not limited to EOT, Cufon, sIFR or similar technologies that may be developed in the future without the prior written consent of the Licensor. 
03. Embedding
You may embed the Font Software in PDF and other digital documents provided that is done in a secured, read-only mode. It must be ensured beyond doubt that the recipient cannot use the Font Software to edit or to create new documents. The design data (PDFs) created in this way and under these created design data (PDFs) may be distributed in any number. 
The extraction of the Font Software in whole or in part is prohibited. 
04. Third party use, Commercial print service provider
You may include the Font Software in a non-editable electronic document solely for printing and display purposes and provide that electronic document to the commercial print service provider for the purpose of printing. If the print service needs to install the fonts, they too need to download the Font Software from the Licensor's website.
05. Derivative Work
You are allowed to make derivative works as far as you use them for your personal or commercial use. However, you cannot modify, make changes or reverse engineer the original font software provided to you. Any derivative works are the exclusive property of the Licensor and shall be subject to the terms and conditions of this EULA. Derivative works may not be sub-licensed, sold, leased, rented, loaned, or given away without the express written permission of the Licensor. 
06. Warranty and Liability
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, INDIAN TYPE FOUNDRY MAKES NO WARRANTIES, EXPRESS OR IMPLIED AS TO THE MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR OTHERWISE. THE FONT SOFTWARE WAS NOT MANUFACTURED FOR USE IN MANUFACTURING CONTROL DEVICES OR NAVIGATION DEVICES OR IN CIRCUMSTANCES THAT COULD RESULT IN ENVIRONMENTAL DAMAGE OR PERSONAL INJURY. WITHOUT LIMITING THE FOREGOING, INDIAN TYPE FOUNDRY SHALL IN NO EVENT BE LIABLE TO THE LICENSED USER OR ANY OTHER THIRD PARTY FOR ANY DIRECT, CONSEQUENTIAL OR INCIDENTAL DAMAGES, INCLUDING DAMAGES FROM LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION NOR FOR LOST PROFITS OR SAVINGS ARISING OUT OF THE USE OR INABILITY TO USE THE PRODUCT EVEN IF NOTIFIED IN ADVANCE, UNDER NO CIRCUMSTANCES SHALL INDIAN TYPE FOUNDRYS LIABILITY EXCEED THE REPLACEMENT COST OF THE SOFTWARE. 
IF LICENSEE CHOOSES TO ACCESS THE FONT SOFTWARE THROUGH A CODE (API), IT MAY HAVE A DIRECT IMPACT ON LICENSEE'S WEBSITE OR APPLICATIONS. INDIAN TYPE FOUNDRY IS NOT RESPONSIBLE OR LIABLE FOR ANY INTERRUPTION, MALFUNCTION, DOWNTIME OR OTHER FAILURE OF THE WEBSITE OR ITS API.
07. Updates, Maintenance and Support Services
Licensor will not provide you with any support services for the Software under this Agreement.
08. Termination 
Any breach of the terms of this agreement shall be a cause for termination, provided that such breach is notified in writing to the Licensee by the Licensor and the Licensee failed to rectify the breach within 30 days of the receipt of such notification. 
In the event of termination and without limitation of any remedies under law or equity, you must delete the Font Software and all copies thereof. Proof of this must be provided upon request of the Licensor.  
We reserve the right to claim damages for the violation of the conditions. 
09. Final Provisions
If individual provisions of this agreement are or become invalid, the validity of the remaining provisions shall remain unaffected. Invalid provisions shall be replaced by mutual agreement by such provisions that are suitable to achieve the desired economic purpose, taking into account the interests of both parties. The same shall apply mutatis mutandis to the filling of any gaps which may arise in this agreement.
This contract is subject to laws of the Republic of India. Place of performance and exclusive place of jurisdiction for all disputes between the parties arising out of or in connection with this contract is, as far as legally permissible, Ahmedabad, India.
- 
Last Updated on 22 March 2021
Copyright 2021 Indian Type Foundry. All rights reserved. 

View File

@@ -1,96 +0,0 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font
Name 'Source'. Source is a trademark of Adobe in the United States
and/or other countries.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,7 +0,0 @@
<svg width="71" height="71" viewBox="0 0 71 71" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.1133 39.2642L42.3935 49.9446C41.8086 50.5274 41.5161 50.8188 41.1788 50.928C40.8822 51.024 40.5626 51.024 40.2659 50.928C39.9287 50.8188 39.6362 50.5274 39.0512 49.9446L30.2776 41.2032L21.5041 49.9446C20.9191 50.5274 20.6266 50.8188 20.2893 50.928C19.9927 51.024 19.6731 51.024 19.3764 50.928C19.0392 50.8188 18.7467 50.5274 18.1617 49.9446L16.1772 47.9673L30.2776 33.9187L39.2299 42.8381C40.0952 43.7002 41.498 43.7002 42.3633 42.8381L53.1133 32.1276V39.2642Z" fill="#EA5252"/>
<path d="M57.5446 34.8491L59.9407 32.4618C60.5256 31.879 60.8181 31.5876 60.9277 31.2516C61.0241 30.956 61.0241 30.6376 60.9277 30.342C60.8181 30.006 60.5256 29.7146 59.9407 29.1318L57.5439 26.7438C57.5444 26.7619 57.5446 26.78 57.5446 26.7982L57.5446 34.8491Z" fill="#EA5252"/>
<path d="M55.3835 24.5913C55.3653 24.5908 55.3472 24.5906 55.3289 24.5906L46.9514 24.5906L49.4959 22.0554C50.0809 21.4726 50.3734 21.1812 50.7107 21.072C51.0073 20.976 51.3269 20.976 51.6236 21.072C51.9608 21.1812 52.2533 21.4726 52.8383 22.0554L55.3835 24.5913Z" fill="#EA5252"/>
<path d="M42.5201 29.0057L40.7224 30.7968L31.9488 22.0554C31.3638 21.4726 31.0714 21.1812 30.7341 21.072C30.4374 20.976 30.1179 20.976 29.8212 21.072C29.4839 21.1812 29.1914 21.4726 28.6065 22.0554L11.0593 39.5382C10.4744 40.121 10.1819 40.4124 10.0723 40.7484C9.9759 41.044 9.9759 41.3624 10.0723 41.658C10.1819 41.994 10.4744 42.2854 11.0593 42.8682L13.0438 44.8454L28.7109 29.2358C29.5762 28.3737 30.9791 28.3737 31.8443 29.2358L40.7966 38.1552L49.9799 29.0057H42.5201Z" fill="#EA5252"/>
<circle cx="35.5" cy="35.5" r="29.5" stroke="#EA5252" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M50 160V200H0V0H120V20H85V50H50V108L120 95V115H180V80H200V200H150V142L50 160ZM135 100V65H100V35H135V0H165V35H200V65H165V100H135Z" fill="#D8FA9C"/>
</svg>

Before

Width:  |  Height:  |  Size: 303 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M50 160V200H0V0H160L178 18H95V50H50V108L150 90V78H200V200H150V142L50 160ZM200 33V63H110V33H200Z" fill="#D8FA9C"/>
</svg>

Before

Width:  |  Height:  |  Size: 270 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M50 160V200H0V0H160L200 40V200H150V142L50 160ZM150 50H50V108L150 90V50Z" fill="#D8FA9C"/>
</svg>

Before

Width:  |  Height:  |  Size: 246 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 160V115L185 100L200 85V80H180V115H120V80H85V75H50V50H85V20H120V0H0V200H160L200 160ZM49.9998 125H150V150H49.9998V125ZM135 100V65H99.9998V35H135V1.03335e-05H165V35H200V65H165V100H135Z" fill="#B9E6FD"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 160V115L185 100L200 85V80H95V75H50V50H95V20H180L160 0H0V200H160L200 160ZM50 125H150V150H50V125ZM110 35H200V65H110V35Z" fill="#B9E6FD"/>
</svg>

Before

Width:  |  Height:  |  Size: 297 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 160L160 200H0V0H160L200 40V85L185 100L200 115V160ZM50 50H150V75H50V50ZM50 125H150V150H50V125Z" fill="#B9E6FD"/>
</svg>

Before

Width:  |  Height:  |  Size: 273 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 200V150H50V50H85V20H120V0H40L0 40V160L40 200H200ZM135 65V100H165V65H200V35H165V0H135V35H100V65H135ZM200 0V20H180V0H200Z" fill="#DDD6FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 299 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 200H40L0 160V40L40 0H200V20H95V50H50V150H200V200ZM200 35V65H110V35H200Z" fill="#DDD6FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 251 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M40 200H200V150H50V50H200V0H40L0 40V160L40 200Z" fill="#DDD6FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 182 B

View File

@@ -1,3 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 80V160L160 200H0V0H120V20H85V50H50V150H150V115H180V80H200ZM135 100V65H100V35H135V0H165V35H200V65H165V100H135Z" fill="#F5CFFE"/>
</svg>

Before

Width:  |  Height:  |  Size: 289 B

Some files were not shown because too many files have changed in this diff Show More