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 = {}