抽卡记录新UI,无法抽卡链接,通过Stoken更新

This commit is contained in:
CMHopeSunshine 2022-08-29 18:54:24 +08:00
parent ee24302a13
commit b6f603896b
28 changed files with 570 additions and 604 deletions

View File

@ -7,7 +7,7 @@ from LittlePaimon.utils.migration import migrate_database
from LittlePaimon.utils.tool import check_resource
DRIVER = get_driver()
__version__ = '3.0.0beta2'
__version__ = '3.0.0beta3'
try:
SUPERUSERS: List[int] = [int(s) for s in DRIVER.config.superusers]

View File

@ -7,7 +7,7 @@ from pywebio.session import run_asyncio_coroutine
from LittlePaimon.utils import logger
from LittlePaimon.database.models import LastQuery, PrivateCookie
from LittlePaimon.utils.genshin_api import get_bind_game_info, get_stoken_by_cookie
from LittlePaimon.utils.api import get_bind_game_info, get_stoken_by_cookie
css_style = 'body {background: #000000 url(https://static.cherishmoon.fun/blog/h-wallpaper/zhounianqing.png);} #input-container {background: rgba(0,0,0,0);} summary {background-color: rgba(255,255,255,1);} .markdown-body {background-color: rgba(255,255,255,1);}'

View File

@ -328,6 +328,7 @@
"天目影打刀": "UI_EquipIcon_Sword_Bakufu",
"辰砂之纺锤": "UI_EquipIcon_Sword_Opus",
"笼钓瓶一心": "UI_EquipIcon_Sword_Youtou",
"原木刀": "UI_EquipIcon_Sword_Arakalari",
"「一心传」名刀": "UI_EquipIcon_Sword_YoutouEnchanted",
"风鹰剑": "UI_EquipIcon_Sword_Falcon",
"天空之刃": "UI_EquipIcon_Sword_Dvalin",
@ -341,6 +342,7 @@
"铁影阔剑": "UI_EquipIcon_Claymore_Glaive",
"沐浴龙血的剑": "UI_EquipIcon_Claymore_Siegfry",
"白铁大剑": "UI_EquipIcon_Claymore_Tin",
"石英大剑": "UI_EquipIcon_Claymore_Quartz",
"以理服人": "UI_EquipIcon_Claymore_Reasoning",
"飞天大御剑": "UI_EquipIcon_Claymore_Mitsurugi",
"西风大剑": "UI_EquipIcon_Claymore_Zephyrus",
@ -357,6 +359,7 @@
"衔珠海皇": "UI_EquipIcon_Claymore_MillenniaTuna",
"桂木斩长正": "UI_EquipIcon_Claymore_Bakufu",
"恶王丸": "UI_EquipIcon_Claymore_Maria",
"森林王器": "UI_EquipIcon_Claymore_Arakalari",
"天空之傲": "UI_EquipIcon_Claymore_Dvalin",
"狼的末路": "UI_EquipIcon_Claymore_Wolfmound",
"松籁响起之时": "UI_EquipIcon_Claymore_Widsith",
@ -367,6 +370,7 @@
"白缨枪": "UI_EquipIcon_Pole_Ruby",
"钺矛": "UI_EquipIcon_Pole_Halberd",
"黑缨枪": "UI_EquipIcon_Pole_Noire",
"「旗杆」": "UI_EquipIcon_Pole_Flagpole",
"匣里灭辰": "UI_EquipIcon_Pole_Stardust",
"试作星镰": "UI_EquipIcon_Pole_Proto",
"流月针": "UI_EquipIcon_Pole_Exotic",
@ -379,6 +383,7 @@
"喜多院十文字": "UI_EquipIcon_Pole_Bakufu",
"「渔获」": "UI_EquipIcon_Pole_Mori",
"断浪长鳍": "UI_EquipIcon_Pole_Maria",
"贯月矢": "UI_EquipIcon_Pole_Arakalari",
"护摩之杖": "UI_EquipIcon_Pole_Homa",
"天空之脊": "UI_EquipIcon_Pole_Dvalin",
"贯虹之槊": "UI_EquipIcon_Pole_Kunwu",
@ -392,6 +397,7 @@
"异世界行记": "UI_EquipIcon_Catalyst_Lightnov",
"翡玉法球": "UI_EquipIcon_Catalyst_Jade",
"甲级宝珏": "UI_EquipIcon_Catalyst_Phoney",
"琥珀玥": "UI_EquipIcon_Catalyst_Amber",
"西风秘典": "UI_EquipIcon_Catalyst_Zephyrus",
"流浪乐章": "UI_EquipIcon_Catalyst_Troupe",
"祭礼残章": "UI_EquipIcon_Catalyst_Fossil",
@ -406,6 +412,7 @@
"嘟嘟可故事集": "UI_EquipIcon_Catalyst_Ludiharpastum",
"白辰之环": "UI_EquipIcon_Catalyst_Bakufu",
"证誓之明瞳": "UI_EquipIcon_Catalyst_Jyanome",
"盈满之实": "UI_EquipIcon_Catalyst_Arakalari",
"天空之卷": "UI_EquipIcon_Catalyst_Dvalin",
"四风原典": "UI_EquipIcon_Catalyst_Fourwinds",
"尘世之锁": "UI_EquipIcon_Catalyst_Kunwu",
@ -418,6 +425,7 @@
"反曲弓": "UI_EquipIcon_Bow_Curve",
"弹弓": "UI_EquipIcon_Bow_Sling",
"信使": "UI_EquipIcon_Bow_Msg",
"黑檀弓": "UI_EquipIcon_Bow_Hardwood",
"西风猎弓": "UI_EquipIcon_Bow_Zephyrus",
"绝弦": "UI_EquipIcon_Bow_Troupe",
"祭礼弓": "UI_EquipIcon_Bow_Fossil",
@ -428,17 +436,22 @@
"黑岩战弓": "UI_EquipIcon_Bow_Blackrock",
"苍翠猎弓": "UI_EquipIcon_Bow_Viridescent",
"暗巷猎手": "UI_EquipIcon_Bow_Outlaw",
"落霞": "UI_EquipIcon_Bow_Fallensun",
"幽夜华尔兹": "UI_EquipIcon_Bow_Nachtblind",
"风花之颂": "UI_EquipIcon_Bow_Fleurfair",
"破魔之弓": "UI_EquipIcon_Bow_Bakufu",
"掠食者": "UI_EquipIcon_Bow_Predator",
"曚云之月": "UI_EquipIcon_Bow_Maria",
"王下近侍": "UI_EquipIcon_Bow_Arakalari",
"竭泽": "UI_EquipIcon_Bow_Fin",
"天空之翼": "UI_EquipIcon_Bow_Dvalin",
"阿莫斯之弓": "UI_EquipIcon_Bow_Amos",
"终末嗟叹之诗": "UI_EquipIcon_Bow_Widsith",
"冬极白星": "UI_EquipIcon_Bow_Worldbane",
"若水": "UI_EquipIcon_Bow_Kirin",
"飞雷之弦振": "UI_EquipIcon_Bow_Narukami",
"落霞": "UI_EquipIcon_Bow_Fallensun"
"猎人之径": "UI_EquipIcon_Bow_Ayus",
"(test)竿测试": "UI_EquipIcon_FishingRod",
"(test)穿模测试": "UI_EquipIcon_Bow_Template"
}
}

View File

