Merge branch 'Genshin-bots:master' into master

This commit is contained in:
LiShenshun 2023-07-11 09:54:05 +08:00 committed by GitHub
commit baf988cfe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1570 additions and 490 deletions

1
.gitignore vendored
View File

@ -666,6 +666,7 @@ config.json
res_data
GsData.db
data
core_help.jpg
gsuid_core/plugins/*
!gsuid_core/plugins/core_command
!gsuid_core/plugins/gs_test.py

View File

@ -23,7 +23,7 @@ repos:
- id: flake8
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.3
rev: v2.1.5
hooks:
- id: pycln
@ -34,7 +34,7 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/python-poetry/poetry
rev: 1.4.0
rev: 1.5.0
hooks:
- id: poetry-check
- id: poetry-lock

View File

@ -7,14 +7,14 @@
[KimigaiiWuyi/GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) 的核心部分,平台无关,支持 HTTP/WS 形式调用,便于移植到其他平台以及框架。
目前仍在开发中。
**🎉[详细文档](https://docs.gsuid.gbots.work/#/)**
## 安装Core
1. git clone gsuid-core本体
```shell
git clone https://ghproxy.com/https://github.com/Genshin-bots/gsuid-core.git --depth=1 --single-branch
git clone https://ghproxy.com/https://github.com/Genshin-bots/gsuid_core.git --depth=1 --single-branch
```
2. 安装poetry
@ -26,28 +26,34 @@ pip install poetry
3. 安装所需依赖
```shell
# cd进入clone好的文件夹内
cd gsuid_core
# 安装依赖
poetry install
```
4. 安装所需插件
4. 安装所需插件(可选)
```shell
# 安装v4 GenshinUID
# cd进入插件文件夹内
cd plugins
# 安装v4 GenshinUID
git clone -b v4 https://ghproxy.com/https://github.com/KimigaiiWuyi/GenshinUID.git --depth=1 --single-branch
```
5. 启动gsuid-core
5. 启动gsuid_core早柚核心
```shell
# 在gsuid_core/genshin_core文件夹内
poetry run python core.py
# 或者(二选一即可)
poetry run core
```
6. 链接其他适配端
+ 默认core将运行在`localhost:8765`端口上,如有需要可至`config.json`修改。
+ 在支持的Bot上例如NoneBot2、HoshinoBot安装相应适配插件启动Bot如果有修改端口则需要在启动Bot前修改适配插件相应端口即可自动连接Core端。
+ 在支持的Bot上例如NoneBot2、HoshinoBot、ZeroBot、YunZaiBot等安装相应适配插件启动Bot如果有修改端口则需要在启动Bot前修改适配插件相应端口即可自动连接Core端。
## Docker部署Core可选
@ -56,7 +62,15 @@ poetry run python core.py
1. git clone gsuid-core本体
```shell
git clone https://ghproxy.com/https://github.com/Genshin-bots/gsuid-core.git --depth=1 --single-branch
git clone https://ghproxy.com/https://github.com/Genshin-bots/gsuid_core.git --depth=1 --single-branch
```
2. 安装所需插件
```shell
# 安装v4 GenshinUID
cd plugins
git clone -b v4 https://ghproxy.com/https://github.com/KimigaiiWuyi/GenshinUID.git --depth=1 --single-branch
```
2. 安装所需插件
@ -77,6 +91,42 @@ docker-compose up -d
- 默认core将运行在`localhost:8765`端口上Docker部署必须修改`config.json`,如`0.0.0.0:8765`
- 如果Bot例如NoneBot2、HoshinoBot也是Docker部署的Core或其插件更新后可能需要将Core和Bot的容器都重启才生效
## 配置文件
修改`gsuid_core/gsuid_core/config.json`,参考如下
**注意json不支持`#`,所以不要复制下面的配置到自己的文件中)**
```json
{
"HOST": "localhost", # 如需挂载公网修改为`0.0.0.0`
"PORT": "8765", # core端口
"masters": ["444835641", "111"], # Bot主人pm为0
"superusers": ["123456789"], # 超管pm为1
"sv": {
"Core管理": {
"priority": 5, # 某个服务的优先级
"enabled": true, # 某个服务是否启动
"pm": 1, # 某个服务要求的权限等级
"black_list": [], # 某个服务的黑名单
"area": "ALL", # 某个服务的触发范围
"white_list": [] # 某个服务的白名单
},
},
"log": {
"level": "DEBUG" # log等级
},
"command_start": ["/", "*"], # core内所有插件的要求前缀
"misfire_grace_time": 90
}
```
> 黑名单一旦设置黑名单中的用户ID将无法访问该服务
>
> 白名单一旦设置只有白名单的用户ID能访问该服务
>
> 服务配置可以通过[网页控制台](https://docs.gsuid.gbots.work/#/WebConsole)实时修改, 如果手动修改`config.json`需要**重启**
## 编写插件
@ -101,7 +151,9 @@ sv=SV(
pm=2, # 权限 0为master1为superuser2为群的群主&管理员3为普通
priority=5, # 整组服务的优先级
enabled=True, # 是否启用
black_list=[] # 黑名单
area= 'ALL', # 群聊和私聊均可触发
black_list=[], # 黑名单
white_list=[], # 白名单
)
@sv.on_prefix('测试')

View File

@ -1,3 +1,4 @@
import random
import asyncio
from typing import List, Union, Literal, Optional
@ -7,7 +8,14 @@ from msgspec import json as msgjson
from gsuid_core.logger import logger
from gsuid_core.gs_logger import GsLogger
from gsuid_core.segment import MessageSegment
from gsuid_core.utils.image.convert import text2pic
from gsuid_core.models import Event, Message, MessageSend
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
R_enabled = core_plugins_config.get_config('AutoAddRandomText').data
R_text = core_plugins_config.get_config('RandomText').data
is_text2pic = core_plugins_config.get_config('AutoTextToPic').data
text2pic_limit = core_plugins_config.get_config('TextToPicThreshold').data
class _Bot:
@ -39,13 +47,33 @@ class _Bot:
elif isinstance(message, bytes):
message = [MessageSegment.image(message)]
elif isinstance(message, List):
message = [MessageSegment.node(message)]
if all(isinstance(x, str) for x in message):
message = [MessageSegment.node(message)]
_message: List[Message] = message # type: ignore
if at_sender and sender_id:
message.append(MessageSegment.at(sender_id))
_message.append(MessageSegment.at(sender_id))
if R_enabled:
result = ''.join(
random.choice(R_text)
for _ in range(random.randint(1, len(R_text)))
)
_message.append(MessageSegment.text(result))
if is_text2pic:
if (
len(_message) == 1
and _message[0].type == 'text'
and isinstance(_message[0].data, str)
and len(_message[0].data) >= int(text2pic_limit)
):
img = await text2pic(_message[0].data)
_message = [MessageSegment.image(img)]
send = MessageSend(
content=message,
content=_message,
bot_id=bot_id,
bot_self_id=bot_self_id,
target_type=target_type,

View File

@ -1,16 +1,19 @@
from pathlib import Path
from typing import Optional
from typing import List, Union, Optional
gs_data_path = Path(__file__).parents[1] / 'data'
def get_res_path(_path: Optional[str] = None) -> Path:
def get_res_path(_path: Optional[Union[str, List]] = None) -> Path:
if _path:
path = gs_data_path / _path
if isinstance(_path, str):
path = gs_data_path / _path
else:
path = gs_data_path.joinpath(*_path)
else:
path = gs_data_path
if not path.exists():
path.mkdir()
path.mkdir(parents=True)
return path

View File

@ -1,13 +1,16 @@
import asyncio
import inspect
from gsuid_core.aps import scheduler
from gsuid_core.logger import logger
from gsuid_core.server import GsServer
from gsuid_core.help.draw_help import get_help_img
gss = GsServer()
if not gss.is_load:
gss.is_load = True
gss.load_plugins()
asyncio.run(get_help_img())
repeat_jobs = {}
for i in scheduler.get_jobs():

View File

@ -20,7 +20,7 @@ async def get_user_pml(msg: MessageReceive) -> int:
elif msg.user_id in config_superusers:
return 1
else:
return msg.user_pm
return msg.user_pm if msg.user_pm >= 1 else 2
async def msg_process(msg: MessageReceive) -> Event:

View File

@ -0,0 +1,197 @@
from pathlib import Path
from typing import Dict, List, Tuple, Union, Optional
from PIL import Image, ImageDraw
from gsuid_core.sv import SV
# from gsuid_core.utils.image.image_tools import get_color_bg
from gsuid_core.utils.fonts.fonts import core_font
TEXT_PATH = Path(__file__).parent / 'texture2d'
CORE_HELP_IMG = Path(__file__).parent / 'core_help.jpg'
plugin_title = 92
sv_title = 67
tag_color = {
'prefix': (137, 228, 124),
'suffix': (124, 180, 228),
'file': (190, 228, 217),
'keyword': (217, 228, 254),
'fullmatch': (228, 124, 124),
'regex': (225, 228, 124),
'command': (228, 124, 124),
'other': (228, 190, 191),
}
tag_text: Dict[str, str] = {
'prefix': '前缀',
'suffix': '后缀',
'file': '文件',
'keyword': '包含',
'fullmatch': '完全',
'regex': '正则',
'command': '命令',
'other': '其他',
}
tags: Dict[str, Optional[Image.Image]] = {
'prefix': None,
'suffix': None,
'file': None,
'keyword': None,
'fullmatch': None,
'regex': None,
'command': None,
'other': None,
}
def get_tag(tag_type: str) -> Image.Image:
cache = tags[tag_type]
if cache is not None:
return cache
text = tag_text[tag_type]
tag = Image.new('RGBA', (60, 40))
tag_draw = ImageDraw.Draw(tag)
tag_draw.rounded_rectangle((7, 5, 53, 35), 10, tag_color[tag_type])
tag_draw.text((30, 20), text, (62, 62, 62), core_font(22), 'mm')
tags[tag_type] = tag
return tag
def get_command_bg(command: str, tag_type: str):
img = Image.new('RGBA', (220, 40))
img_draw = ImageDraw.Draw(img)
img_draw.rounded_rectangle((6, 5, 160, 35), 10, (230, 202, 167))
img_draw.text((83, 20), command, (62, 62, 62), core_font(20), 'mm')
tag = get_tag(tag_type)
img.paste(tag, (160, 0), tag)
return img
def _c(data: Union[int, str, bool]) -> Tuple[int, int, int]:
gray_color = (184, 184, 184)
if isinstance(data, bool):
color = tag_color['prefix'] if data else gray_color
elif isinstance(data, str):
color = (
tag_color['prefix']
if data == 'ALL'
else tag_color['command']
if data == 'GROUP'
else tag_color['file']
)
else:
colors = list(tag_color.values())
if data <= len(colors) and data >= 0:
color = colors[data]
else:
color = tag_color['other']
return color
def _t(data: Union[int, str, bool]) -> str:
if isinstance(data, bool):
text = '开启' if data else '关闭'
elif isinstance(data, str):
text = '不限' if data == 'ALL' else '群聊' if data == 'GROUP' else '私聊'
else:
texts = ['主人', '超管', '群主', '管理', '频管', '子管', '正常', '', '']
if data <= len(texts) and data >= 0:
text = ['主人', '超管', '群主', '管理', '频管', '子管', '正常', '', ''][data]
else:
text = '最低'
return text
def get_plugin_bg(plugin_name: str, sv_list: List[SV]):
img_list: List[Image.Image] = []
for sv in sv_list:
sv_img = Image.new(
'RGBA',
(
900,
sv_title + ((len(sv.TL) + 3) // 4) * 40,
),
)
sv_img_draw = ImageDraw.Draw(sv_img)
for index, trigger_name in enumerate(sv.TL):
tg_img = get_command_bg(trigger_name, sv.TL[trigger_name].type)
sv_img.paste(
tg_img, (6 + 220 * (index % 4), 67 + 40 * (index // 4)), tg_img
)
sv_img_draw.rounded_rectangle((15, 19, 25, 50), 10, (62, 62, 62))
sv_img_draw.text((45, 31), sv.name, (62, 62, 62), core_font(36), 'lm')
sv_img_draw.rounded_rectangle((710, 15, 760, 50), 10, _c(sv.enabled))
sv_img_draw.rounded_rectangle((770, 15, 820, 50), 10, _c(sv.pm))
sv_img_draw.rounded_rectangle((830, 15, 880, 50), 10, _c(sv.area))
sv_img_draw.text(
(735, 32), _t(sv.enabled), (62, 62, 62), core_font(22), 'mm'
)
sv_img_draw.text(
(795, 32), _t(sv.pm), (62, 62, 62), core_font(22), 'mm'
)
sv_img_draw.text(
(855, 32), _t(sv.area), (62, 62, 62), core_font(22), 'mm'
)
img_list.append(sv_img)
img = Image.new(
'RGBA',
(
900,
plugin_title + sum([i.size[1] for i in img_list]),
),
)
img_draw = ImageDraw.Draw(img)
img_draw.rounded_rectangle((10, 26, 890, 76), 10, (230, 202, 167))
img_draw.text((450, 51), plugin_name, (62, 62, 62), core_font(42), 'mm')
temp = 0
for _img in img_list:
img.paste(_img, (0, 92 + temp), _img)
temp += _img.size[1]
return img
async def get_help_img() -> Image.Image:
from gsuid_core.sv import SL
content = SL.detail_lst
img_list: List[Image.Image] = []
for plugin_name in content:
plugin_img = get_plugin_bg(plugin_name, content[plugin_name])
img_list.append(plugin_img)
x = 900
y = 200 + sum([i.size[1] for i in img_list])
# img = await get_color_bg(x, y)
img = Image.new('RGBA', (x, y), (255, 255, 255))
title = Image.open(TEXT_PATH / 'title.png')
# white = Image.new('RGBA', img.size, (255, 255, 255, 120))
# img.paste(white, (0, 0), white)
img.paste(title, (0, 50), title)
temp = 0
for _img in img_list:
img.paste(_img, (0, 340 + temp), _img)
temp += _img.size[1]
img = img.convert('RGB')
img.save(
CORE_HELP_IMG,
format='JPEG',
quality=80,
subsampling=0,
)
return img

View File

@ -0,0 +1,107 @@
from copy import deepcopy
from typing import Dict, List, Tuple, Callable, Optional
from PIL import Image, ImageDraw, ImageFont
from gsuid_core.data_store import get_res_path
from gsuid_core.utils.image.convert import convert_img
from gsuid_core.utils.image.image_tools import (
crop_center_img,
easy_alpha_composite,
)
from .model import PluginHelp
cache: Dict[str, int] = {}
async def get_help(
name: str,
sub_text: str,
help_data: Dict[str, PluginHelp],
bg: Image.Image,
icon: Image.Image,
badge: Image.Image,
banner: Image.Image,
button: Image.Image,
font: Callable[[int], ImageFont.FreeTypeFont],
is_dark: bool = True,
text_color: Tuple[int, int, int] = (250, 250, 250),
sub_color: Optional[Tuple[int, int, int]] = None,
) -> bytes:
help_path = get_res_path('help') / f'{name}.jpg'
if help_path.exists() and name in cache and cache[name]:
return await convert_img(Image.open(help_path))
if sub_color is None and is_dark:
sub_color = tuple(x - 50 for x in text_color if x > 50)
elif sub_color is None and not is_dark:
sub_color = tuple(x + 50 for x in text_color if x < 205)
title = Image.new('RGBA', (900, 600))
icon = icon.resize((300, 300))
title.paste(icon, (300, 89), icon)
title.paste(badge, (0, 390), badge)
badge_s = badge.resize((720, 80))
title.paste(badge_s, (90, 480), badge_s)
title_draw = ImageDraw.Draw(title)
title_draw.text((450, 440), f'{name} 帮助', text_color, font(36), 'mm')
title_draw.text((450, 520), sub_text, sub_color, font(26), 'mm')
w, h = 900, 630
sv_img_list: List[Image.Image] = []
for sv_name in help_data:
tr_size = len(help_data[sv_name]['data'])
y = 100 + ((tr_size + 3) // 4) * 80
h += y
sv_img = Image.new('RGBA', (900, y))
sv_data = help_data[sv_name]['data']
sv_desc = help_data[sv_name]['desc']
bc = deepcopy(banner)
bc_draw = ImageDraw.Draw(bc)
bc_draw.text((30, 25), sv_name, text_color, font(35), 'lm')
size, _ = font(35).getsize(sv_name)
bc_draw.text((42 + size, 30), sv_desc, sub_color, font(20), 'lm')
sv_img = easy_alpha_composite(sv_img, bc, (0, 10))
# sv_img.paste(bc, (0, 10), bc)
for index, tr in enumerate(sv_data):
bt = deepcopy(button)
bt_draw = ImageDraw.Draw(bt)
if len(tr['name']) > 8:
tr_name = tr['name'][:5] + '..'
else:
tr_name = tr['name']
bt_draw.text((105, 28), tr_name, text_color, font(26), 'mm')
bt_draw.text((105, 51), tr['eg'], sub_color, font(17), 'mm')
offset_x = 210 * (index % 4)
offset_y = 80 * (index // 4)
sv_img = easy_alpha_composite(
sv_img, bt, (26 + offset_x, 83 + offset_y)
)
# sv_img.paste(bt, (26 + offset_x, 83 + offset_y), bt)
sv_img_list.append(sv_img)
img = crop_center_img(bg, w, h)
img.paste(title, (0, 0), title)
temp = 0
for _sm in sv_img_list:
img.paste(_sm, (0, 600 + temp), _sm)
temp += _sm.size[1]
img = img.convert('RGB')
help_path = get_res_path('help') / f'{name}.jpg'
img.save(
help_path,
'JPEG',
quality=85,
subsampling=0,
)
cache[name] = 1
return await convert_img(img)

15
gsuid_core/help/model.py Normal file
View File

@ -0,0 +1,15 @@
from typing import List, TypedDict
class PluginSV(TypedDict):
name: str
desc: str
eg: str
need_ck: bool
need_sk: bool
need_admin: bool
class PluginHelp(TypedDict):
desc: str
data: List[PluginSV]

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,8 +1,9 @@
from gsuid_core.aps import scheduler
from gsuid_core.logger import logger
from gsuid_core.utils.plugins_update._plugins import update_all_plugins
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
from .auto_task import update_core, restart_core, update_all_plugins
from .auto_task import update_core, restart_core
config = core_plugins_config

View File

@ -1,6 +1,5 @@
from typing import List
from gsuid_core.utils.plugins_update.api import PLUGINS_PATH
from gsuid_core.utils.plugins_update._plugins import update_from_git
from gsuid_core.plugins.core_command.core_restart.restart import (
restart_genshinuid,
@ -11,12 +10,5 @@ async def update_core() -> List[str]:
return update_from_git()
async def update_all_plugins() -> List[str]:
log_list = []
for plugin in PLUGINS_PATH.iterdir():
log_list.extend(update_from_git(0, plugin))
return log_list
async def restart_core():
await restart_genshinuid('', '', '', False)

View File

@ -0,0 +1,20 @@
from gsuid_core.sv import SV
from gsuid_core.bot import Bot
from gsuid_core.models import Event
from gsuid_core.logger import logger
from gsuid_core.utils.image.convert import convert_img
from gsuid_core.help.draw_help import CORE_HELP_IMG, get_help_img
sv_core_help_img = SV('Core帮助')
@sv_core_help_img.on_fullmatch(('core帮助', 'Core帮助'))
async def send_core_htlp_msg(bot: Bot, ev: Event):
logger.info('[早柚核心] 开始执行[帮助图]')
if CORE_HELP_IMG.exists():
img = await convert_img(CORE_HELP_IMG)
else:
img = await get_help_img()
img = await convert_img(img)
logger.info('[早柚核心] 帮助图获取成功!')
await bot.send(img)

View File

@ -52,7 +52,7 @@ async def send_restart_msg(bot: Bot, ev: Event):
else:
send_id = ev.user_id
send_type = 'direct'
await bot.send('正在执行[gs重启]...')
await bot.send('正在执行[core重启]...')
await restart_genshinuid(bot.bot_id, send_type, str(send_id))

View File

@ -7,7 +7,7 @@ from pathlib import Path
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
bot_start = Path(__file__).parents[4] / 'core.py'
bot_start = Path(__file__).parents[3] / 'core.py'
restart_sh_path = Path().cwd() / 'gs_restart.sh'
update_log_path = Path(__file__).parent / 'update_log.json'

View File

@ -0,0 +1,25 @@
from gsuid_core.sv import SV
from gsuid_core.bot import Bot
from gsuid_core.models import Event
from gsuid_core.logger import logger
from gsuid_core.utils.plugins_update._plugins import (
update_from_git,
update_all_plugins,
)
sv_core_config = SV('Core管理', pm=0)
@sv_core_config.on_fullmatch(('core更新'))
async def send_core_update_msg(bot: Bot, ev: Event):
logger.info('开始执行[更新] 早柚核心')
log_list = update_from_git()
await bot.send(log_list)
@sv_core_config.on_fullmatch(('core全部更新'))
async def send_core_all_update_msg(bot: Bot, ev: Event):
logger.info('开始执行[更新] 全部更新')
log_list = update_from_git()
log_list.extend(await update_all_plugins())
await bot.send(log_list)

View File

@ -0,0 +1,15 @@
from gsuid_core.sv import SV
from gsuid_core.bot import Bot
from gsuid_core.models import Event
from .draw_user_card import get_user_card
core_user_info = SV('core用户信息')
@core_user_info.on_fullmatch(('绑定信息'))
async def send_bind_card(bot: Bot, ev: Event):
await bot.logger.info('开始执行[查询用户绑定状态]')
im = await get_user_card(ev.bot_id, ev.user_id)
await bot.logger.info('[查询用户绑定状态]完成!等待图片发送中...')
await bot.send(im)

View File

@ -0,0 +1,133 @@
from pathlib import Path
from typing import List, Tuple, Union, Optional
from PIL import Image, ImageDraw
from gsuid_core.utils.database.api import DBSqla
from gsuid_core.utils.fonts.fonts import core_font
from gsuid_core.utils.database.models import GsPush
from gsuid_core.utils.image.convert import convert_img
from gsuid_core.utils.image.image_tools import (
get_color_bg,
get_qq_avatar,
draw_pic_with_ring,
easy_alpha_composite,
)
TEXT_PATH = Path(__file__).parent / 'texture2d'
status_off = Image.open(TEXT_PATH / 'status_off.png')
status_on = Image.open(TEXT_PATH / 'status_on.png')
EN_MAP = {'coin': '宝钱', 'resin': '体力', 'go': '派遣', 'transform': '质变仪'}
async def get_user_card(bot_id: str, user_id: str) -> Union[bytes, str]:
get_sqla = DBSqla().get_sqla
sqla = get_sqla(bot_id)
uid_list: List = await sqla.get_bind_uid_list(user_id)
sr_uid_list = await sqla.get_bind_sruid_list(user_id)
user_list = await sqla.select_user_all_data_by_user_id(user_id)
if user_list is None:
return '你还没有绑定过UID或者CK!'
w, h = 750, len(max(uid_list, sr_uid_list)) * 900 + 470
# 获取背景图片各项参数
_id = str(user_id)
if _id.startswith('http'):
char_pic = await get_qq_avatar(avatar_url=_id)
else:
char_pic = await get_qq_avatar(qid=_id)
char_pic = await draw_pic_with_ring(char_pic, 290)
img = await get_color_bg(w, h)
img_mask = Image.new('RGBA', img.size, (255, 255, 255))
title = Image.open(TEXT_PATH / 'user_title.png')
title.paste(char_pic, (241, 40), char_pic)
title_draw = ImageDraw.Draw(title)
title_draw.text(
(375, 444), f'{bot_id} - {user_id}', (29, 29, 29), core_font(30), 'mm'
)
img.paste(title, (0, 0), title)
for index, user_data in enumerate(user_list):
user_card = Image.open(TEXT_PATH / 'user_bg.png')
user_draw = ImageDraw.Draw(user_card)
if user_data.uid is not None and user_data.uid != '0':
uid_text = f'原神UID {user_data.uid}'
user_push_data = await sqla.select_push_data(user_data.uid)
else:
uid_text = '未发现原神UID'
user_push_data = GsPush(bot_id='TEMP')
user_draw.text(
(375, 58),
uid_text,
(29, 29, 29),
font=core_font(36),
anchor='mm',
)
if user_data.sr_uid:
sruid_text = f'星铁UID {user_data.sr_uid}'
else:
sruid_text = '未发现星铁UID'
user_draw.text(
(375, 119),
sruid_text,
(29, 29, 29),
font=core_font(36),
anchor='mm',
)
x, y = 331, 112
b = 175
paste_switch(user_card, user_data.cookie, (241, b))
paste_switch(user_card, user_data.stoken, (241 + x, b))
paste_switch(user_card, user_data.sign_switch, (241, b + y))
paste_switch(user_card, user_data.bbs_switch, (241 + x, b + y))
paste_switch(user_card, user_data.push_switch, (241, b + 2 * y))
paste_switch(user_card, user_data.status, (241 + x, b + 2 * y), True)
for _index, mode in enumerate(['coin', 'resin', 'go', 'transform']):
paste_switch(
user_card,
getattr(user_push_data, f'{mode}_push'),
(241 + _index % 2 * x, b + (_index // 2 + 3) * y),
)
if getattr(user_push_data, f'{mode}_push') != 'off':
user_draw.text(
(268 + _index % 2 * x, 168 + 47 + (_index // 2 + 3) * y),
f'{getattr(user_push_data, f"{mode}_value")}',
(35, 35, 35),
font=core_font(15),
anchor='lm',
)
sr_sign = user_data.sr_sign_switch
sr_push = user_data.sr_push_switch
paste_switch(user_card, sr_sign, (241, b + 5 * y))
paste_switch(user_card, sr_push, (241 + x, b + 5 * y))
img.paste(user_card, (0, 500 + index * 870), user_card)
img = easy_alpha_composite(img_mask, img, (0, 0))
return await convert_img(img)
def paste_switch(
card: Image.Image,
status: Optional[str],
pos: Tuple[int, int],
is_status: bool = False,
):
if is_status:
pic = status_off if status else status_on
else:
pic = status_on if status != 'off' and status else status_off
card.paste(pic, pos, pic)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -25,6 +25,8 @@ class MessageSegment:
with open(str(img), 'rb') as fp:
img = fp.read()
else:
if img.startswith('http'):
return Message(type='image', data=f'link://{img}')
if img.startswith('base64://'):
return Message(type='image', data=img)
with open(img, 'rb') as fp:
@ -53,6 +55,10 @@ class MessageSegment:
else:
if msg.startswith('base64://'):
msg_list.append(Message(type='image', data=msg))
elif msg.startswith('http'):
msg_list.append(
Message(type='image', data=f'link://{msg}')
)
else:
msg_list.append(MessageSegment.text(msg))
return Message(type='node', data=msg_list)
@ -79,8 +85,15 @@ class MessageSegment:
elif isinstance(content, bytes):
file = content
else:
with open(content, 'rb') as fp:
file = fp.read()
if content.startswith('http'):
link = content
return Message(
type='file',
data=f'{file_name}|link://{link}',
)
else:
with open(content, 'rb') as fp:
file = fp.read()
return Message(
type='file',
data=f'{file_name}|{b64encode(file).decode()}',

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import traceback
from pathlib import Path
from functools import wraps
from typing import Dict, List, Tuple, Union, Literal, Callable, Optional
@ -11,6 +13,7 @@ from gsuid_core.config import core_config
class SVList:
def __init__(self):
self.lst: Dict[str, SV] = {}
self.detail_lst: Dict[str, List[SV]] = {}
@property
def get_lst(self):
@ -58,6 +61,16 @@ class SV:
# sv内包含的触发器
self.TL: Dict[str, Trigger] = {}
self.is_initialized = True
stack = traceback.extract_stack()
file = stack[-2].filename
path = Path(file)
parts = path.parts
i = parts.index('plugins')
plugins_name = parts[i + 1]
if plugins_name not in SL.detail_lst:
SL.detail_lst[plugins_name] = [self]
else:
SL.detail_lst[plugins_name].append(self)
# 判断sv是否已持久化
if name in config_sv:

View File

@ -0,0 +1,65 @@
import copy
import json
from pathlib import Path
from openpyxl import load_workbook
sample = {
'name': '',
'desc': '',
'eg': '',
'need_ck': False,
'need_sk': False,
'need_admin': False,
}
result = {}
HELP_PATH = Path(__file__).parent / 'Help.xlsx'
OUTPUT_PATH = Path(__file__).parent / 'Help.json'
wb = load_workbook(str(HELP_PATH))
ws = wb.active
module_name_str = ''
for row in range(2, 999):
# 跳过空白行
if not ws.cell(row, 2).value:
continue
_sample = copy.deepcopy(sample)
# 将第一列读取为模块名
if ws.cell(row, 1):
if ws.cell(row, 1).value is not None:
module_name_str = ws.cell(row, 1).value
# if module_name_str is None and not isinstance(module_name_str, str):
# continue
# 第二列为功能名
_sample['name'] = ws.cell(row, 2).value
# 第三列为详细信息
_sample['desc'] = ws.cell(row, 3).value
# 第四列为使用例
_sample['eg'] = ws.cell(row, 4).value
if ws.cell(row, 5).value == '':
_sample['need_ck'] = True
if ws.cell(row, 6).value == '':
_sample['need_sk'] = True
if ws.cell(row, 7).value == '':
_sample['need_admin'] = True
if isinstance(module_name_str, str):
module_name = module_name_str.split(' | ')[0]
module_desc = module_name_str.split(' | ')[1]
if module_name not in result:
result[module_name] = {'desc': module_desc, 'data': []}
result[module_name]['data'].append(_sample)
with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)

View File

@ -134,4 +134,6 @@ CALENDAR_URL = f'{DRAW_BASE_URL}/calendar'
RECEIVE_URL = f'{DRAW_BASE_URL}/post_my_draw'
BS_INDEX_URL = f'{DRAW_BASE_URL}/index'
GET_FP_URL = 'https://public-data-api.mihoyo.com/device-fp/api/getFp'
_API = locals()

View File

@ -761,20 +761,24 @@ class RoleCalendar(TypedDict):
is_subscribe: bool
class RoleCalendarList(TypedDict):
calendar_role: List[RoleCalendar]
MonthlyRoleCalendar = TypedDict(
'MonthlyRoleCalendar',
{
'1': List[RoleCalendar],
'2': List[RoleCalendar],
'3': List[RoleCalendar],
'4': List[RoleCalendar],
'5': List[RoleCalendar],
'6': List[RoleCalendar],
'7': List[RoleCalendar],
'8': List[RoleCalendar],
'9': List[RoleCalendar],
'10': List[RoleCalendar],
'11': List[RoleCalendar],
'12': List[RoleCalendar],
'1': RoleCalendarList,
'2': RoleCalendarList,
'3': RoleCalendarList,
'4': RoleCalendarList,
'5': RoleCalendarList,
'6': RoleCalendarList,
'7': RoleCalendarList,
'8': RoleCalendarList,
'9': RoleCalendarList,
'10': RoleCalendarList,
'11': RoleCalendarList,
'12': RoleCalendarList,
},
)

View File

@ -9,11 +9,12 @@ import uuid
import random
from abc import abstractmethod
from string import digits, ascii_letters
from typing import Any, Dict, List, Union, Literal, Optional, cast
from typing import Any, Dict, List, Tuple, Union, Literal, Optional, cast
from aiohttp import TCPConnector, ClientSession, ContentTypeError
from gsuid_core.logger import logger
from gsuid_core.utils.database.api import DBSqla
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
from .api import _API
@ -87,13 +88,17 @@ class BaseMysApi:
MAPI = _API
is_sr = False
RECOGNIZE_SERVER = RECOGNIZE_SERVER
chs = {}
dbsqla: DBSqla = DBSqla()
@abstractmethod
async def _upass(self, header: Dict):
async def _upass(self, header: Dict) -> str:
...
@abstractmethod
async def _pass(self, gt: str, ch: str, header: Dict):
async def _pass(
self, gt: str, ch: str, header: Dict
) -> Tuple[Optional[str], Optional[str]]:
...
@abstractmethod
@ -106,6 +111,59 @@ class BaseMysApi:
async def get_stoken(self, uid: str) -> Optional[str]:
...
@abstractmethod
async def get_user_fp(self, uid: str) -> Optional[str]:
...
@abstractmethod
async def get_user_device_id(self, uid: str) -> Optional[str]:
...
def get_device_id(self) -> str:
device_id = str(uuid.uuid4()).upper()
return device_id
def generate_seed(self, length: int):
characters = '0123456789abcdef'
result = ''.join(random.choices(characters, k=length))
return result
async def generate_fp_by_uid(self, uid: str) -> str:
seed_id = self.generate_seed(16)
seed_time = str(int(time.time() * 1000))
ext_fields = f'{{"userAgent":"{self._HEADER["User-Agent"]}",\
"browserScreenSize":281520,"maxTouchPoints":5,\
"isTouchSupported":true,"browserLanguage":"zh-CN","browserPlat":"iPhone",\
"browserTimeZone":"Asia/Shanghai","webGlRender":"Apple GPU",\
"webGlVendor":"Apple Inc.",\
"numOfPlugins":0,"listOfPlugins":"unknown","screenRatio":3,"deviceMemory":"unknown",\
"hardwareConcurrency":"4","cpuClass":"unknown","ifNotTrack":"unknown","ifAdBlock":0,\
"hasLiedResolution":1,"hasLiedOs":0,"hasLiedBrowser":0}}'
body = {
'seed_id': seed_id,
'device_id': await self.get_user_device_id(uid),
'platform': '5',
'seed_time': seed_time,
'ext_fields': ext_fields,
'app_name': 'account_cn',
'device_fp': '38d7ee834d1e9',
}
HEADER = copy.deepcopy(self._HEADER)
res = await self._mys_request(
url=self.MAPI['GET_FP_URL'],
method='POST',
header=HEADER,
data=body,
)
if not isinstance(res, Dict):
logger.error(f"获取fp连接失败{res}")
return random_hex(13).lower()
elif res["data"]["code"] != 200:
logger.error(f"获取fp参数不正确{res['data']['msg']}")
return random_hex(13).lower()
else:
return res["data"]["device_fp"]
async def simple_mys_req(
self,
URL: str,
@ -196,6 +254,113 @@ class BaseMysApi:
async with ClientSession(
connector=TCPConnector(verify_ssl=ssl_verify)
) as client:
raw_data = {}
uid = None
if params and 'role_id' in params:
uid = params['role_id']
header['x-rpc-device_id'] = await self.get_user_device_id(uid)
header['x-rpc-device_fp'] = await self.get_user_fp(uid)
for _ in range(3):
if 'Cookie' in header and header['Cookie'] in self.chs:
# header['x-rpc-challenge']=self.chs.pop(header['Cookie'])
if self.is_sr:
header['x-rpc-challenge'] = self.chs.pop(
header['Cookie']
)
if isinstance(params, Dict):
header['DS'] = get_ds_token(
'&'.join(
[f'{k}={v}' for k, v in params.items()]
)
)
header['x-rpc-challenge_game'] = '6' if self.is_sr else '2'
header['x-rpc-page'] = (
'3.1.3_#/rpg' if self.is_sr else '3.1.3_#/ys'
)
if (
'x-rpc-challenge' in header
and not header['x-rpc-challenge']
):
del header['x-rpc-challenge']
del header['x-rpc-page']
del header['x-rpc-challenge_game']
print(header)
async with client.request(
method,
url=url,
headers=header,
params=params,
json=data,
proxy=self.proxy_url if use_proxy else None,
timeout=300,
) as resp:
try:
raw_data = await resp.json()
except ContentTypeError:
_raw_data = await resp.text()
raw_data = {'retcode': -999, 'data': _raw_data}
logger.debug(raw_data)
# 判断retcode
if 'retcode' in raw_data:
retcode: int = raw_data['retcode']
elif 'code' in raw_data:
retcode: int = raw_data['code']
else:
retcode = 0
# 针对1034做特殊处理
if retcode == 1034:
if uid and self.is_sr and _ == 0:
sqla = self.dbsqla.get_sqla('TEMP')
new_fp = await self.generate_fp_by_uid(uid)
await sqla.update_user_data(uid, {'fp': new_fp})
header['x-rpc-device_fp'] = new_fp
if isinstance(params, Dict):
header['DS'] = get_ds_token(
'&'.join(
[f'{k}={v}' for k, v in params.items()]
)
)
else:
ch = await self._upass(header)
self.chs[header['Cookie']] = ch
elif retcode == -10001 and uid:
sqla = self.dbsqla.get_sqla('TEMP')
new_fp = await self.generate_fp_by_uid(uid)
await sqla.update_user_data(uid, {'fp': new_fp})
header['x-rpc-device_fp'] = new_fp
elif retcode != 0:
return retcode
else:
return raw_data
else:
return -999
'''
async def _mys_request(
self,
url: str,
method: Literal['GET', 'POST'] = 'GET',
header: Dict[str, Any] = _HEADER,
params: Optional[Dict[str, Any]] = None,
data: Optional[Dict[str, Any]] = None,
use_proxy: Optional[bool] = False,
) -> Union[Dict, int]:
import types
import inspect
async with ClientSession(
connector=TCPConnector(verify_ssl=ssl_verify)
) as client:
if 'Cookie' in header:
if header['Cookie'] in self.chs:
header['x-rpc-challenge'] = self.chs.pop(header["Cookie"])
async with client.request(
method,
url=url,
@ -211,22 +376,73 @@ class BaseMysApi:
_raw_data = await resp.text()
raw_data = {'retcode': -999, 'data': _raw_data}
logger.debug(raw_data)
# 判断retcode
if 'retcode' in raw_data:
retcode: int = raw_data['retcode']
elif 'code' in raw_data:
retcode: int = raw_data['code']
else:
retcode = 0
# 针对1034做特殊处理
if retcode == 1034:
await self._upass(header)
return retcode
try:
# 获取ch
ch = await self._upass(header)
# 记录ck -> ch的对照表
if "Cookie" in header:
self.chs[header["Cookie"]] = ch
# 获取当前的栈帧
curframe = inspect.currentframe()
# 确保栈帧存在
assert curframe
# 获取调用者的栈帧
calframe = curframe.f_back
# 确保调用者的栈帧存在
assert calframe
# 获取调用者的函数名
caller_name = calframe.f_code.co_name
# 获取调用者函数的局部变量字典
caller_args = inspect.getargvalues(calframe).locals
# 获取调用者的参数列表
caller_args2 = inspect.getargvalues(calframe).args
# # 生成一个字典键为调用者的参数名值为对应的局部变量值如果不存在则为None
caller_args3 = {
k: caller_args.get(k, None) for k in caller_args2
}
if caller_name != '_mys_req_get':
return await types.FunctionType(
calframe.f_code, globals()
)(**caller_args3)
else:
curframe = calframe
calframe = curframe.f_back
assert calframe
caller_name = calframe.f_code.co_name
caller_args = inspect.getargvalues(calframe).locals
caller_args2 = inspect.getargvalues(calframe).args
caller_args3 = {
k: caller_args.get(k, None)
for k in caller_args2
}
return await types.FunctionType(
calframe.f_code, globals()
)(**caller_args3)
except Exception as e:
logger.error(e)
traceback.print_exc()
return -999
elif retcode != 0:
return retcode
return raw_data
'''
class MysApi(BaseMysApi):
async def _pass(self, gt: str, ch: str, header: Dict):
async def _pass(
self, gt: str, ch: str, header: Dict
) -> Tuple[Optional[str], Optional[str]]:
# 警告使用该服务例如某RR等需要注意风险问题
# 本项目不以任何形式提供相关接口
# 代码来源GITHUB项目MIT开源
@ -247,13 +463,14 @@ class MysApi(BaseMysApi):
return validate, ch
async def _upass(self, header: Dict, is_bbs: bool = False):
async def _upass(self, header: Dict, is_bbs: bool = False) -> str:
logger.info('[upass] 进入处理...')
if is_bbs:
raw_data = await self.get_bbs_upass_link(header)
else:
raw_data = await self.get_upass_link(header)
if isinstance(raw_data, int):
return False
return ''
gt = raw_data['data']['gt']
ch = raw_data['data']['challenge']
@ -261,8 +478,13 @@ class MysApi(BaseMysApi):
if vl:
await self.get_header_and_vl(header, ch, vl)
if ch:
logger.info(f'[upass] 获取ch -> {ch}')
return ch
else:
return ''
else:
return True
return ''
async def get_upass_link(self, header: Dict) -> Union[int, Dict]:
header['DS'] = get_ds_token('is_high=false')
@ -958,7 +1180,11 @@ class MysApi(BaseMysApi):
'currency': 'CNY',
'pay_plat': method,
}
data = {'order': order, 'sign': gen_payment_sign(order)}
data = {
'order': order,
'special_info': 'topup_center',
'sign': gen_payment_sign(order),
}
HEADER['x-rpc-device_id'] = device_id
HEADER['x-rpc-client_type'] = '4'
resp = await self._mys_request(

View File

@ -1,56 +1,15 @@
from typing import Dict, Literal, Optional
from typing import Literal, Optional
from gsuid_core.utils.api.mys import MysApi
from gsuid_core.utils.database.api import DBSqla
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
gsconfig = core_plugins_config
class _MysApi(MysApi):
dbsqla: DBSqla = DBSqla()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def _pass(self, gt: str, ch: str, header: Dict):
# 警告使用该服务例如某RR等需要注意风险问题
# 本项目不以任何形式提供相关接口
# 代码来源GITHUB项目MIT开源
_pass_api = gsconfig.get_config('_pass_API').data
if _pass_api:
data = await self._mys_request(
url=f'{_pass_api}&gt={gt}&challenge={ch}',
method='GET',
header=header,
)
if isinstance(data, int):
return None, None
else:
validate = data['data']['validate']
ch = data['data']['challenge']
else:
validate = None
return validate, ch
async def _upass(self, header: Dict, is_bbs: bool = False):
if is_bbs:
raw_data = await self.get_bbs_upass_link(header)
else:
raw_data = await self.get_upass_link(header)
if isinstance(raw_data, int):
return False
gt = raw_data['data']['gt']
ch = raw_data['data']['challenge']
vl, ch = await self._pass(gt, ch, header)
if vl:
await self.get_header_and_vl(header, ch, vl)
else:
return True
async def get_ck(
self, uid: str, mode: Literal['OWNER', 'RANDOM'] = 'RANDOM'
) -> Optional[str]:
@ -62,5 +21,23 @@ class _MysApi(MysApi):
async def get_stoken(self, uid: str) -> Optional[str]:
return await self.dbsqla.get_sqla('TEMP').get_user_stoken(uid)
async def get_user_fp(self, uid: str) -> Optional[str]:
data = await self.dbsqla.get_sqla('TEMP').get_user_fp(uid)
if data is None:
data = await self.generate_fp_by_uid(uid)
await self.dbsqla.get_sqla('TEMP').update_user_data(
uid, {'fp': data}
)
return data
async def get_user_device_id(self, uid: str) -> Optional[str]:
data = await self.dbsqla.get_sqla('TEMP').get_user_device_id(uid)
if data is None:
data = self.get_device_id()
await self.dbsqla.get_sqla('TEMP').update_user_data(
uid, {'device_id': data}
)
return data
mys_api = _MysApi()

View File

@ -217,7 +217,7 @@ async def _deal_ck(bot_id: str, mes: str, user_id: str) -> str:
uid_bind = i['game_role_id']
elif i['game_id'] == 6:
sr_uid_bind = i['game_role_id']
if uid and sr_uid:
if uid_bind and sr_uid_bind:
break
else:
if not (uid_bind or sr_uid_bind):
@ -236,8 +236,16 @@ async def _deal_ck(bot_id: str, mes: str, user_id: str) -> str:
if uid is None:
uid = '0'
device_id = mys_api.get_device_id()
fp = await mys_api.generate_fp_by_uid(uid)
await sqla.insert_user_data(
user_id, uid_bind, sr_uid_bind, account_cookie, app_cookie
user_id,
uid_bind,
sr_uid_bind,
account_cookie,
app_cookie,
fp,
device_id,
)
im_list.append(

View File

@ -2,10 +2,13 @@ import io
import json
import base64
import asyncio
from pathlib import Path
from http.cookies import SimpleCookie
from typing import Any, List, Tuple, Union, Literal
import qrcode
import aiofiles
from qrcode.image.pil import PilImage
from qrcode.constants import ERROR_CORRECT_L
from gsuid_core.bot import Bot
@ -15,17 +18,10 @@ from gsuid_core.segment import MessageSegment
from gsuid_core.utils.api.mys_api import mys_api
from gsuid_core.utils.database.api import DBSqla
disnote = '''免责声明:您将通过扫码完成获取米游社sk以及ck。
本Bot将不会保存您的登录状态
我方仅提供米游社查询及相关游戏内容服务
若您的账号封禁被盗等处罚与我方无关
害怕风险请勿扫码!
'''
get_sqla = DBSqla().get_sqla
def get_qrcode_base64(url):
async def get_qrcode_base64(url: str, path: Path, bot_id: str) -> bytes:
qr = qrcode.QRCode(
version=1,
error_correction=ERROR_CORRECT_L,
@ -34,11 +30,33 @@ def get_qrcode_base64(url):
)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
img_byte = io.BytesIO()
img.save(img_byte, format='PNG') # type: ignore
img_byte = img_byte.getvalue()
return base64.b64encode(img_byte).decode()
img = qr.make_image(fill_color=(255, 134, 36), back_color='white')
assert isinstance(img, PilImage)
if bot_id == 'onebot':
img = img.resize((700, 700)) # type: ignore
img.save( # type: ignore
path,
format='PNG',
save_all=True,
append_images=[img],
duration=100,
loop=0,
)
async with aiofiles.open(path, 'rb') as fp:
img = await fp.read()
elif bot_id == 'onebot_v12':
img_byte = io.BytesIO()
img.save(img_byte, format='PNG') # type: ignore
img_byte = img_byte.getvalue()
img = f'base64://{base64.b64encode(img_byte).decode()}'
return img # type: ignore
else:
img_byte = io.BytesIO()
img.save(img_byte, format='PNG') # type: ignore
img = img_byte.getvalue()
return img
async def refresh(
@ -51,15 +69,15 @@ async def refresh(
code_data['app_id'], code_data['ticket'], code_data['device']
)
if isinstance(status_data, int):
logger.warning('二维码已过期')
logger.warning('[登录]二维码已过期')
return False, None
if status_data['stat'] == 'Scanned':
if not scanned:
logger.info('二维码已扫描')
logger.info('[登录]二维码已扫描')
scanned = True
continue
if status_data['stat'] == 'Confirmed':
logger.info('二维码已确认')
logger.info('[登录]二维码已确认')
break
return True, json.loads(status_data['payload']['raw'])
@ -73,39 +91,41 @@ async def qrcode_login(bot: Bot, ev: Event, user_id: str) -> str:
code_data = await mys_api.create_qrcode_url()
if isinstance(code_data, int):
return await send_msg('链接创建失败...')
try:
im = []
im.append(MessageSegment.text('请使用米游社扫描下方二维码登录:'))
im.append(
MessageSegment.image(
f'base64://{get_qrcode_base64(code_data["url"])}'
)
return await send_msg('[登录]链接创建失败...')
path = Path(__file__).parent / f'{user_id}.gif'
im = []
im.append(MessageSegment.text('请使用米游社扫描下方二维码登录:'))
im.append(
MessageSegment.image(
await get_qrcode_base64(code_data['url'], path, ev.bot_id)
)
im.append(
MessageSegment.text(
'免责声明:您将通过扫码完成获取米游社sk以及ck。\n'
'本Bot将不会保存您的登录状态。\n'
'我方仅提供米游社查询及相关游戏内容服务,\n'
'若您的账号封禁、被盗等处罚与我方无关。\n'
'害怕风险请勿扫码~'
)
)
im.append(
MessageSegment.text(
'免责声明:您将通过扫码完成获取米游社sk以及ck。\n'
'我方仅提供米游社查询及相关游戏内容服务,\n'
'若您的账号封禁、被盗等处罚与我方无关。\n'
'害怕风险请勿扫码~'
)
await bot.send(MessageSegment.node(im))
except Exception as e:
logger.error(e)
logger.warning(f'[扫码登录] {user_id} 图片发送失败')
)
await bot.send(MessageSegment.node(im))
if path.exists():
path.unlink()
status, game_token_data = await refresh(code_data)
if status:
assert game_token_data is not None # 骗过 pyright
logger.info('game_token获取成功')
logger.info('[登录]game_token获取成功')
cookie_token = await mys_api.get_cookie_token(**game_token_data)
stoken_data = await mys_api.get_stoken_by_game_token(
account_id=int(game_token_data['uid']),
game_token=game_token_data['token'],
)
if isinstance(stoken_data, int):
return await send_msg('获取SK失败...')
return await send_msg('[登录]获取SK失败...')
account_id = game_token_data['uid']
stoken = stoken_data['token']['token']
mid = stoken_data['user_info']['mid']
@ -114,7 +134,7 @@ async def qrcode_login(bot: Bot, ev: Event, user_id: str) -> str:
stoken, account_id, app_cookie
)
if isinstance(ck, int):
return await send_msg('获取CK失败...')
return await send_msg('[登录]获取CK失败...')
ck = ck['cookie_token']
cookie_check = f'account_id={account_id};cookie_token={ck}'
get_uid = await mys_api.get_mihoyo_bbs_info(account_id, cookie_check)
@ -131,26 +151,26 @@ async def qrcode_login(bot: Bot, ev: Event, user_id: str) -> str:
if uid_check or sruid_check:
pass
else:
im = f'你的米游社账号{account_id}尚未绑定原神账号,请前往米游社操作!'
im = f'[登录]你的米游社账号{account_id}尚未绑定原神账号,请前往米游社操作!'
return await send_msg(im)
else:
im = '请求失败, 请稍后再试...'
im = '[登录]请求失败, 请稍后再试...'
return await send_msg(im)
uid_bind = await sqla.get_bind_uid(user_id)
sruid_bind = await sqla.get_bind_sruid(user_id)
uid_bind_list = await sqla.get_bind_uid_list(user_id)
sruid_bind_list = await sqla.get_bind_sruid_list(user_id)
# 没有在gsuid绑定uid的情况
if not (uid_bind or sruid_bind):
logger.warning('game_token获取失败')
if not (uid_bind_list or sruid_bind_list):
logger.warning('[登录]game_token获取失败')
im = '你还没有绑定uid, 请输入[绑定uid123456]绑定你的uid, 再发送[扫码登录]进行绑定'
return await send_msg(im)
if isinstance(cookie_token, int):
return await send_msg('获取CK失败...')
return await send_msg('[登录]获取CK失败...')
# 比对gsuid数据库和扫码登陆获取到的uid
if (
str(uid_bind) == uid_check
or str(sruid_bind) == str(sruid_check)
or str(uid_bind) == account_id
uid_check in uid_bind_list
or sruid_check in sruid_bind_list
or account_id in uid_bind_list
):
return SimpleCookie(
{
@ -161,12 +181,12 @@ async def qrcode_login(bot: Bot, ev: Event, user_id: str) -> str:
}
).output(header='', sep=';')
else:
logger.warning('game_token获取失败')
logger.warning('[登录]game_token获取失败')
im = (
f'检测到扫码登录UID{uid_check}与绑定UID{uid_bind}不同, '
f'检测到扫码登录UID{uid_check}与绑定UID不同, '
'gametoken获取失败, 请重新发送[扫码登录]进行登录!'
)
else:
logger.warning('game_token获取失败')
im = 'game_token获取失败: 二维码已过期'
logger.warning('[登录]game_token获取失败')
im = '[登录]game_token获取失败: 二维码已过期'
return await send_msg(im)

View File

@ -1,6 +1,5 @@
import re
import asyncio
import contextlib
from typing import Dict, List, Literal, Optional
from sqlmodel import SQLModel
@ -43,13 +42,20 @@ class SQLA:
'ALTER TABLE GsBind ADD COLUMN sr_uid TEXT',
'ALTER TABLE GsUser ADD COLUMN sr_uid TEXT',
'ALTER TABLE GsUser ADD COLUMN sr_region TEXT',
'ALTER TABLE GsUser ADD COLUMN fp TEXT',
'ALTER TABLE GsUser ADD COLUMN device_id TEXT',
'ALTER TABLE GsUser ADD COLUMN sr_sign_switch TEXT DEFAULT "off"',
'ALTER TABLE GsUser ADD COLUMN sr_push_switch TEXT DEFAULT "off"',
'ALTER TABLE GsUser ADD COLUMN draw_switch TEXT DEFAULT "off"',
'ALTER TABLE GsCache ADD COLUMN sr_uid TEXT',
]
with contextlib.suppress(Exception):
async with self.async_session() as session:
for _t in exec_list:
async with self.async_session() as session:
for _t in exec_list:
try:
await session.execute(text(_t))
await session.commit()
await session.commit()
except: # noqa: E722
pass
#####################
# GsBind 部分 #
@ -207,6 +213,22 @@ class SQLA:
result = await session.execute(sql)
return data[0] if (data := result.scalars().all()) else None
async def select_user_all_data_by_user_id(
self, user_id: str
) -> Optional[List[GsUser]]:
async with self.async_session() as session:
async with session.begin():
sql = select(GsUser).where(GsUser.user_id == user_id)
result = await session.execute(sql)
data = result.scalars().all()
return data if data else None
async def select_user_data_by_user_id(
self, user_id: str
) -> Optional[GsUser]:
data = await self.select_user_all_data_by_user_id(user_id)
return data[0] if data else None
async def select_cache_cookie(self, uid: str) -> Optional[str]:
async with self.async_session() as session:
async with session.begin():
@ -228,6 +250,14 @@ class SQLA:
await session.execute(sql)
return True
async def get_user_fp(self, uid: str) -> Optional[str]:
data = await self.select_user_data(uid)
return data.fp if data else None
async def get_user_device_id(self, uid: str) -> Optional[str]:
data = await self.select_user_data(uid)
return data.device_id if data else None
async def insert_cache_data(
self,
cookie: str,
@ -251,6 +281,8 @@ class SQLA:
sr_uid: Optional[str] = None,
cookie: Optional[str] = None,
stoken: Optional[str] = None,
fp: Optional[str] = None,
device_id: Optional[str] = None,
) -> bool:
async with self.async_session() as session:
async with session.begin():
@ -265,6 +297,7 @@ class SQLA:
bot_id=self.bot_id,
user_id=user_id,
sr_uid=sr_uid,
fp=fp,
)
)
await session.execute(sql)
@ -279,6 +312,7 @@ class SQLA:
bot_id=self.bot_id,
user_id=user_id,
uid=uid,
fp=fp,
)
)
await session.execute(sql)
@ -301,10 +335,15 @@ class SQLA:
sign_switch='off',
push_switch='off',
bbs_switch='off',
draw_switch='off',
region=SERVER.get(uid[0], 'cn_gf01') if uid else None,
sr_region=SR_SERVER.get(sr_uid[0], None)
if sr_uid
else None,
fp=fp,
device_id=device_id,
sr_push_switch='off',
sr_sign_switch='off',
)
session.add(user_data)
await session.commit()
@ -314,13 +353,9 @@ class SQLA:
async with self.async_session() as session:
async with session.begin():
sql = (
update(GsUser).where(
GsUser.sr_uid == uid, GsUser.bot_id == self.bot_id
)
update(GsUser).where(GsUser.sr_uid == uid)
if self.is_sr
else update(GsUser).where(
GsUser.uid == uid, GsUser.bot_id == self.bot_id
)
else update(GsUser).where(GsUser.uid == uid)
)
if data is not None:
query = sql.values(**data)
@ -456,10 +491,18 @@ class SQLA:
data = await self.select_user_data(uid)
return data.cookie if data else None
async def get_user_cookie_by_user_id(self, user_id: str) -> Optional[str]:
data = await self.select_user_data_by_user_id(user_id)
return data.cookie if data else None
async def cookie_validate(self, uid: str) -> bool:
data = await self.select_user_data(uid)
return True if data and data.status is None else False
async def get_user_stoken_by_user_id(self, user_id: str) -> Optional[str]:
data = await self.select_user_data_by_user_id(user_id)
return data.stoken if data and data.stoken else None
async def get_user_stoken(self, uid: str) -> Optional[str]:
data = await self.select_user_data(uid)
return data.stoken if data and data.stoken else None
@ -528,7 +571,7 @@ class SQLA:
return None
async def get_switch_status_list(
self, switch: Literal['push', 'sign', 'bbs']
self, switch: Literal['push', 'sign', 'bbs', 'sr_push', 'sr_sign']
) -> List[GsUser]:
async with self.async_session() as session:
async with session.begin():
@ -567,11 +610,7 @@ class SQLA:
async with self.async_session() as session:
async with session.begin():
await self.push_exists(uid)
sql = (
update(GsPush)
.where(GsPush.uid == uid, GsPush.bot_id == self.bot_id)
.values(**data)
)
sql = update(GsPush).where(GsPush.uid == uid).values(**data)
await session.execute(sql)
await session.commit()
return True
@ -588,9 +627,7 @@ class SQLA:
async with self.async_session() as session:
async with session.begin():
await self.push_exists(uid)
sql = select(GsPush).where(
GsPush.uid == uid, GsPush.bot_id == self.bot_id
)
sql = select(GsPush).where(GsPush.uid == uid)
result = await session.execute(sql)
data = result.scalars().all()
return data[0] if len(data) >= 1 else None
@ -598,9 +635,7 @@ class SQLA:
async def push_exists(self, uid: str) -> bool:
async with self.async_session() as session:
async with session.begin():
sql = select(GsPush).where(
GsPush.uid == uid, GsPush.bot_id == self.bot_id
)
sql = select(GsPush).where(GsPush.uid == uid)
result = await session.execute(sql)
data = result.scalars().all()
if not data:

View File

@ -28,7 +28,12 @@ class GsUser(SQLModel, table=True):
push_switch: str = Field(title='全局推送开关')
sign_switch: str = Field(title='自动签到')
bbs_switch: str = Field(title='自动米游币')
draw_switch: str = Field(title='自动留影叙佳期')
sr_push_switch: str = Field(title='星铁全局推送开关')
sr_sign_switch: str = Field(title='星铁自动签到')
status: Optional[str] = Field(default=None, title='状态')
fp: Optional[str] = Field(default=None, title='Fingerprint')
device_id: Optional[str] = Field(default=None, title='设备ID')
class GsCache(SQLModel, table=True):
@ -45,15 +50,15 @@ class GsPush(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True, title='序号')
bot_id: str = Field(title='平台')
uid: str = Field(default=None, title='原神UID')
coin_push: Optional[str] = Field(title='洞天宝钱推送')
coin_value: Optional[int] = Field(title='洞天宝钱阈值')
coin_is_push: Optional[str] = Field(title='洞天宝钱是否已推送')
resin_push: Optional[str] = Field(title='体力推送')
resin_value: Optional[int] = Field(title='体力阈值')
resin_is_push: Optional[str] = Field(title='体力是否已推送')
go_push: Optional[str] = Field(title='派遣推送')
go_value: Optional[int] = Field(title='派遣阈值')
go_is_push: Optional[str] = Field(title='派遣是否已推送')
transform_push: Optional[str] = Field(title='质变仪推送')
transform_value: Optional[int] = Field(title='质变仪阈值')
transform_is_push: Optional[str] = Field(title='质变仪是否已推送')
coin_push: Optional[str] = Field(title='洞天宝钱推送', default='off')
coin_value: Optional[int] = Field(title='洞天宝钱阈值', default=2100)
coin_is_push: Optional[str] = Field(title='洞天宝钱是否已推送', default='off')
resin_push: Optional[str] = Field(title='体力推送', default='off')
resin_value: Optional[int] = Field(title='体力阈值', default=140)
resin_is_push: Optional[str] = Field(title='体力是否已推送', default='off')
go_push: Optional[str] = Field(title='派遣推送', default='off')
go_value: Optional[int] = Field(title='派遣阈值', default=300)
go_is_push: Optional[str] = Field(title='派遣是否已推送', default='off')
transform_push: Optional[str] = Field(title='质变仪推送', default='off')
transform_value: Optional[int] = Field(title='质变仪阈值', default=1000)
transform_is_push: Optional[str] = Field(title='质变仪是否已推送', default='off')

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -1,4 +1,15 @@
from typing import Union
from pathlib import Path
from typing import Union, Optional
from PIL import Image, ImageDraw
from gsuid_core.utils.fonts.fonts import core_font
from gsuid_core.utils.image.convert import convert_img
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
from gsuid_core.utils.image.image_tools import (
get_color_bg,
draw_center_text_by_line,
)
UID_HINT = '你还没有绑定过uid哦!\n请使用[绑定uid123456]命令绑定!'
MYS_HINT = '你还没有绑定过mysid哦!\n请使用[绑定mys1234]命令绑定!'
@ -16,6 +27,9 @@ UPDATE_HINT = '''更新失败!更多错误信息请查看控制台...
>> [gs强制更新](危险)
>> [gs强行强制更新](超级危险)!'''
TEXT_PATH = Path(__file__).parent / 'image' / 'texture2d'
is_pic_error = core_plugins_config.get_config('ChangeErrorToPic').data
def get_error(retcode: Union[int, str]) -> str:
if retcode == -51:
@ -48,5 +62,54 @@ def get_error(retcode: Union[int, str]) -> str:
return '该API需要CK, 查询的用户/UID未绑定CK...'
elif retcode == 10104:
return 'CK与用户信息不符, 请检查代码实现...'
elif retcode == -999:
return VERIFY_HINT
elif retcode == 125:
return '该充值方式暂时不可用!'
elif retcode == 126:
return '该充值方式不正确!'
else:
return f'API报错, 错误码为{retcode}!'
return f'未知错误, 错误码为{retcode}!'
def get_error_type(retcode: Union[int, str]) -> str:
retcode = int(retcode)
if retcode in [-51, 10104]:
return '绑定信息错误'
elif retcode in [-400, 400]:
return 'MGGApi错误'
else:
return 'Api错误'
async def get_error_img(retcode: Union[int, str]) -> Union[bytes, str]:
error_message = get_error(retcode)
if is_pic_error:
error_type = get_error_type(retcode)
return await draw_error_img(retcode, error_message, error_type)
else:
return error_message
async def draw_error_img(
retcode: Union[int, str] = 51233,
error_message: Optional[str] = None,
error_type: Optional[str] = None,
) -> bytes:
if error_type is None:
error_type = 'API报错'
if error_message is None:
error_message = '未知错误, 请检查控制台输出...'
error_img = Image.open(TEXT_PATH / 'error_img.png')
img = await get_color_bg(
*error_img.size, is_full=True, color=(228, 222, 210)
)
img.paste(error_img, (0, 0), error_img)
img_draw = ImageDraw.Draw(img)
img_draw.text((350, 646), error_type, 'white', core_font(26), 'mm')
img_draw.text((350, 695), f'错误码 {retcode}', 'white', core_font(36), 'mm')
draw_center_text_by_line(
img_draw, (350, 750), error_message, core_font(30), 'black', 440
)
return await convert_img(img)

View File

@ -0,0 +1,9 @@
from pathlib import Path
from PIL import ImageFont
FONT_ORIGIN_PATH = Path(__file__).parent / 'yuanshen_origin.ttf'
def core_font(size: int) -> ImageFont.FreeTypeFont:
return ImageFont.truetype(str(FONT_ORIGIN_PATH), size=size)

Binary file not shown.

View File

@ -4,7 +4,10 @@ from base64 import b64encode
from typing import Union, overload
import aiofiles
from PIL import Image, ImageFont
from PIL import Image, ImageDraw, ImageFont
from gsuid_core.utils.fonts.fonts import core_font
from gsuid_core.utils.image.image_tools import draw_center_text_by_line
@overload
@ -107,3 +110,18 @@ def get_str_size(
def get_height(content: str, size: int) -> int:
line_count = content.count('\n')
return (line_count + 1) * size
async def text2pic(text: str, max_size: int = 600, font_size: int = 24):
if text.endswith('\n'):
text = text[:-1]
img = Image.new(
'RGB', (max_size, len(text) * font_size // 5), (228, 222, 210)
)
img_draw = ImageDraw.ImageDraw(img)
y = draw_center_text_by_line(
img_draw, (50, 0), text, core_font(font_size), 'black', 500, True
)
img = img.crop((0, 0, 600, int(y + 30)))
return await convert_img(img)

View File

@ -8,7 +8,10 @@ import httpx
from httpx import get
from PIL import Image, ImageDraw, ImageFont
from gsuid_core.data_store import get_res_path
TEXT_PATH = Path(__file__).parent / 'texture2d'
BG_PATH = Path(__file__).parents[1] / 'default_bg'
async def get_pic(url, size: Optional[Tuple[int, int]] = None) -> Image.Image:
@ -28,6 +31,36 @@ async def get_pic(url, size: Optional[Tuple[int, int]] = None) -> Image.Image:
return pic
def draw_center_text_by_line(
img: ImageDraw.ImageDraw,
pos: Tuple[int, int],
text: str,
font: ImageFont.FreeTypeFont,
fill: Union[Tuple[int, int, int, int], str],
max_length: float,
not_center: bool = False,
) -> float:
pun = "。!?;!?"
x, y = pos
_, h = font.getsize('X')
line = ''
lenth = 0
anchor = 'la' if not_center else 'mm'
for char in text:
size, _ = font.getsize(char) # 获取当前字符的宽度
lenth += size
line += char
if lenth < max_length and char not in pun and char != '\n':
pass
else:
img.text((x, y), line, fill, font, anchor)
line, lenth = '', 0
y += h * 1.55
else:
img.text((x, y), line, fill, font, anchor)
return y
def draw_text_by_line(
img: Image.Image,
pos: Tuple[int, int],
@ -37,7 +70,7 @@ def draw_text_by_line(
max_length: float,
center=False,
line_space: Optional[float] = None,
):
) -> float:
"""
在图片上写长段文字, 自动换行
max_length单行最大长度, 单位像素
@ -63,7 +96,7 @@ def draw_text_by_line(
font_size = font.getsize(row)
x = math.ceil((img.size[0] - font_size[0]) / 2)
draw.text((x, y), row, font=font, fill=fill)
row = ""
row = ''
length = 0
y += y_add
if row != "":
@ -71,6 +104,7 @@ def draw_text_by_line(
font_size = font.getsize(row)
x = math.ceil((img.size[0] - font_size[0]) / 2)
draw.text((x, y), row, font=font, fill=fill)
return y
def easy_paste(
@ -174,12 +208,42 @@ def crop_center_img(
return crop_img
async def get_color_bg(
based_w: int,
based_h: int,
bg_path: Optional[Path] = None,
without_mask: bool = False,
is_full: bool = False,
color: Optional[Tuple[int, int, int]] = None,
full_opacity: int = 200,
) -> Image.Image:
if bg_path is None:
bg_path = get_res_path(['GsCore', 'bg'])
CI_img = CustomizeImage(bg_path)
img = CI_img.get_image(None, based_w, based_h)
if color is None:
color = CI_img.get_bg_color(img)
if is_full:
color_img = Image.new('RGBA', (based_w, based_h), color)
mask = Image.new(
'RGBA', (based_w, based_h), (255, 255, 255, full_opacity)
)
img.paste(color_img, (0, 0), mask)
elif not without_mask:
color_mask = Image.new('RGBA', (based_w, based_h), color)
enka_mask = Image.open(TEXT_PATH / 'bg_mask.png').resize(
(based_w, based_h)
)
img.paste(color_mask, (0, 0), enka_mask)
return img
class CustomizeImage:
def __init__(self, bg_path: Path) -> None:
self.bg_path = bg_path
def get_image(
self, image: Union[str, Image.Image], based_w: int, based_h: int
self, image: Union[str, Image.Image, None], based_w: int, based_h: int
) -> Image.Image:
# 获取背景图片
if isinstance(image, Image.Image):
@ -187,7 +251,11 @@ class CustomizeImage:
elif image:
edit_bg = Image.open(BytesIO(get(image).content)).convert('RGBA')
else:
path = random.choice(list(self.bg_path.iterdir()))
_lst = list(self.bg_path.iterdir())
if _lst:
path = random.choice(list(self.bg_path.iterdir()))
else:
path = random.choice(list(BG_PATH.iterdir()))
edit_bg = Image.open(path).convert('RGBA')
# 确定图片的长宽

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -49,4 +49,11 @@ CONIFG_DEFAULT: Dict[str, GSC] = {
'AutoRestartCoreTime': GsListStrConfig(
'自动重启Core时间设置', '每晚自动重启Core时间设置(时, 分)', ['4', '40']
),
'AutoAddRandomText': GsBoolConfig('自动加入随机字符串', '自动加入随机字符串', False),
'RandomText': GsStrConfig(
'随机字符串列表', '随机字符串列表', 'abcdefghijklmnopqrstuvwxyz'
),
'ChangeErrorToPic': GsBoolConfig('错误提示转换为图片', '将一部分报错提示转换为图片', True),
'AutoTextToPic': GsBoolConfig('自动文字转图', '将所有发送的文字转图', True),
'TextToPicThreshold': GsStrConfig('文转图阈值', '开启自动转图后超过该阈值的文字会转成图片', '20'),
}

View File

@ -13,6 +13,14 @@ from .api import CORE_PATH, PLUGINS_PATH, proxy_url, plugins_lib
plugins_list: Dict[str, Dict[str, str]] = {}
async def update_all_plugins() -> List[str]:
log_list = []
for plugin in PLUGINS_PATH.iterdir():
if plugin.is_dir():
log_list.extend(update_from_git(0, plugin))
return log_list
async def refresh_list() -> List[str]:
refresh_list = []
async with aiohttp.ClientSession() as session:
@ -57,7 +65,12 @@ def install_plugins(plugins: Dict[str, str]) -> str:
path = PLUGINS_PATH / plugin_name
if path.exists():
return '该插件已经安装过了!'
Repo.clone_from(git_path, path, single_branch=True, depth=1)
config = {'single_branch': True, 'depth': 1}
if plugins['branch'] != 'main':
config['branch'] = plugins['branch']
Repo.clone_from(git_path, path, **config)
logger.info(f'插件{plugin_name}安装成功!')
return f'插件{plugin_name}安装成功!发送[gs重启]以应用!'

View File

@ -11,11 +11,11 @@ from fastapi_amis_admin.crud import BaseApiOut
from sqlalchemy.ext.asyncio import AsyncEngine
from fastapi_user_auth.site import AuthAdminSite
from fastapi_amis_admin.models.fields import Field
from fastapi_amis_admin.admin.site import APIDocsApp
from fastapi_amis_admin.admin.settings import Settings
from fastapi_user_auth.auth.models import UserRoleLink
from fastapi_amis_admin.utils.translation import i18n as _
from fastapi import Depends, FastAPI, Request, HTTPException
from fastapi_amis_admin.admin.site import FileAdmin, APIDocsApp
from fastapi_amis_admin.amis.constants import LevelEnum, DisplayModeEnum
from fastapi_user_auth.admin import (
FormAdmin,
@ -417,4 +417,4 @@ class PluginsManagePage(GsAdminPage):
# 取消注册默认管理类
site.unregister_admin(admin.HomeAdmin, APIDocsApp)
site.unregister_admin(admin.HomeAdmin, APIDocsApp, FileAdmin)

491
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ uvicorn = ">=0.20.0"
websockets = "^10.4"
loguru = "^0.6.0"
urllib3 = "^1.26.15"
mpmath = "^1.3.0"
[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
@ -42,11 +43,14 @@ pre-commit = "^2.21.0"
pycln = "^2.1.2"
[tool.poetry.scripts]
core = 'gsuid_core.core:main'
[[tool.poetry.source]]
name = "mirrors"
url = "https://mirrors.bfsu.edu.cn/pypi/web/simple/"
default = true
secondary = false
priority = "default"
[build-system]
requires = ["poetry-core"]

View File

@ -4,7 +4,7 @@ aiofiles==23.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0"
aiohttp==3.8.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
aiosignal==1.3.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
aiosqlite==0.19.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
anyio==3.6.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
anyio==3.7.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
apscheduler==3.10.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
async-timeout==4.0.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
attrs==23.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
@ -14,33 +14,34 @@ beautifulsoup4==4.12.2 ; python_full_version >= "3.8.1" and python_full_version
certifi==2023.5.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
charset-normalizer==3.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
click==8.1.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and platform_system == "Windows" or python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "win32"
colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and (platform_system == "Windows" or sys_platform == "win32")
dnspython==2.3.0 ; python_full_version >= "3.8.1" and python_version < "4.0"
email-validator==2.0.0.post2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
fastapi-amis-admin==0.5.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
exceptiongroup==1.1.1 ; python_full_version >= "3.8.1" and python_version < "3.11"
fastapi-amis-admin==0.5.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
fastapi-user-auth==0.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
fastapi==0.95.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
fastapi==0.97.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
frozenlist==1.3.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
gitdb==4.0.10 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
gitpython==3.1.31 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
greenlet==2.0.2 ; python_full_version >= "3.8.1" and platform_machine == "aarch64" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "ppc64le" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "x86_64" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "amd64" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "AMD64" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "win32" and python_full_version < "4.0.0" or python_full_version >= "3.8.1" and platform_machine == "WIN32" and python_full_version < "4.0.0"
greenlet==2.0.2 ; python_full_version >= "3.8.1" and (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64") and python_full_version < "4.0.0"
h11==0.14.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
httpcore==0.17.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
httpx==0.24.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
httpcore==0.17.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
httpx==0.24.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
idna==3.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
loguru==0.6.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
lxml==4.9.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
msgspec==0.15.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
mpmath==1.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
msgspec==0.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
multidict==6.0.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
passlib==1.7.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pillow==9.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic==1.10.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic==1.10.9 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pypng==0.20220715.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
python-multipart==0.0.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytz-deprecation-shim==0.1.0.post0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pytz==2023.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
qrcode[pil]==7.4.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
setuptools==67.7.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
setuptools==67.8.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
six==1.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
smmap==5.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sniffio==1.3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
@ -50,11 +51,11 @@ sqlalchemy2-stubs==0.0.2a34 ; python_full_version >= "3.8.1" and python_full_ver
sqlalchemy==1.4.41 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sqlmodel==0.0.8 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
sqlmodelx==0.0.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
starlette==0.26.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
typing-extensions==4.5.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
tzdata==2023.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
tzlocal==4.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
urllib3==1.26.15 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
starlette==0.27.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
typing-extensions==4.6.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
tzdata==2023.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and platform_system == "Windows"
tzlocal==5.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
urllib3==1.26.16 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
uvicorn==0.22.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
websockets==10.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
win32-setctime==1.1.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "win32"