diff --git a/egenshin/README.md b/egenshin/README.md index 44c5e88..c16b116 100644 --- a/egenshin/README.md +++ b/egenshin/README.md @@ -7,6 +7,8 @@ > pip install pyyaml -i https://pypi.tuna.tsinghua.edu.cn/simple > > pip install sqlitedict -i https://pypi.tuna.tsinghua.edu.cn/simple +> +> pip install xlsxwriter -i https://pypi.tuna.tsinghua.edu.cn/simple --- @@ -132,23 +134,19 @@ ysa# | 和ys#一样 但是显示全部角色 | ysa# 原神成就查漏功能 (用ys#绑定 切换号直接查询另一个就好)
不获取游戏内任何数据,仅仅只是记录玩家完成的成就
方便查看还有什么隐藏成就尚未完成
-只有未完成的成就数量小于100时才有界面
- +仅限天地万象!!
+
+如果要删除现有的成就请使用 重置原神成就
+重置仅仅是ys#绑定的成就
+如果要查看带有攻略请使用 a原神成就
+
使用方法:
- -(方法1): 可以直接使用命令后跟n张游戏内的截图来进行更新,例如
-原神成就[完成的成就截图1][完成的成就截图2][完成的成就截图3] - -(方法2): 可以上传图床然后使用命令跟n个上传的图片地址更新,例如
-原神成就
-https://imgtu.com/i/h5Rq6x
-https://imgtu.com/i/h5RHpR
-https://imgtu.com/i/h5Rb11
- -支持的图床有
-https://imgtu.com/
-https://ibb.co/
- +
+可以直接使用命令后跟n张游戏内的截图来进行更新,例如
+原神成就[完成的成就截图1][完成的成就截图2][完成的成就截图3]
+
+*第一次需要全部截完 不用一个个发,可以一次性发完
+
命令 | 说明 | 例 ------------- | ------------- | ------------- @@ -163,4 +161,24 @@ https://ibb.co/
![image](./doc/achievements.jpeg) + + +## 原神实时便笺 + +*目前测试中
+这个功能需谨慎使用, 因为涉及到获取cookie, 不想使用可以直接删除`daily_note`文件夹
+ +命令 | 说明 | 例 +------------- | ------------- | ------------- +yss | - | - +yss# | - | - +原神状态 | - | - +原神实时 | - | - +原神便笺 | - | - + +
+返回示例 + +![image](./doc/daily_note.jpeg) +
\ No newline at end of file diff --git a/egenshin/assets/daily_note/bg.png b/egenshin/assets/daily_note/bg.png new file mode 100644 index 0000000..c7efefe Binary files /dev/null and b/egenshin/assets/daily_note/bg.png differ diff --git a/egenshin/assets/daily_note/expeditions_item.png b/egenshin/assets/daily_note/expeditions_item.png new file mode 100644 index 0000000..cea46f0 Binary files /dev/null and b/egenshin/assets/daily_note/expeditions_item.png differ diff --git a/egenshin/assets/daily_note/expeditions_item_ok.png b/egenshin/assets/daily_note/expeditions_item_ok.png new file mode 100644 index 0000000..ce6a523 Binary files /dev/null and b/egenshin/assets/daily_note/expeditions_item_ok.png differ diff --git "a/egenshin/assets/daily_note/\344\270\255\345\215\210.png" "b/egenshin/assets/daily_note/\344\270\255\345\215\210.png" new file mode 100644 index 0000000..0c00e45 Binary files /dev/null and "b/egenshin/assets/daily_note/\344\270\255\345\215\210.png" differ diff --git "a/egenshin/assets/daily_note/\345\244\234\346\231\232.png" "b/egenshin/assets/daily_note/\345\244\234\346\231\232.png" new file mode 100644 index 0000000..6dbe55c Binary files /dev/null and "b/egenshin/assets/daily_note/\345\244\234\346\231\232.png" differ diff --git "a/egenshin/assets/daily_note/\346\227\251\346\231\250.png" "b/egenshin/assets/daily_note/\346\227\251\346\231\250.png" new file mode 100644 index 0000000..a287410 Binary files /dev/null and "b/egenshin/assets/daily_note/\346\227\251\346\231\250.png" differ diff --git "a/egenshin/assets/daily_note/\351\273\204\346\230\217.png" "b/egenshin/assets/daily_note/\351\273\204\346\230\217.png" new file mode 100644 index 0000000..a4ad275 Binary files /dev/null and "b/egenshin/assets/daily_note/\351\273\204\346\230\217.png" differ diff --git a/egenshin/daily_note/__init__.py b/egenshin/daily_note/__init__.py new file mode 100644 index 0000000..415cff6 --- /dev/null +++ b/egenshin/daily_note/__init__.py @@ -0,0 +1,36 @@ +from hoshino import Service, priv, MessageSegment +from ..util import support_private +from .main import Daily_Note, Error_Message, Account_Error +from .info_card import draw_info_card + +sv_help = ''' +'''.strip() +sv = Service( + name='原神实时便笺', # 功能名 + use_priv=priv.NORMAL, # 使用权限 + manage_priv=priv.ADMIN, # 管理权限 + visible=True, # 可见性 + enable_on_default=True, # 默认启用 + bundle='娱乐', #分组归类 + help_=sv_help # 帮助说明 +) + + +@support_private(sv) +@sv.on_prefix(('yss', 'yss#', '原神状态', '原神实时', '原神便笺')) +async def main(bot, ev): + text = ev.message.extract_plain_text().strip() + cookie_raw = '' + try: + if text.startswith('绑定'): + if ev.detail_type == 'group': + raise Error_Message('请撤回, 不支持在群内绑定, 请私聊机器人') + cookie_raw = text[2:] + info = await Daily_Note(ev.user_id, cookie_raw).get_info() + im = await draw_info_card(info) + await bot.send(ev, MessageSegment.image(im), at_sender=True) + + except Error_Message as e: + await bot.send(ev, repr(e), at_sender=True) + except Account_Error as e: + await bot.send(ev, 'cookie信息错误, 请重新检查是否正确', at_sender=True) diff --git a/egenshin/daily_note/error.py b/egenshin/daily_note/error.py new file mode 100644 index 0000000..15c2ddb --- /dev/null +++ b/egenshin/daily_note/error.py @@ -0,0 +1,27 @@ +class Error_Message(Exception): + def __init__(self, message=''): + self.message = message + + def __repr__(self): + return self.message + +class Cookie_Error(Error_Message): + def __repr__(self): + t = ''' +1. 打开米游社(https://bbs.mihoyo.com/ys/) +2. 登录游戏账号 +3. F12打开控制台 +4. 输入以下代码运行 +javascript:(()=>{_=(n)=>document.cookie.match(`[;\s+]?${n}=([^;]*)`)?.pop();alert(_("account_id")+","+_("cookie_token"))})(); +5. 复制提示的内容, 私聊发给机器人 +私聊格式为 + +原神便笺绑定xxxxxxxxxxxxxx + +其中xxxxxxxxxxxxxx是复制的内容 + ''' + return t.rstrip() + + +class Login_Error(Error_Message): + pass \ No newline at end of file diff --git a/egenshin/daily_note/info_card.py b/egenshin/daily_note/info_card.py new file mode 100644 index 0000000..9c52e59 --- /dev/null +++ b/egenshin/daily_note/info_card.py @@ -0,0 +1,108 @@ +import datetime +from pathlib import Path + +from ..imghandler import * +from ..util import cache, get_font, get_path, pil2b64 +from .main import Daily_Note_Info + +default_text_color = '#78818b' +success_text_color = '#669999' + +assets_dir = Path(get_path('assets')) / 'daily_note' +box_bg = Image.open(assets_dir / "bg.png") +expedition_box = Image.open(assets_dir / "expeditions_item.png") +expedition_box_ok = Image.open(assets_dir / "expeditions_item_ok.png") + +def in_time(time_str1, time_str2): + now = datetime.datetime.now() + day = str(now.date()) + t1 = datetime.datetime.strptime(day + time_str1, '%Y-%m-%d%H:%M') + t2 = datetime.datetime.strptime(day + time_str2, '%Y-%m-%d%H:%M') + if t2 < t1: + return True + return now > t1 and now < t2 + + +async def get_time_icon(): + morning = ('6:00', '11:00', '早晨.png') + noon = ('11:00', '17:00', '中午.png') + dusk = ('17:00', '19:00', '黄昏.png') + night = ('19:00', '6:00', '夜晚.png') + for t1, t2, png in [morning, noon, dusk, night]: + if in_time(t1, t2): + return Image.open(assets_dir / png) + raise Exception('unknown time') + + +async def gen_expedition_items(expeditions): + for exp in expeditions: + remained_time = int(exp['remained_time']) + avatar = await get_pic(exp['avatar_side_icon'], (49, 60)) + + if exp['status'] == 'Finished': + bg = expedition_box_ok.copy() + s = '已完成!' + ok = True + else: + bg = expedition_box.copy() + mm, ss = divmod(remained_time, 60) + hh, mm = divmod(mm, 60) + s = "剩余%d小时%02d分%02d秒" % (hh, mm, ss) + ok = False + + draw_text_by_line(bg, (0, 49.88), s, get_font(49), ok and success_text_color or default_text_color, 881, True) + yield (avatar, bg.resize((273, 76))) + + +async def draw_info_card(info: Daily_Note_Info): + now = datetime.datetime.now() + + bg = Image.new('RGB', (box_bg.width, box_bg.height), '#f1ece6') + easy_paste(bg, box_bg.copy()) + + now_str = now.strftime(" %m月%d日\n%H:%M:%S") + draw_text_by_line(bg, (219.38, 59.79), now_str, get_font(21), default_text_color, 881) + easy_paste(bg, await get_time_icon(), (84, 30)) + + # 树脂完全回复时间 + resin_recovery_time = now + datetime.timedelta(seconds=int(info.resin_recovery_time)) + resin_recovery_time_day = resin_recovery_time.day > now.day and '明天' or '今天' + resin_recovery_str = f'将于{resin_recovery_time_day} {resin_recovery_time.strftime("%H:%M:%S")} 完全回复' + draw_text_by_line(bg, (173.09, 150.2), resin_recovery_str, get_font(16), default_text_color, 881) + + # 树脂 + resin_str = f'{info.current_resin}/{info.max_resin}' + draw_text_by_line(bg, (233.55, 196.93), resin_str, get_font(36), default_text_color, 881) + + # 每日 + ok = False + task_str = f'{info.finished_task_num}/{info.total_task_num}' + if info.finished_task_num == info.total_task_num: + task_str = '完成' + ok = True + draw_text_by_line(bg, (253, 327), task_str, get_font(36), ok and success_text_color or default_text_color, 881) + + # 周本 + ok = False + resin_discount_str = f'{info.remain_resin_discount_num}/{info.resin_discount_num_limit}' + if info.remain_resin_discount_num == 0: + resin_discount_str = '完成' + ok = True + draw_text_by_line(bg, (253, 452), resin_discount_str, get_font(36), ok and success_text_color or default_text_color, 881) + + # 探索 + first_expeditions_time = int(min([x['remained_time'] for x in info.expeditions])) + first_expeditions_time = now + datetime.timedelta(seconds=first_expeditions_time) + first_expeditions_time_day = first_expeditions_time.day > now.day and '明天' or '今天' + first_expeditions_str = f'最快{first_expeditions_time_day} {first_expeditions_time.strftime("%H:%M:%S")} 派遣完成' + draw_text_by_line(bg, (600, 74), first_expeditions_str, get_font(18), default_text_color, 881) + + bg = bg.convert("RGBA") + exp_index = 114 + async for avatar, exp_bg in gen_expedition_items(info.expeditions): + bg = easy_alpha_composite(bg, avatar, (538, exp_index)) + bg = easy_alpha_composite(bg, exp_bg, (587, exp_index)) + + exp_index += 72 + + return pil2b64(bg) diff --git a/egenshin/daily_note/main.py b/egenshin/daily_note/main.py new file mode 100644 index 0000000..b5e1b5c --- /dev/null +++ b/egenshin/daily_note/main.py @@ -0,0 +1,62 @@ +from typing import List +from dataclasses import dataclass +from http.cookies import SimpleCookie +from ..player_info import query +from .error import * + +Account_Error = query.Account_Error + + +@dataclass +class Daily_Note_expeditions: + avatar_side_icon: str # 头像icon + status: str # 'Finished' or 'Ongoing' 状态 + remained_time: str # 剩余时间 + + +@dataclass +class Daily_Note_Info: + current_resin: int # 当前树脂 + max_resin: int # 最大树脂 + resin_recovery_time: str # 树脂回复时间 + finished_task_num: int # 完成委托个数 + total_task_num: int # 全部委托个数 + is_extra_task_reward_received: bool + remain_resin_discount_num: int # 周本树脂减半剩余次数 + resin_discount_num_limit: int # 周本树脂减半次数 + current_expedition_num: int # 当前派遣数量 + max_expedition_num: int # 最大派遣数量 + expeditions: List[Daily_Note_expeditions] # 派遣列表 + + +class Daily_Note(): + def __init__(self, qid, cookie_raw=None): + self.qid = qid + + self.cookie = query.get_cookie_by_qid(qid) + + if not any([cookie_raw, self.cookie]): + raise Cookie_Error() + + self.uid = query.get_uid_by_qid(qid) + if not self.uid: + raise Error_Message('请先使用ys#绑定') + + self.cookie = SimpleCookie(self.cookie) + if cookie_raw: + self.cookie.load( + dict(zip(['account_id', 'cookie_token'], + cookie_raw.split(',')))) + + self.cookie_raw = self.cookie.output(header='', sep=';').strip() + + async def get_info(self) -> Daily_Note_Info: + json_data = await query.daily_note(self.uid, self.cookie_raw) + + if json_data.retcode == 10102: + raise Login_Error('请先在米游社角色信息那打开实时便笺功能') + if json_data.retcode != 0: + raise Login_Error(json_data.message) + + query.save_cookie(self.qid, self.cookie_raw) + return Daily_Note_Info(**json_data.data) diff --git a/egenshin/doc/daily_note.jpeg b/egenshin/doc/daily_note.jpeg new file mode 100644 index 0000000..c8a3535 Binary files /dev/null and b/egenshin/doc/daily_note.jpeg differ diff --git a/egenshin/player_info/query.py b/egenshin/player_info/query.py index f0dfb35..b615661 100644 --- a/egenshin/player_info/query.py +++ b/egenshin/player_info/query.py @@ -15,6 +15,10 @@ cookies = config.setting.cookies +class Account_Error(Exception): + pass + + def __md5__(text): _md5 = hashlib.md5() _md5.update(text.encode()) @@ -26,11 +30,15 @@ def __get_ds__(query, body=None): i = str(int(time.time())) r = ''.join(random.sample(string.ascii_lowercase + string.digits, 6)) q = '&'.join([f'{k}={v}' for k, v in query.items()]) - c = __md5__("salt=" + n + "&t=" + i + "&r=" + r + '&b=' + (body or '') + '&q=' + q) + c = __md5__("salt=" + n + "&t=" + i + "&r=" + r + '&b=' + (body or '') + + '&q=' + q) return i + "," + r + "," + c + last = {'current': 0, 'last': 0, 'all': 0} -async def request_data(uid, api='index', character_ids=None): + + +async def request_data(uid, api='index', character_ids=None, user_cookie=None): next_cookie = False now = datetime.datetime.now().timestamp() if now > config.runtime: @@ -41,10 +49,12 @@ async def request_data(uid, api='index', character_ids=None): server = 'cn_qd01' if config.use_cookie_index == len(cookies): return 'all cookie(%s) has limited' % len(cookies) - cookie = cookies[config.use_cookie_index] + cookie = user_cookie or cookies[config.use_cookie_index] account_id = SimpleCookie(cookie)['account_id'].value - print('原神UID:(%s) 当前已查询%s次, 上一个账号查询%s次, 当前第%s个账号(%s), 一共%s个账号, 调用API-> %s' % - (last['all'], last['current'] + 1, last['last'], config.use_cookie_index + 1, account_id, len(cookies), api)) + print( + '原神UID:(%s) 当前已查询%s次, 上一个账号查询%s次, 当前第%s个账号(%s), 一共%s个账号, 调用API-> %s' % + (last['all'], last['current'] + 1, last['last'], + config.use_cookie_index + 1, account_id, len(cookies), api)) headers = { 'Accept': 'application/json, text/plain, */*', @@ -73,10 +83,16 @@ async def request_data(uid, api='index', character_ids=None): json_data = {"character_ids": character_ids} json_data.update(params) params = {} + elif api == 'dailyNote': + url += urlencode(params) headers['DS'] = __get_ds__(params, json_data and json.dumps(json_data)) res = await fn(url=url, headers=headers, json=json_data) json_data = await res.json(object_hook=Dict) + + if json_data.retcode == 10104: + raise Account_Error() + if json_data.retcode == 10001: print('账号已失效 可能被修改密码, 请检查') next_cookie = True @@ -85,6 +101,9 @@ async def request_data(uid, api='index', character_ids=None): (config.use_cookie_index, cookies[config.use_cookie_index])) next_cookie = True if json_data.retcode == 10101 or next_cookie: + if user_cookie: + print('user_cookie is limited!') + raise Account_Error() print('cookie [%s] is limited!' % config.use_cookie_index) config.use_cookie_index += 1 last['last'] = last['current'] @@ -113,6 +132,10 @@ async def character(uid, character_ids): return await request_data(uid, 'character', character_ids) +async def daily_note(uid, cookie): + return await request_data(uid, 'dailyNote', user_cookie=cookie) + + class stats: def __init__(self, data, max_hide=False): self.data = data @@ -233,19 +256,11 @@ def precious_chest_str(self) -> str: @property def string(self): str_list = [ - self.active_day_str, - self.achievement_str, - self.anemoculus_str, - self.geoculus_str, - self.electroculus_str, - self.avatar_str, - self.way_point_str, - self.domain_str, - self.spiral_abyss_str, - self.luxurious_chest_str, - self.precious_chest_str, - self.exquisite_chest_str, - self.common_chest_str + self.active_day_str, self.achievement_str, self.anemoculus_str, + self.geoculus_str, self.electroculus_str, self.avatar_str, + self.way_point_str, self.domain_str, self.spiral_abyss_str, + self.luxurious_chest_str, self.precious_chest_str, + self.exquisite_chest_str, self.common_chest_str ] return '\n'.join(list(filter(None, str_list))) @@ -253,12 +268,25 @@ def string(self): db = init_db(config.cache_dir, 'uid.sqlite') +def get_db(qid): + return db.get(qid, {}) + + def get_uid_by_qid(qid): - db_info = db.get(qid, {}) - if not db_info: - return None - return db_info['uid'] + return get_db(qid).get('uid') def save_uid_by_qid(qid, uid): - db[qid] = {'uid': uid} + info = get_db(qid) + info['uid'] = uid + db[qid] = info + + +def get_cookie_by_qid(qid): + return get_db(qid).get('cookie') + + +def save_cookie(qid, cookie): + info = get_db(qid) + info['cookie'] = cookie + db[qid] = info \ No newline at end of file diff --git a/egenshin/util.py b/egenshin/util.py index 8e90aa1..1325404 100644 --- a/egenshin/util.py +++ b/egenshin/util.py @@ -11,7 +11,7 @@ import aiofiles import yaml -from hoshino import aiorequests +from hoshino import CanceledException, aiorequests, trigger, priv from nonebot import * from PIL import ImageFont from sqlitedict import SqliteDict @@ -111,11 +111,41 @@ def get_font(size, w='85'): def pil2b64(data): bio = BytesIO() data = data.convert("RGB") - data.save(bio, format='JPEG', quality=80) + data.save(bio, format='JPEG', quality=75) base64_str = base64.b64encode(bio.getvalue()).decode() return 'base64://' + base64_str +private_prefix = [] + + +# support private message +@message_preprocessor +async def handler(bot, ev, _): + if ev.detail_type != 'private': + return + for t in trigger.chain: + for service in t.find_handler(ev): + sv = service.sv + if sv in private_prefix: + if priv.get_user_priv(ev) >= priv.NORMAL: + try: + await service.func(bot, ev) + except CanceledException: + raise + sv.logger.info( + f'Private Message {ev.message_id} triggered {service.func.__name__}.' + ) + + +def support_private(sv): + def wrap(func): + private_prefix.append(sv) + return func + + return wrap + + def cache(ttl=datetime.timedelta(hours=1), arg_key=None): def wrap(func): cache_data = {}