@ -1,61 +1,8 @@
from typing import List, Dict, Literal, DefaultDict, Optional
from typing import Literal, DefaultDict
from collections import defaultdict
from pydantic import BaseModel
# class Ban(BaseModel):
# """插件屏蔽列表"""
# all_groups: bool = False
# """屏蔽所有群组"""
# group: List[int] = []
# """屏蔽群组列表"""
# all_privates: bool = False
# """屏蔽所有群组"""
# private: List[int] = []
# """屏蔽私聊列表"""
# group_member: List[str] = []
# """屏蔽群组中特定成员列表"""
# all_guilds: bool = False
# """屏蔽所有频道"""
# guild: List[int] = []
# """屏蔽频道列表"""
# guild_channel: List[str] = []
# """屏蔽频道中特定子频道列表"""
# invert_selection: Dict[Literal['group', 'private', 'group_member', 'guild', 'guild_channel'], bool] = {
# 'group': False,
# 'private': False,
# 'group_member': False,
# 'guild': False,
# 'guild_channel': False}
# """是否反选"""
#
# def check(self, event: MessageEvent) -> bool:
# if isinstance(event, GroupMessageEvent):
# if self.all_groups:
# return False
# if event.group_id in self.group:
# return self.invert_selection['group']
# if f'{event.group_id}_{event.user_id}' in self.group_member:
# return self.invert_selection['group_member']
# return True
# elif isinstance(event, PrivateMessageEvent):
# if self.all_privates:
# return False
# if event.user_id in self.private:
# return self.invert_selection['private']
# return True
# elif event.message_type == 'guild':
# if self.all_guilds:
# return False
# if event.guild_id in self.guild:
# return self.invert_selection['guild']
# if f'{event.guild_id}_{event.channel_id}' in self.guild_channel:
# return self.invert_selection['guild_channel']
# return True
# else:
# raise TypeError(f'{event.message_type} is not supported by plugin manager')
class Statistics(BaseModel):
"""
插件调用统计

View File

@ -35,6 +35,7 @@ GACHA_RES = RESOURCE_BASE_PATH / 'gacha_res'
GACHA_SIM = USER_DATA_PATH / 'gacha_sim_data'
# 原神抽卡记录数据路径
GACHA_LOG = USER_DATA_PATH / 'gacha_log_data'
GACHA_LOG.mkdir(parents=True, exist_ok=True)
# 字体路径
FONTS_PATH = Path() / 'resources' / 'fonts'
FONTS_PATH.mkdir(parents=True, exist_ok=True)

View File

@ -68,14 +68,12 @@ class PluginManager:
self.save()
return f'成功设置{config_name}{value}'
async def init_plugins(self):
plugin_list = nb_plugin.get_loaded_plugins()
group_list = await get_bot().get_group_list()
user_list = await get_bot().get_friend_list()
for plugin in plugin_list:
if plugin.name not in hidden_plugins and not await PluginPermission.filter(name=plugin.name).exists():
logger.info('插件管理器', f'新纳入插件<m>{plugin.name}</m>进行权限管理')
if plugin.name not in hidden_plugins:
await asyncio.gather(*[PluginPermission.update_or_create(name=plugin.name, session_id=group['group_id'],
session_type='group') for group in group_list])
await asyncio.gather(*[PluginPermission.update_or_create(name=plugin.name, session_id=user['user_id'],

View File

@ -6,7 +6,7 @@ from collections import defaultdict
from LittlePaimon.database.models import PrivateCookie, MihoyoBBSSub
from LittlePaimon.utils import logger, aiorequests
from LittlePaimon.utils import scheduler
from LittlePaimon.utils.genshin_api import random_text, random_hex, get_old_version_ds, get_ds
from LittlePaimon.utils.api import random_text, random_hex, get_old_version_ds, get_ds
from LittlePaimon.manager.plugin_manager import plugin_manager as pm
# 米游社的API列表

View File

@ -7,7 +7,7 @@ from typing import Tuple
from LittlePaimon import DRIVER
from LittlePaimon.database.models import MihoyoBBSSub
from LittlePaimon.utils import logger, scheduler
from LittlePaimon.utils.genshin_api import get_mihoyo_private_data, get_sign_reward_list
from LittlePaimon.utils.api import get_mihoyo_private_data, get_sign_reward_list
from LittlePaimon.manager.plugin_manager import plugin_manager as pm
from .draw import SignResult, draw_result

View File

@ -12,7 +12,7 @@ from LittlePaimon import NICKNAME
from LittlePaimon.database.models import LastQuery, PrivateCookie, PublicCookie, Character, PlayerInfo, GeneralSub, \
DailyNoteSub, MihoyoBBSSub
from LittlePaimon.utils import logger
from LittlePaimon.utils.genshin_api import get_bind_game_info, get_stoken_by_cookie
from LittlePaimon.utils.api import get_bind_game_info, get_stoken_by_cookie
from LittlePaimon.utils.message import recall_message
from LittlePaimon.manager.plugin_manager import plugin_manager as pm

View File

@ -10,7 +10,7 @@ from nonebot.params import Arg, Depends
from LittlePaimon.database.models import DailyNoteSub, Player
from LittlePaimon.utils import logger, scheduler
from LittlePaimon.utils.genshin_api import get_mihoyo_private_data
from LittlePaimon.utils.api import get_mihoyo_private_data
from LittlePaimon.manager.plugin_manager import plugin_manager as pm
from .draw import draw_daily_note_card

View File

@ -1,120 +0,0 @@
import time
import xlsxwriter
from LittlePaimon.config import GACHA_LOG
from .meta_data import *
def id_generator():
id = 1000000000000000000
while True:
id = id + 1
yield str(id)
def convertUIGF(gachaLog, uid):
if 'gachaLog' in gachaLog:
gachaLog = gachaLog['gachaLog']
UIGF_data = {"info": {}}
UIGF_data["info"]["uid"] = uid
UIGF_data["info"]["lang"] = "zh-cn"
UIGF_data["info"]["export_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
UIGF_data["info"]["export_app"] = "genshin-gacha-export"
UIGF_data["info"]["export_app_version"] = 'v2.5.0.02221942'
UIGF_data["info"]["uigf_version"] = "v2.2"
UIGF_data["info"]["export_timestamp"] = int(time.time())
all_gachaDictList = []
for gacha_type in gachaQueryTypeIds:
gacha_log = gachaLog.get(gacha_type, [])
gacha_log = sorted(gacha_log, key=lambda gacha: gacha["time"], reverse=True)
gacha_log.reverse()
for gacha in gacha_log:
gacha["uigf_gacha_type"] = gacha_type
all_gachaDictList.extend(gacha_log)
all_gachaDictList = sorted(all_gachaDictList, key=lambda gacha: gacha["time"])
id = id_generator()
for gacha in all_gachaDictList:
if gacha.get("id", "") == "":
gacha["id"] = next(id)
all_gachaDictList = sorted(all_gachaDictList, key=lambda gacha: gacha["id"])
UIGF_data["list"] = all_gachaDictList
return UIGF_data
def writeXLSX(uid, gachaLog, gachaTypeIds):
t = time.strftime("%Y%m%d%H%M%S", time.localtime())
workbook = xlsxwriter.Workbook(GACHA_LOG / f"gachaExport-{uid}.xlsx")
first_row = 1 # 不包含表头第一行 (zero indexed)
first_col = 0 # 第一列
for id in gachaTypeIds:
gachaDictList = gachaLog[id]
gachaTypeName = gachaQueryTypeDict[id]
gachaDictList.reverse()
worksheet = workbook.add_worksheet(gachaTypeName)
content_css = workbook.add_format(
{"align": "left", "font_name": "微软雅黑", "border_color": "#c4c2bf", "bg_color": "#ebebeb", "border": 1})
title_css = workbook.add_format(
{"align": "left", "font_name": "微软雅黑", "color": "#757575", "bg_color": "#dbd7d3", "border_color": "#c4c2bf",
"border": 1, "bold": True})
excel_header = ["时间", "名称", "类别", "星级", "祈愿类型", "总次数", "保底内"]
worksheet.set_column("A:A", 22)
worksheet.set_column("B:B", 14)
worksheet.set_column("E:E", 14)
worksheet.write_row(0, 0, excel_header, title_css)
worksheet.freeze_panes(1, 0)
counter = 0
pity_counter = 0
for gacha in gachaDictList:
time_str = gacha["time"]
name = gacha["name"]
item_type = gacha["item_type"]
rank_type = gacha["rank_type"]
gacha_type = gacha["gacha_type"]
uid = gacha["uid"]
gacha_type_name = gacha_type_dict.get(gacha_type, "")
counter = counter + 1
pity_counter = pity_counter + 1
excel_data = [time_str, name, item_type, rank_type, gacha_type_name, counter, pity_counter]
excel_data[3] = int(excel_data[3])
worksheet.write_row(counter, 0, excel_data, content_css)
if excel_data[3] == 5:
pity_counter = 0
star_5 = workbook.add_format({"color": "#bd6932", "bold": True})
star_4 = workbook.add_format({"color": "#a256e1", "bold": True})
star_3 = workbook.add_format({"color": "#8e8e8e"})
last_row = len(gachaDictList) # 最后一行
last_col = len(excel_header) - 1 # 最后一列zero indexed 所以要减 1
worksheet.conditional_format(first_row, first_col, last_row, last_col,
{"type": "formula", "criteria": "=$D2=5", "format": star_5})
worksheet.conditional_format(first_row, first_col, last_row, last_col,
{"type": "formula", "criteria": "=$D2=4", "format": star_4})
worksheet.conditional_format(first_row, first_col, last_row, last_col,
{"type": "formula", "criteria": "=$D2=3", "format": star_3})
worksheet = workbook.add_worksheet("原始数据")
raw_data_header = ["count", "gacha_type", "id", "item_id", "item_type", "lang", "name", "rank_type", "time", "uid",
"uigf_gacha_type"]
worksheet.write_row(0, 0, raw_data_header)
UIGF_data = convertUIGF(gachaLog, uid)
all_gachaDictList = UIGF_data["list"]
for all_counter, gacha in enumerate(all_gachaDictList):
count = gacha.get("count", "")
gacha_type = gacha.get("gacha_type", "")
id = gacha.get("id", "")
item_id = gacha.get("item_id", "")
item_type = gacha.get("item_type", "")
lang = gacha.get("lang", "")
name = gacha.get("name", "")
rank_type = gacha.get("rank_type", "")
time_str = gacha.get("time", "")
uid = gacha.get("uid", "")
uigf_gacha_type = gacha.get("uigf_gacha_type", "")
excel_data = [count, gacha_type, id, item_id, item_type, lang, name, rank_type, time_str, uid, uigf_gacha_type]
worksheet.write_row(all_counter + 1, 0, excel_data)
workbook.close()

View File

@ -1,113 +1,61 @@
import json
import re
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, GroupMessageEvent
from nonebot.params import CommandArg, Arg
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.plugin import PluginMetadata
from LittlePaimon.config import GACHA_LOG
from LittlePaimon.utils.files import load_json, save_json
from LittlePaimon.utils import logger
from LittlePaimon.utils.message import CommandPlayer
from .api import toApi, checkApi
from .gacha_logs import get_data
from .get_img import get_gacha_log_img
from .data_source import get_gacha_log_img, get_gacha_log_data
__plugin_meta__ = PluginMetadata(
name="原神抽卡记录分析",
description="小派蒙的原神抽卡记录模块",
usage=(
"1.[获取抽卡记录 (uid) (url)]提供url获取原神抽卡记录需要一定时间"
"2.[查看抽卡记录 (uid)]查看抽卡记录分析"
"3.[导出抽卡记录 (uid) (xlsx/json)]导出抽卡记录文件,上传到群文件中"
),
usage='',
extra={
'type': '原神抽卡记录',
"author": "惜月 <277073121@qq.com>",
"version": "0.1.3",
'type': '原神抽卡记录',
"author": "惜月 <277073121@qq.com>",
"version": "3.0.0",
'priority': 10,
},
)
gacha_log_export = on_command('ckjldc', aliases={'抽卡记录导出', '导出抽卡记录'}, priority=12, block=True, state={
'pm_name': '抽卡记录导出',
'pm_description': '将抽卡记录导出到群文件中',
'pm_usage': '抽卡记录导出(uid)[xlsx/json]',
'pm_priority': 3
})
gacha_log_update = on_command('ckjlgx', aliases={'抽卡记录更新', '更新抽卡记录', '获取抽卡记录'}, priority=12, block=True, state={
'pm_name': '获取抽卡记录',
'pm_description': '从抽卡链接获取抽卡记录,链接可以通过祈愿页面断网取得',
'pm_usage': '获取抽卡记录(uid)<链接>',
'pm_priority': 1
})
gacha_log_show = on_command('ckjl', aliases={'抽卡记录', '查看抽卡记录'}, priority=12, block=True, state={
'pm_name': '查看抽卡记录',
'pm_description': '查看你的抽卡记录分析',
'pm_usage': '查看抽卡记录',
'pm_priority': 2
})
update_log = on_command('更新抽卡记录', aliases={'抽卡记录更新', '获取抽卡记录'}, priority=12, block=True, state={
'pm_name': '更新抽卡记录',
'pm_description': '*通过stoken更新原神抽卡记录',
'pm_usage': '更新抽卡记录(uid)',
'pm_priority': 1
})
show_log = on_command('查看抽卡记录', aliases={'抽卡记录'}, priority=12, block=True, state={
'pm_name': '查看抽卡记录',
'pm_description': '*查看你的抽卡记录分析',
'pm_usage': '查看抽卡记录(uid)',
'pm_priority': 2
})
running_update = []
@gacha_log_export.handle()
async def ckjl(bot: Bot, event: GroupMessageEvent, player=CommandPlayer(1), msg: str = Arg('msg')):
player = player[0]
if match := re.search(r'(?P<filetype>xlsx|json)', msg):
filetype = match['filetype']
@update_log.handle()
async def _(event: MessageEvent, player=CommandPlayer(1)):
if f'{player[0].user_id}-{player[0].uid}' in running_update:
await update_log.finish(f'UID{player[0].uid}已经在获取抽卡记录中,请勿重复发送')
else:
filetype = 'xlsx'
filetype = f'gachaExport-{player.uid}.xlsx' if filetype == 'xlsx' else f'UIGF_gachaData-{player.uid}.json'
local_data = GACHA_LOG / filetype
if not local_data.exists():
await gacha_log_export.finish('你在派蒙这里还没有抽卡记录哦,使用 更新抽卡记录 吧!', at_sender=True)
else:
await bot.upload_group_file(group_id=event.group_id, file=local_data, name=filetype)
running_update.append(f'{player[0].user_id}-{player[0].uid}')
await update_log.send(f'开始为UID{player[0].uid}更新抽卡记录,请稍候...')
try:
result = await get_gacha_log_data(player[0].user_id, player[0].uid)
await update_log.send(result, at_sender=True)
except Exception as e:
logger.info('原神抽卡记录', f'➤➤更新抽卡记录时出现错误:<r>{e}</r>')
await update_log.send(f'更新抽卡记录时出现错误:{e}')
running_update.remove(f'{player[0].user_id}-{player[0].uid}')
@gacha_log_update.handle()
async def update_ckjl(event: MessageEvent, msg: Message = CommandArg()):
url = None
if msg := msg.extract_plain_text().strip():
if log_url := re.search(r'(https://webstatic.mihoyo.com/.*#/log)', msg):
url = log_url[1]
msg = msg.replace(url, '')
if not url:
await gacha_log_update.finish('你这个抽卡链接不对哦应该是以https://开头、#/log结尾的', at_sender=True)
user_data = load_json(GACHA_LOG / 'gacha_log_url.json')
if not url and str(event.user_id) in user_data:
url = user_data[str(event.user_id)]
await gacha_log_update.send('发现历史抽卡记录链接,尝试使用...')
else:
await gacha_log_update.finish('拿到游戏抽卡记录链接后,对派蒙说[获取抽卡记录 uid 链接]就可以啦\n获取抽卡记录链接的方式和vx小程序的是一样的还请旅行者自己搜方法',
at_sender=True)
if str(event.user_id) not in user_data:
user_data[str(event.user_id)] = url
save_json(user_data, path=GACHA_LOG / 'gacha_log_url.json')
url = toApi(url)
apiRes = await checkApi(url)
if apiRes != 'OK':
await gacha_log_update.finish(apiRes, at_sender=True)
await gacha_log_update.send('抽卡记录开始获取,请给派蒙一点时间...')
uid = await get_data(url)
local_data = GACHA_LOG / f'gachaData-{uid}.json'
gacha_data = load_json(local_data)
gacha_img = await get_gacha_log_img(gacha_data, 'all')
await gacha_log_update.finish(gacha_img, at_sender=True)
@gacha_log_show.handle()
async def get_ckjl(event: MessageEvent, player=CommandPlayer(1), msg: str = Arg('msg')):
player = player[0]
if pool_type := re.search(r'(all|角色|武器|常驻|新手)', msg):
pool_type = pool_type[1]
else:
pool_type = 'all'
local_data = GACHA_LOG / f'gachaData-{player.uid}.json'
if not local_data.exists():
await gacha_log_update.finish('你在派蒙这里还没有抽卡记录哦,对派蒙说 获取抽卡记录 吧!', at_sender=True)
with open(local_data, 'r', encoding="utf-8") as f:
gacha_data = json.load(f)
gacha_img = await get_gacha_log_img(gacha_data, pool_type)
await gacha_log_update.finish(gacha_img, at_sender=True)
@show_log.handle()
async def _(event: MessageEvent, player=CommandPlayer(1)):
logger.info('原神抽卡记录', '', {'用户': player[0].user_id, 'UID': player[0].uid}, '开始绘制抽卡记录图片', True)
try:
result = await get_gacha_log_img(player[0].user_id, player[0].uid, event.sender.nickname)
await show_log.finish(result, at_sender=True)
except Exception as e:
logger.info('原神抽卡记录', f'➤➤绘制抽卡记录图片时出现错误:<r>{e}</r>')
await update_log.finish(f'绘制抽卡记录分析时出现错误:{e}')

View File

@ -1,59 +0,0 @@
from urllib import parse
from LittlePaimon.utils import aiorequests
def toApi(url):
spliturl = str(url).replace('amp;', '').split("?")
if "webstatic-sea" in spliturl[0] or "hk4e-api-os" in spliturl[0]:
spliturl[0] = "https://hk4e-api-os.mihoyo.com/event/gacha_info/api/getGachaLog"
else:
spliturl[0] = "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog"
url = "?".join(spliturl)
return url
def getApi(url, gachaType, size, page, end_id=""):
parsed = parse.urlparse(url)
querys = parse.parse_qsl(str(parsed.query))
param_dict = dict(querys)
param_dict["size"] = size
param_dict["gacha_type"] = gachaType
param_dict["page"] = page
param_dict["lang"] = "zh-cn"
param_dict["end_id"] = end_id
param = parse.urlencode(param_dict)
path = str(url).split("?")[0]
return f"{path}?{param}"
async def checkApi(url):
try:
j = await aiorequests.get(url=url)
j = j.json()
except Exception as e:
return f'API请求解析出错{e}'
if not j["data"]:
if j["message"] == "authkey error":
return "authkey错误请重新获取链接给派蒙"
elif j["message"] == "authkey timeout":
return "authkey已过期请重新获取链接给派蒙"
else:
return f'数据为空,错误代码:{j["message"]}'
return 'OK'
def getQueryVariable(url, variable):
query = str(url).split("?")[1]
vars = query.split("&")
return next((v.split("=")[1] for v in vars if v.split("=")[0] == variable), "")
async def getGachaInfo(url):
region = getQueryVariable(url, "region")
lang = getQueryVariable(url, "lang")
gachaInfoUrl = f"https://webstatic.mihoyo.com/hk4e/gacha_info/{region}/items/{lang}.json"
resp = await aiorequests.get(url=gachaInfoUrl)
return resp.json()

View File

@ -0,0 +1,137 @@
import asyncio
import time
import datetime
from typing import Dict, Union, Tuple
from LittlePaimon import DRIVER
from LittlePaimon.database.models import PlayerInfo
from LittlePaimon.config import GACHA_LOG
from LittlePaimon.utils.api import get_authkey_by_stoken
from LittlePaimon.utils import aiorequests, logger
from LittlePaimon.utils.files import load_json
from .models import GachaItem, GachaLogInfo, GACHA_TYPE_LIST
from .draw import draw_gacha_log
GACHA_LOG_API = 'https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog'
HEADERS: Dict[str, str] = {
'x-rpc-app_version': '2.11.1',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 ('
'KHTML, like Gecko) miHoYoBBS/2.11.1',
'x-rpc-client_type': '5',
'Referer': 'https://webstatic.mihoyo.com/',
'Origin': 'https://webstatic.mihoyo.com',
}
PARAMS: Dict[str, Union[str, int]] = {
'authkey_ver': '1',
'sign_type': '2',
'auth_appid': 'webview_gacha',
'init_type': '200',
'gacha_id': 'fecafa7b6560db5f3182222395d88aaa6aaac1bc',
'lang': 'zh-cn',
'device_type': 'mobile',
'plat_type': 'ios',
'game_biz': 'hk4e_cn',
'size': '20',
}
def load_history_info(user_id: str, uid: str) -> Tuple[GachaLogInfo, bool]:
"""
读取历史抽卡记录数据
:param user_id: 用户id
:param uid: 原神uid
:return: 抽卡记录数据
"""
file_path = GACHA_LOG / f'gacha_log-{user_id}-{uid}.json'
if file_path.exists():
return GachaLogInfo.parse_obj(load_json(file_path)), True
else:
return GachaLogInfo(user_id=user_id,
uid=uid,
update_time=datetime.datetime.now()), False
def save_gacha_log_info(user_id: str, uid: str, info: GachaLogInfo):
"""
保存抽卡记录数据
:param user_id: 用户id
:param uid: 原神uid
:param info: 抽卡记录数据
"""
save_path = GACHA_LOG / f'gacha_log-{user_id}-{uid}.json'
save_path_bak = GACHA_LOG / f'gacha_log-{user_id}-{uid}.json.bak'
# 将旧数据备份一次
if save_path.exists():
if save_path_bak.exists():
save_path_bak.unlink()
save_path.rename(save_path.parent / f'{save_path.name}.bak')
# 写入新数据
with save_path.open('w', encoding='utf-8') as f:
f.write(info.json(ensure_ascii=False, indent=4))
async def get_gacha_log_data(user_id: str, uid: str):
"""
使用authkey获取抽卡记录数据并合并旧数据
:param user_id: 用户id
:param uid: 原神uid
:return: 更新结果
"""
new_num = 0
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
authkey, state, cookie_info = await get_authkey_by_stoken(user_id, uid)
if not state:
return authkey
gacha_log, _ = load_history_info(user_id, uid)
params = PARAMS.copy()
params['region'] = server_id
params['authkey'] = authkey
logger.info('原神抽卡记录', '', {'用户': user_id, 'UID': uid}, '开始更新抽卡记录', True)
for pool_id, pool_name in GACHA_TYPE_LIST.items():
params['gacha_type'] = pool_id
end_id = 0
for page in range(1, 999):
params['page'] = page
params['end_id'] = end_id
params['timestamp'] = str(int(time.time()))
data = await aiorequests.get(url=GACHA_LOG_API,
headers=HEADERS,
params=params)
data = data.json()
if 'data' not in data or 'list' not in data['data']:
logger.info('原神抽卡记录', '➤➤', {}, 'Stoken已失效更新失败', False)
cookie_info.stoken = None
await cookie_info.save()
return f'UID{uid}的Stoken已失效请重新绑定后再更新抽卡记录'
data = data['data']['list']
if not data:
break
for item in data:
item_info = GachaItem.parse_obj(item)
if item_info not in gacha_log.item_list[pool_name]:
gacha_log.item_list[pool_name].append(item_info)
new_num += 1
end_id = data[-1]['id']
await asyncio.sleep(1)
logger.info('原神抽卡记录', f'➤➤<m>{pool_name}</m>', {}, '获取完成', True)
for i in gacha_log.item_list.values():
i.sort(key=lambda x: x.time)
gacha_log.update_time = datetime.datetime.now()
save_gacha_log_info(user_id, uid, gacha_log)
if new_num == 0:
return f'UID{uid}更新完成,本次没有新增数据'
else:
return f'UID{uid}更新完成,本次共新增{new_num}条抽卡记录'
async def get_gacha_log_img(user_id: str, uid: str, nickname: str):
data, state = load_history_info(user_id, uid)
if not state:
return f'UID{uid}还没有抽卡记录数据,请先更新'
player_info = await PlayerInfo.get_or_none(user_id=user_id, uid=uid)
if player_info:
return await draw_gacha_log(player_info.user_id, player_info.uid, player_info.nickname, player_info.signature, data)
else:
return await draw_gacha_log(user_id, uid, nickname, None, data)

View File

@ -0,0 +1,209 @@
import asyncio
import datetime
import math
from typing import Tuple, List, Optional
from LittlePaimon.config import RESOURCE_BASE_PATH
from LittlePaimon.utils.files import load_image
from LittlePaimon.utils.message import MessageBuild
from LittlePaimon.utils.image import PMImage, get_qq_avatar, font_manager as fm
from .models import GachaLogInfo, FiveStarItem, FourStarItem
avatar_point = [69, 156, 259, 358, 456, 558, 645, 746, 840, 945]
line_point = [88, 182, 282, 378, 477, 574, 673, 769, 864, 967]
bar_color = [('#b6d6f2', '#3d6e99'), ('#c8b6f2', '#593d99'), ('#abede0', '#3a9382')]
name_level_color = ['#ff3600', '#ff7800', '#ffb400', 'black']
small_avatar_cache = {}
avatar_cache = {}
async def get_avatar(qid: str, size: Tuple[int, int] = (146, 146)) -> PMImage:
avatar = await get_qq_avatar(qid)
await avatar.resize(size)
await avatar.to_circle('circle')
await avatar.add_border(6, '#ddcdba', 'circle')
return avatar
async def small_avatar(info: FiveStarItem):
if info.name in small_avatar_cache:
return small_avatar_cache[info.name]
bg = PMImage(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'small_circle.png'))
img = PMImage(
await load_image(RESOURCE_BASE_PATH / ('avatar' if info.type == '角色' else 'weapon') / f'{info.icon}.png',
size=(42, 42)))
await img.to_circle('circle')
await bg.paste(img.image, (2, 2))
small_avatar_cache[info.name] = bg
return bg
async def detail_avatar(info: FiveStarItem):
if info.name in avatar_cache:
bg = avatar_cache[info.name]
else:
bg = PMImage(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'item_avatar_5.png'))
img = PMImage(
await load_image(RESOURCE_BASE_PATH / ('avatar' if info.type == '角色' else 'weapon') / f'{info.icon}.png',
size=(123, 123)))
await img.to_circle('circle')
await bg.paste(img.image, (14, 26))
await bg.text(info.name, (0, bg.width), 162, fm.get('hywh', 24),
'#ff3600' if info.name not in {'迪卢克', '刻晴', '莫娜', '七七', ''} else '#33231a', 'center')
avatar_cache[info.name] = bg.copy()
if info.count < (20 if info.type == '角色' else 15):
color = name_level_color[0]
elif (20 if info.type == '角色' else 15) <= info.count < (40 if info.type == '角色' else 30):
color = name_level_color[1]
elif (40 if info.type == '角色' else 30) <= info.count < (70 if info.type == '角色' else 60):
color = name_level_color[2]
else:
color = name_level_color[3]
await bg.text(str(info.count), 144, 6, fm.get('bahnschrift_regular', 48, 'Bold'), color, 'right')
return bg
async def draw_pool_detail(pool_name: str, data: List[FiveStarItem], total_count: int, not_out: int) -> Optional[
PMImage]:
if not data:
return None
total_height = 181 + (446 if len(data) > 3 else 0) + 47 + 204 * math.ceil(len(data) / 6) + 20 + 60
img = PMImage(size=(1009, total_height), mode='RGBA', color=(255, 255, 255, 0))
# 橙线
await img.paste(await load_image(RESOURCE_BASE_PATH / 'general' / 'line.png'), (0, 0))
await img.text(f'{pool_name[:2]}卡池', 25, 11, fm.get('hywh', 30), 'white')
# 数据
await img.text('平均出货', 174, 142, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(round((total_count - not_out) / len(data), 2)), (176, 270), 84,
fm.get('bahnschrift_regular', 48, 'Regular'),
'#252525', 'center')
await img.text('总抽卡数', 471, 142, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(total_count), (472, 567), 84, fm.get('bahnschrift_regular', 48, 'Regular'),
'#252525', 'center')
await img.text('未出五星', 753, 142, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(not_out), (755, 848), 84, fm.get('bahnschrift_regular', 48, 'Regular'),
'#252525', 'center')
# 折线图
if len(data) > 3:
last_point = None
await img.paste(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'broken_line_bg.png'), (1, 181))
for chara in data:
height = int(583 - (chara.count / 90) * 340)
if last_point:
await img.draw_line(last_point, (line_point[data.index(chara)], height), '#ff6f30', 4)
last_point = (line_point[data.index(chara)], height)
for chara in data:
height = int(583 - (chara.count / 90) * 340)
point = avatar_point[data.index(chara)]
await img.paste(await small_avatar(chara), (point, height - 23))
await img.text(str(chara.count), (point, point + 44), height - 48, fm.get('bahnschrift_regular', 30,
'Regular'), '#040404', 'center')
# 详细数据统计
chara_bg = PMImage(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'detail_bg.png'))
await chara_bg.stretch((47, chara_bg.height - 20), 204 + 204 * (len(data) // 6), 'height')
await img.paste(chara_bg, (1, 655 if len(data) > 3 else 181))
await asyncio.gather(
*[img.paste(await detail_avatar(chara),
(18 + data.index(chara) % 6 * 163, (708 if len(data) > 3 else 234) + data.index(chara) // 6 * 204))
for chara in data])
return img
async def draw_four_star(info: FourStarItem) -> PMImage:
bg = PMImage(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'item_avatar_4.png'))
img = PMImage(
await load_image(RESOURCE_BASE_PATH / ('avatar' if info.type == '角色' else 'weapon') / f'{info.icon}.png',
size=(123, 123)))
await img.to_circle('circle')
await bg.paste(img, (34, 26))
await bg.text(info.name, (0, bg.width), 163, fm.get('hywh', 24), '#221a33', 'center')
await bg.text(str(info.num['角色祈愿']), (4, 64), 209, fm.get('bahnschrift_regular', 36, 'Bold'), '#3d6e99',
'center')
await bg.text(str(info.num['武器祈愿']), (65, 125), 209, fm.get('bahnschrift_regular', 36, 'Bold'), '#593d99',
'center')
await bg.text(str(info.num['常驻祈愿'] + info.num['新手祈愿']), (126, 186), 209, fm.get('bahnschrift_regular', 36, 'Bold'),
'#3a9381',
'center')
return bg
async def draw_four_star_detail(data: List[FourStarItem]):
bar = await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'four_star_bar.png')
total_height = 105 + 260 * math.ceil(len(data) / 5)
bg = PMImage(size=(1008, total_height), mode='RGBA', color=(255, 255, 255, 0))
await bg.paste(bar, (0, 0))
await asyncio.gather(*[bg.paste(await draw_four_star(chara), (data.index(chara) % 5 * 204, 105 + data.index(chara) // 5 * 260)) for chara in data])
return bg
async def draw_gacha_log(user_id: str, uid: str, nickname: Optional[str], signature: Optional[str], gacha_log: GachaLogInfo):
img = PMImage(size=(1080, 8000), mode='RGBA', color=(255, 255, 255, 0))
bg = PMImage(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'bg.png'))
line = await load_image(RESOURCE_BASE_PATH / 'general' / 'line.png')
# 头像
avatar = await get_avatar(user_id, (108, 108))
await img.paste(avatar, (38, 45))
# 昵称
await img.text(nickname, 166, 49, fm.get('hywh', 48), '#252525')
# 签名和uid
if signature:
await img.text(signature, 165, 116, fm.get('hywh', 32), '#252525')
nickname_length = img.text_length(nickname, fm.get('hywh', 40))
await img.text(f'UID{uid}', 166 + nickname_length + 36, 58,
fm.get('bahnschrift_regular', 48, 'Regular'),
'#252525')
else:
await img.text(f'UID{uid}', 165, 103, fm.get('bahnschrift_regular', 48, 'Regular'), '#252525')
data5, data4, data_not = gacha_log.get_statistics()
# 数据总览
await img.paste(line, (36, 181))
await img.text('数据总览', 60, 192, fm.get('hywh', 30), 'white')
await img.text(f'CREATED BY LITTLEPAIMON AT {datetime.datetime.now().strftime("%m-%d %H:%M")}', 1025, 196,
fm.get('bahnschrift_regular', 30), '#252525', 'right')
total_gacha_count = sum(len(pool) for pool in gacha_log.item_list.values())
out_gacha_count = total_gacha_count - sum(data_not.values())
total_five_star_count = sum(len(pool) for pool in data5.values())
five_star_average = round(out_gacha_count / total_five_star_count, 2) if total_five_star_count else 0
await img.text('平均出货', 209, 335, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(five_star_average), (211, 305), 286, fm.get('bahnschrift_regular', 48), '#040404', 'center')
await img.text('总抽卡数', 506, 335, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(total_gacha_count), (507, 602), 286, fm.get('bahnschrift_regular', 48), '#040404', 'center')
await img.text('总计出金', 788, 335, fm.get('hywh', 24), (24, 24, 24, 102))
await img.text(str(total_five_star_count), (789, 884), 286, fm.get('bahnschrift_regular', 48), '#040404', 'center')
four_star_detail = await draw_four_star_detail(list(data4.values()))
if total_five_star_count:
chara_pool_per = round(len(data5['角色祈愿']) / total_five_star_count, 3)
weapon_pool_per = round(len(data5['武器祈愿']) / total_five_star_count, 3)
new_pool_per = round((len(data5['常驻祈愿']) + len(data5['新手祈愿'])) / total_five_star_count, 3)
now_used_width = 56
pers = [chara_pool_per, weapon_pool_per, new_pool_per]
for per in pers:
if per >= 0.03:
await img.draw_rectangle((now_used_width, 399, now_used_width + int(per * 967), 446),
bar_color[pers.index(per)][0])
if per >= 0.1:
await img.text(f'{per * 100}%', now_used_width + 18, 410, fm.get('bahnschrift_regular', 30, 'Bold'),
bar_color[pers.index(per)][1])
now_used_width += int(per * 967)
await img.paste(await load_image(RESOURCE_BASE_PATH / 'gacha_log' / 'text.png'), (484, 464))
now_height = 525
for pool_name, data in data5.items():
pool_detail_img = await draw_pool_detail(pool_name, data, len(gacha_log.item_list[pool_name]),
data_not[pool_name])
if pool_detail_img:
await img.paste(pool_detail_img, (36, now_height))
now_height += pool_detail_img.height
now_height += 10
await img.paste(four_star_detail, (36, now_height))
now_height += four_star_detail.height + 30
await img.crop((0, 0, 1080, now_height))
await bg.stretch((50, bg.height - 50), now_height - 100, 'height')
else:
await img.paste(four_star_detail, (36, 410))
await img.crop((0, 0, 1080, 410 + four_star_detail.height + 30))
await bg.stretch((50, bg.height - 50), img.height - 100, 'height')
await bg.paste(img, (0, 0))
return MessageBuild.Image(bg, mode='RGB', quality=80)

View File

@ -1,83 +0,0 @@
from asyncio import sleep
from pathlib import Path
from LittlePaimon.utils import aiorequests
from LittlePaimon.utils.files import load_json, save_json
from .UIGF_and_XLSX import convertUIGF, writeXLSX
from .api import getApi
from .meta_data import gachaQueryTypeIds, gachaQueryTypeDict
data_path = Path() / 'data' / 'LittlePaimon' / 'user_data' / 'gacha_log_data'
async def getGachaLogs(url, gachaTypeId):
size = "20"
# api限制一页最大20
gachaList = []
end_id = "0"
for page in range(1, 9999):
api = getApi(url, gachaTypeId, size, page, end_id)
resp = await aiorequests.get(url=api)
j = resp.json()
gacha = j["data"]["list"]
if not len(gacha):
break
gachaList.extend(iter(gacha))
end_id = j["data"]["list"][-1]["id"]
await sleep(0.5)
return gachaList
def mergeDataFunc(localData, gachaData):
for banner in gachaQueryTypeDict:
bannerLocal = localData["gachaLog"][banner]
bannerGet = gachaData["gachaLog"][banner]
if bannerGet != bannerLocal:
flaglist = [1] * len(bannerGet)
loc = [[i["time"], i["name"]] for i in bannerLocal]
for i in range(len(bannerGet)):
gachaGet = bannerGet[i]
get = [gachaGet["time"], gachaGet["name"]]
if get not in loc:
flaglist[i] = 0
tempData = []
for i in range(len(bannerGet)):
if flaglist[i] == 0:
gachaGet = bannerGet[i]
tempData.insert(0, gachaGet)
for i in tempData:
localData["gachaLog"][banner].insert(0, i)
return localData
async def get_data(url):
gachaData = {"gachaLog": {}}
for gachaTypeId in gachaQueryTypeIds:
gachaLog = await getGachaLogs(url, gachaTypeId)
gachaData["gachaLog"][gachaTypeId] = gachaLog
uid_flag = 1
for gachaType in gachaData["gachaLog"]:
for log in gachaData["gachaLog"][gachaType]:
if uid_flag and log["uid"]:
gachaData["uid"] = log["uid"]
uid_flag = 0
uid = gachaData["uid"]
localDataFilePath = data_path / f"gachaData-{uid}.json"
if localDataFilePath.is_file():
localData = load_json(localDataFilePath)
mergeData = mergeDataFunc(localData, gachaData)
else:
mergeData = gachaData
mergeData["gachaType"] = gachaQueryTypeDict
# 写入json
save_json(mergeData, localDataFilePath)
# 写入UIGF、json
UIGF_data = convertUIGF(mergeData['gachaLog'], uid)
save_json(UIGF_data, data_path / f"UIGF_gachaData-{uid}.json")
# 写入xlsx
writeXLSX(uid, mergeData['gachaLog'], gachaQueryTypeIds)
return uid

View File

@ -1,145 +0,0 @@
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from LittlePaimon.utils.alias import get_short_name
from LittlePaimon.utils.message import MessageBuild
res_path = Path() / 'resources' / 'LittlePaimon'
def get_font(size):
return ImageFont.truetype(str(res_path / 'msyh.ttc'), size)
async def get_circle_avatar(avatar, size):
avatar = Image.open(res_path / 'thumb' / f'{avatar}.png')
w, h = avatar.size
bg = Image.new('RGBA', (w, h), (213, 153, 77, 255))
bg.alpha_composite(avatar, (0, 0))
bg = bg.resize((size, size))
scale = 5
mask = Image.new('L', (size * scale, size * scale), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, size * scale, size * scale), fill=255)
mask = mask.resize((size, size), Image.ANTIALIAS)
ret_img = bg.copy()
ret_img.putalpha(mask)
return ret_img
async def sort_data(gacha_data):
sprog_data = {'type': '新手', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
permanent_data = {'type': '常驻', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
role_data = {'type': '角色', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
weapon_data = {'type': '武器', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
new_gacha_data = [sprog_data, permanent_data, role_data, weapon_data]
i = 0
for pool in gacha_data['gachaLog'].values():
pool.reverse()
new_gacha_data[i]['total_num'] = len(pool)
for p in pool:
if p['rank_type'] == "5":
new_gacha_data[i]['5_star'].append((p['name'], new_gacha_data[i]['5_gacha'] + 1))
new_gacha_data[i]['5_gacha'] = 0
new_gacha_data[i]['4_gacha'] = 0
elif p['rank_type'] == "4":
new_gacha_data[i]['4_star'].append((p['name'], new_gacha_data[i]['4_gacha'] + 1))
new_gacha_data[i]['5_gacha'] += 1
new_gacha_data[i]['4_gacha'] = 0
else:
new_gacha_data[i]['5_gacha'] += 1
new_gacha_data[i]['4_gacha'] += 1
i += 1
return new_gacha_data
async def draw_gacha_log(data):
if data['total_num'] == 0:
return None
top = Image.open(res_path / 'player_card' / 'gacha_log_top.png')
mid = Image.open(res_path / 'player_card' / '卡片身体.png').resize((768, 80))
bottom = Image.open(res_path / 'player_card' / '卡片底部.png').resize((768, 51))
five_star = data['5_star']
col = int(len(five_star) / 6)
if not len(five_star) % 6 == 0:
col += 1
top_draw = ImageDraw.Draw(top)
top_draw.text((348, 30), f'{data["type"]}', font=get_font(24), fill='#F8F5F1')
top_draw.text((145 - 6 * len(str(data["total_num"])), 88), f'{data["total_num"]}', font=get_font(24), fill='black')
five_ave = round(sum([x[1] for x in five_star]) / len(five_star), 1) if five_star else ' '
top_draw.text((321 - 10 * len(str(five_ave)), 88), f'{five_ave}', font=get_font(24),
fill='black' if five_ave != ' ' and five_ave > 60 else 'red')
five_per = round(len(five_star) / (data['total_num'] - data['5_gacha']) * 100, 2) if five_star else -1
five_per_str = str(five_per) + '%' if five_per > -1 else ' '
top_draw.text((427, 88), f'{five_per_str}', font=get_font(24), fill='black' if five_per < 1.7 else 'red')
five_up = round(len([x[0] for x in five_star if not x[0] in ['刻晴', '迪卢克', '七七', '莫娜', '']]) / len(five_star) * 100,
1) if five_star else -1
five_up_str = str(five_up) + '%' if five_per > -1 else ' '
top_draw.text((578 if len(five_up_str) != 6 else 569, 88), f'{five_up_str}', font=get_font(24),
fill='black' if five_up < 75 else 'red')
most_five = sorted(five_star, key=lambda x: x[1], reverse=False)[0][0] if five_star else ' '
top_draw.text((152 - 14 * len(most_five), 163), f'{most_five}', font=get_font(24), fill='red')
four_ave = round(sum([x[1] for x in data['4_star']]) / len(data['4_star']), 1) if data['4_star'] else ' '
top_draw.text((316 - 10 * len(str(four_ave)), 163), f'{four_ave}', font=get_font(24),
fill='black' if four_ave != ' ' and four_ave > 7 else 'red')
top_draw.text((461 - 6 * len(str(data['5_gacha'])), 163), f'{data["5_gacha"]}', font=get_font(24), fill='black')
top_draw.text((604, 163), f'{data["4_gacha"]}', font=get_font(24), fill='black')
bg_img = Image.new('RGBA', (768, 288 + col * 80 - (20 if col > 0 else 0) + 51), (0, 0, 0, 0))
bg_img.paste(top, (0, 0))
for i in range(0, col):
bg_img.paste(mid, (0, 288 + i * 80))
bg_img.paste(bottom, (0, 288 + col * 80 - (20 if col > 0 else 0)))
bg_draw = ImageDraw.Draw(bg_img)
n = 0
for c in five_star:
avatar = await get_circle_avatar(c[0], 45)
f = 10 if data['type'] == '武器' else 0
if c[1] <= 20:
color = 'red'
elif 20 < c[1] <= 50 - f:
color = 'orangered'
elif 50 - f < c[1] < 70 - f:
color = 'darkorange'
else:
color = 'black'
bg_img.alpha_composite(avatar, (30 + 120 * (n % 6), 298 + 80 * int(n / 6)))
name = get_short_name(c[0])
bg_draw.text((111 + 120 * (n % 6) - 8 * len(name), 298 + 80 * int(n / 6)), name, font=get_font(16), fill=color)
bg_draw.text((107 - 5 * len(str(c[1])) + 120 * (n % 6), 317 + 80 * int(n / 6)), f'[{c[1]}]', font=get_font(16),
fill=color)
n += 1
return bg_img
async def get_gacha_log_img(gacha_data, pool):
all_gacha_data = await sort_data(gacha_data)
if pool != 'all':
img = None
for pd in all_gacha_data:
if pd['type'] == pool:
img = await draw_gacha_log(pd)
break
if not img:
return '这个池子没有抽卡记录哦'
total_height = img.size[1]
else:
img_list = []
total_height = 0
now_height = 0
for pd in all_gacha_data:
p_img = await draw_gacha_log(pd)
if p_img:
img_list.append(p_img)
total_height += p_img.size[1]
if not img_list:
return '没有找到任何抽卡记录诶!'
img = Image.new('RGBA', (768, total_height), (0, 0, 0, 255))
for i in img_list:
img.paste(i, (0, now_height))
now_height += i.size[1]
img_draw = ImageDraw.Draw(img)
img_draw.text((595, 44), f'UID:{gacha_data["uid"]}', font=get_font(16), fill='black')
img_draw.text((530, total_height - 45), 'Created by LittlePaimon', font=get_font(16), fill='black')
return MessageBuild.Image(img, mode='RGB')

View File

@ -1,10 +0,0 @@
gachaQueryTypeIds = ["100", "200", "301", "302"]
gachaQueryTypeNames = ["新手祈愿", "常驻祈愿", "角色活动祈愿", "武器活动祈愿"]
gachaQueryTypeDict = dict(zip(gachaQueryTypeIds, gachaQueryTypeNames))
gacha_type_dict = {
"100": "新手祈愿",
"200": "常驻祈愿",
"301": "角色活动祈愿",
"302": "武器活动祈愿",
"400": "角色活动祈愿-2",
}

View File

@ -0,0 +1,81 @@
import datetime
from typing import List, Dict, Union, Tuple
from pydantic import BaseModel
from LittlePaimon.utils.alias import get_chara_icon, get_weapon_icon
GACHA_TYPE_LIST = {'100': '新手祈愿', '200': '常驻祈愿', '302': '武器祈愿', '301': '角色祈愿', '400': '角色祈愿'}
class FiveStarItem(BaseModel):
name: str
icon: str
count: int
type: str
class FourStarItem(BaseModel):
name: str
icon: str
type: str
num: Dict[str, int] = {
'角色祈愿': 0,
'武器祈愿': 0,
'常驻祈愿': 0,
'新手祈愿': 0}
class GachaItem(BaseModel):
id: str
name: str
gacha_type: str
item_type: str
rank_type: str
time: datetime.datetime
class GachaLogInfo(BaseModel):
user_id: str
uid: str
update_time: datetime.datetime
item_list: Dict[str, List[GachaItem]] = {
'角色祈愿': [],
'武器祈愿': [],
'常驻祈愿': [],
'新手祈愿': [],
}
def get_statistics(self) -> Tuple[Dict[str, List[FiveStarItem]], Dict[str, FourStarItem], Dict[str, int]]:
gacha_data_five: Dict[str, List[FiveStarItem]] = {
'角色祈愿': [],
'武器祈愿': [],
'常驻祈愿': [],
'新手祈愿': [],
}
gacha_data_four: Dict[str, FourStarItem] = {}
gacha_not_out: Dict[str, int] = {}
for pool_name, item_list in self.item_list.items():
count_now = 0
for item in item_list:
if item.rank_type == '5':
gacha_data_five[pool_name].append(
FiveStarItem(
name=item.name,
icon=get_chara_icon(name=item.name) if item.item_type == '角色' else get_weapon_icon(
item.name),
count=count_now + 1,
type=item.item_type))
count_now = 0
else:
count_now += 1
if item.rank_type == '4':
if item.name in gacha_data_four:
gacha_data_four[item.name].num[pool_name] += 1
else:
gacha_data_four[item.name] = FourStarItem(
name=item.name,
icon=get_chara_icon(name=item.name) if item.item_type == '角色' else get_weapon_icon(
item.name),
type=item.item_type)
gacha_data_four[item.name].num[pool_name] = 1
gacha_not_out[pool_name] = count_now
return gacha_data_five, gacha_data_four, gacha_not_out

View File

@ -58,35 +58,35 @@ async def draw_abyss_card(info: AbyssInfo):
# 最多击破
await img.text(str(info.max_defeat.value), (370, 473), 357, fm.get('bahnschrift_regular.ttf', 56), '#40342d',
'center')
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{info.max_defeat.name}.png', size=(96, 96)))
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{info.max_defeat.icon}.png', size=(96, 96)))
await chara_img.to_circle('circle')
await img.paste(chara_img, (373, 248))
# 战技次数
await img.text(str(info.max_normal_skill.value), (532, 635), 357, fm.get('bahnschrift_regular.ttf', 56), '#40342d',
'center')
chara_img = PMImage(
await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{info.max_normal_skill.name}.png', size=(96, 96)))
await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{info.max_normal_skill.icon}.png', size=(96, 96)))
await chara_img.to_circle('circle')
await img.paste(chara_img, (536, 248))
# 爆发次数
await img.text(str(info.max_energy_skill.value), (693, 796), 357, fm.get('bahnschrift_regular.ttf', 56), '#40342d',
'center')
chara_img = PMImage(
await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{info.max_energy_skill.name}.png', size=(96, 96)))
await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{info.max_energy_skill.icon}.png', size=(96, 96)))
await chara_img.to_circle('circle')
await img.paste(chara_img, (696, 248))
# 最深抵达
await img.text(str(info.max_floor), (838, 1038), 298, fm.get('bahnschrift_regular.ttf', 60), '#40342d', 'center')
# 最强一击
circle = await load_image(RESOURCE_BASE_PATH / 'general' / 'orange_circle.png')
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{info.max_damage.name}.png', size=(205, 205)))
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{info.max_damage.icon}.png', size=(205, 205)))
await chara_img.to_circle('circle')
await img.text('最强一击', 270, 520, fm.get('SourceHanSansCN-Bold.otf', 48), '#40342d')
await img.text(str(info.max_damage.value), 270, 590, fm.get('bahnschrift_bold.ttf', 72, 'Bold'), '#40342d')
await img.paste(circle, (46, 485))
await img.paste(chara_img, (49, 488))
# 最多承伤
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{info.max_take_damage.name}.png', size=(205, 205)))
chara_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{info.max_take_damage.icon}.png', size=(205, 205)))
await chara_img.to_circle('circle')
await img.text('最多承伤', 791, 520, fm.get('SourceHanSansCN-Bold.otf', 48), '#40342d')
await img.text(str(info.max_take_damage.value), 791, 590, fm.get('bahnschrift_bold.ttf', 72, 'Bold'), '#40342d')
@ -113,7 +113,7 @@ async def draw_abyss_card(info: AbyssInfo):
await load_image(RESOURCE_BASE_PATH / 'icon' / f'star{character.rarity}.png', size=(95, 95)),
(192 + (j % 4) * 103, 832 + (i - 9) * 194))
await img.paste(
await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{character.name}.png', size=(95, 95)),
await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{character.icon}.png', size=(95, 95)),
(192 + (j % 4) * 103, 832 + (i - 9) * 194))
await img.draw_rounded_rectangle2((192 + (j % 4) * 103, 903 + (i - 9) * 194), (30, 23), 10,
'#333333',
@ -128,7 +128,7 @@ async def draw_abyss_card(info: AbyssInfo):
await load_image(RESOURCE_BASE_PATH / 'icon' / f'star{character.rarity}.png', size=(95, 95)),
(637 + (j % 4) * 103, 832 + (i - 9) * 194))
await img.paste(
await load_image(RESOURCE_BASE_PATH / 'thumb' / f'{character.name}.png', size=(95, 95)),
await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{character.icon}.png', size=(95, 95)),
(637 + (j % 4) * 103, 832 + (i - 9) * 194))
await img.draw_rounded_rectangle2((637 + (j % 4) * 103, 903 + (i - 9) * 194), (30, 23), 10,
'#333333',

View File

@ -6,6 +6,7 @@ from typing import List
from LittlePaimon.config import RESOURCE_BASE_PATH
from LittlePaimon.database.models import Character, PlayerInfo, Player
from LittlePaimon.utils.files import load_image
from LittlePaimon.utils.alias import get_chara_icon
from LittlePaimon.utils.genshin import GenshinTools
from LittlePaimon.utils.image import PMImage, font_manager as fm
from LittlePaimon.utils.message import MessageBuild
@ -13,7 +14,6 @@ from .draw_player_card import get_avatar, draw_weapon_icon
RESOURCES = RESOURCE_BASE_PATH / 'chara_bag'
ICON = RESOURCE_BASE_PATH / 'icon'
THUMB = RESOURCE_BASE_PATH / 'thumb'
ARTIFACT_ICON = RESOURCE_BASE_PATH / 'artifact'
talent_color = [('#d5f2b6', '#6d993d'), ('#d5f2b6', '#6d993d'), ('#d5f2b6', '#6d993d'),
@ -68,7 +68,7 @@ async def draw_chara_card(info: Character) -> PMImage:
:return: 角色卡片图
"""
# 头像
avatar = PMImage(await load_image(THUMB / f'{info.name}.png'))
avatar = PMImage(await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{get_chara_icon(name=info.name)}.png'))
await avatar.to_circle('circle')
await avatar.resize((122, 122))
# 背景

View File

@ -5,7 +5,7 @@ from LittlePaimon.utils import load_image
from LittlePaimon.utils.genshin import GenshinTools
from LittlePaimon.utils.image import PMImage, font_manager as fm
from LittlePaimon.utils.message import MessageBuild
from LittlePaimon.utils.alias import get_icon
from LittlePaimon.utils.alias import get_chara_icon
from LittlePaimon.database.models import Character
from .damage_cal import get_role_dmg
@ -34,10 +34,12 @@ async def draw_chara_detail(uid: str, info: Character):
await img.paste(dmg_img, (42, 1820))
# 立绘
chara_img = await load_image(RESOURCE_BASE_PATH / 'splash' / f'{get_icon(chara_id=info.character_id, icon_type="splash")}.png')
chara_img = await load_image(RESOURCE_BASE_PATH / 'splash' / f'{get_chara_icon(chara_id=info.character_id, icon_type="splash")}.png')
if chara_img.height >= 630:
chara_img = chara_img.resize((chara_img.width * 630 // chara_img.height, 630))
await img.paste(chara_img, (770 - chara_img.width // 2, 20))
else:
chara_img = chara_img.resize((chara_img.width, chara_img.height * 630 // chara_img.height))
await img.paste(chara_img, (770 - chara_img.width // 2, 20))
await img.paste(await load_image(ENKA_RES / '底遮罩.png'), (0, 0))
# 地区
if info.name not in ['', '', '埃洛伊']:

View File

@ -4,12 +4,12 @@ from typing import List, Tuple, Optional
from LittlePaimon.config import RESOURCE_BASE_PATH
from LittlePaimon.database.models import PlayerInfo, Character, PlayerWorldInfo, Weapon, Player
from LittlePaimon.utils.files import load_image
from LittlePaimon.utils.alias import get_chara_icon
from LittlePaimon.utils.image import PMImage, get_qq_avatar, font_manager as fm
from LittlePaimon.utils.message import MessageBuild
RESOURCES = RESOURCE_BASE_PATH / 'player_card'
ICON = RESOURCE_BASE_PATH / 'icon'
THUMB = RESOURCE_BASE_PATH / 'thumb'
WEAPON = RESOURCE_BASE_PATH / 'weapon'
@ -44,7 +44,7 @@ async def draw_character_card(info: Character) -> Optional[PMImage]:
if info is None:
return None
# 头像
avatar = PMImage(await load_image(THUMB / f'{info.name}.png'))
avatar = PMImage(await load_image(RESOURCE_BASE_PATH / 'avatar' / f'{get_chara_icon(name=info.name)}.png'))
await avatar.to_circle('circle')
await avatar.resize((122, 122))
# 背景

View File

@ -1,5 +1,5 @@
from LittlePaimon.database.models import Player
from LittlePaimon.utils.genshin_api import get_mihoyo_private_data
from LittlePaimon.utils.api import get_mihoyo_private_data
from LittlePaimon.utils import logger
from .draw import draw_monthinfo_card

View File

@ -2,7 +2,7 @@ from PIL import Image, ImageDraw, ImageFont
from LittlePaimon.utils.files import load_image
from LittlePaimon.utils.image import PMImage, font_manager as fm
from LittlePaimon.utils.alias import get_icon
from LittlePaimon.utils.alias import get_chara_icon
from LittlePaimon.utils.message import MessageBuild
from LittlePaimon.config import RESOURCE_BASE_PATH
from .abyss_rate_data import get_rate, get_formation_rate
@ -29,7 +29,7 @@ async def draw_rate_rank(type: str = 'role'):
font=fm.get('msyh.ttc', 35))
for n, role in enumerate(data['result']['rateList']):
role_card = PMImage(size=(160, 200), color=(200, 200, 200, 255), mode='RGBA')
role_img = await load_image(RESOURCE_BASE_PATH / 'avatar_card' / f'{get_icon(name=role["name"], icon_type="card")}.png', size=(160, 160))
role_img = await load_image(RESOURCE_BASE_PATH / 'avatar_card' / f'{get_chara_icon(name=role["name"], icon_type="card")}.png', size=(160, 160))
await role_card.paste(role_img, (0, 0))
await role_card.text((28 if len(role['rate']) == 6 else 38, 158), role['rate'], font=fm.get('msyh.ttc', 30),
color='black')

View File

@ -6,6 +6,7 @@ from LittlePaimon.config import JSON_DATA
alias_file = load_json(JSON_DATA / 'alias.json')
info_file = load_json(JSON_DATA / 'genshin_info.json')
weapon_file = load_json(JSON_DATA / 'weapon.json')
def get_short_name(name: str) -> str:
@ -52,7 +53,8 @@ def get_alias_by_name(name: str) -> List[str]:
return next((r for r in name_list.values() if name in r), None)
def get_match_alias(msg: str, type: Literal['角色', '武器', '原魔', '圣遗物'] = '角色', single_to_dict: bool = False) -> Union[str, list, dict]:
def get_match_alias(msg: str, type: Literal['角色', '武器', '原魔', '圣遗物'] = '角色', single_to_dict: bool = False) -> Union[
str, list, dict]:
"""
根据字符串消息获取与之相似或匹配的角色武器原魔名
:param msg: 消息
@ -72,7 +74,8 @@ def get_match_alias(msg: str, type: Literal['角色', '武器', '原魔', '圣
elif match_list:
possible[alias[0]] = role_id
if len(possible) == 1:
return {list(possible.keys())[0]: possible[list(possible.keys())[0]]} if single_to_dict else list(possible.keys())[0]
return {list(possible.keys())[0]: possible[list(possible.keys())[0]]} if single_to_dict else \
list(possible.keys())[0]
return possible
elif type in {'武器', '圣遗物'}:
possible = []
@ -88,8 +91,8 @@ def get_match_alias(msg: str, type: Literal['角色', '武器', '原魔', '圣
return match_list[0] if len(match_list) == 1 else match_list
def get_icon(name: Optional[str] = None, chara_id: Optional[int] = None,
icon_type: Literal['avatar', 'card', 'splash', 'slice', 'side'] = 'avatar') -> Optional[str]:
def get_chara_icon(name: Optional[str] = None, chara_id: Optional[int] = None,
icon_type: Literal['avatar', 'card', 'splash', 'slice', 'side'] = 'avatar') -> Optional[str]:
"""
根据角色名字或id获取角色的图标
:param name: 角色名
@ -114,3 +117,7 @@ def get_icon(name: Optional[str] = None, chara_id: Optional[int] = None,
elif icon_type == 'slice':
return side_icon.replace('_Side', '').replace('UI_', 'UI_Gacha_')
def get_weapon_icon(name: str) -> Optional[str]:
icon_list = weapon_file['Icon']
return icon_list.get(name)

View File

@ -4,7 +4,7 @@ import re
import string
import time
import json
from typing import Optional, Literal
from typing import Optional, Literal, Union, Tuple
from LittlePaimon.utils import logger
from nonebot import logger as nb_logger
@ -25,6 +25,7 @@ GAME_RECORD_API = 'https://api-takumi-record.mihoyo.com/game_record/card/wapi/ge
SIGN_INFO_API = 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/info'
SIGN_REWARD_API = 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/home'
SIGN_ACTION_API = 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/sign'
AUTHKEY_API = 'https://api-takumi.mihoyo.com/binding/api/genAuthKey'
def md5(text: str) -> str:
@ -173,7 +174,8 @@ async def check_retcode(data: dict, cookie_info, cookie_type: str, user_id: str,
return True
async def get_cookie(user_id: str, uid: str, check: bool = True, own: bool = False):
async def get_cookie(user_id: str, uid: str, check: bool = True, own: bool = False) -> Tuple[
Union[None, PrivateCookie, PublicCookie, CookieCache], str]:
"""
获取可用的cookie
:param user_id: 用户id
@ -222,7 +224,7 @@ async def get_mihoyo_public_data(
user_id: Optional[str],
mode: Literal['abyss', 'player_card', 'role_detail'],
schedule_type: Optional[str] = '1'):
server_id = "cn_qd01" if uid[0] == '5' else "cn_gf01"
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
check = True
while True:
cookie_info, cookie_type = await get_cookie(user_id, uid, check)
@ -270,7 +272,7 @@ async def get_mihoyo_private_data(
mode: Literal['role_skill', 'month_info', 'daily_note', 'sign_info', 'sign_action'],
role_id: Optional[str] = None,
month: Optional[str] = None):
server_id = "cn_qd01" if uid[0] == '5' else "cn_gf01"
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
cookie_info, _ = await get_cookie(user_id, uid, True, True)
if not cookie_info:
return '未绑定私人cookie获取cookie的教程\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定'
@ -361,11 +363,49 @@ async def get_stoken_by_cookie(cookie: str) -> Optional[str]:
bbs_cookie_url2 = 'https://api-takumi.mihoyo.com/auth/api/getMultiTokenByLoginTicket?login_ticket={}&token_types=3&uid={}'
data2 = (await aiorequests.get(url=bbs_cookie_url2.format(login_ticket[0].split('=')[1], stuid))).json()
return data2['data']['list'][0]['token']
else:
return None
return None
async def get_authkey_by_stoken(user_id: str, uid: str) -> Tuple[Optional[str], bool, Optional[PrivateCookie]]:
"""
根据stoken获取authkey
:param user_id: 用户id
:param uid: 原神uid
:return: authkey
"""
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
cookie_info, _ = await get_cookie(user_id, uid, True, True)
if not cookie_info:
return '未绑定私人cookie获取cookie的教程\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定', False, cookie_info
if not cookie_info.stoken:
return 'cookie中没有stoken字段请重新绑定', False, cookie_info
headers = {
'Cookie': cookie_info.stoken,
'DS': get_old_version_ds(True),
'User-Agent': 'okhttp/4.8.0',
'x-rpc-app_version': '2.35.2',
'x-rpc-sys_version': '12',
'x-rpc-client_type': '5',
'x-rpc-channel': 'mihoyo',
'x-rpc-device_id': random_hex(32),
'x-rpc-device_name': random_text(random.randint(1, 10)),
'x-rpc-device_model': 'Mi 10',
'Referer': 'https://app.mihoyo.com',
'Host': 'api-takumi.mihoyo.com'}
data = await aiorequests.post(url=AUTHKEY_API,
headers=headers,
json={
'auth_appid': 'webview_gacha',
'game_biz': 'hk4e_cn',
'game_uid': uid,
'region': server_id})
data = data.json()
if 'data' in data and 'authkey' in data['data']:
return data['data']['authkey'], True, cookie_info
else:
return None, False, cookie_info
async def get_enka_data(uid):
try:
url = f'https://enka.network/u/{uid}/__data.json'

View File

@ -10,7 +10,7 @@ from LittlePaimon.database.models import PlayerInfo, Character, LastQuery, Priva
from LittlePaimon.database.models import Artifact, CharacterProperty, Artifacts, Talents, Talent
from LittlePaimon.utils import logger, scheduler
from LittlePaimon.utils.files import load_json
from LittlePaimon.utils.genshin_api import get_enka_data, get_mihoyo_public_data, get_mihoyo_private_data
from LittlePaimon.utils.api import get_enka_data, get_mihoyo_public_data, get_mihoyo_private_data
from LittlePaimon.utils.typing import DataSourceType
from LittlePaimon.utils.alias import get_name_by_id
from LittlePaimon.utils.typing import CHARACTERS