Created
October 18, 2021 03:08
-
-
Save monsterxcn/9f7b188583eb0ff7b13c6b4cae2b8660 to your computer and use it in GitHub Desktop.
PIL 绘制原神实时便笺草稿(原谅我两空格缩进还手搓 CQ 码
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import base64 | |
import hashlib | |
import json | |
import os | |
import sys | |
import time | |
from io import BytesIO | |
from random import randint | |
import nonebot | |
from httpx import AsyncClient | |
from nonebot.log import logger | |
from PIL import Image, ImageDraw, ImageFont | |
resPath = nonebot.get_driver().config.resources_dir | |
if not resPath: | |
raise ValueError(f'请在环境变量中添加 resources_dir 参数') | |
# 检查 UID 是否合法 | |
def uidChecker(uid): | |
try: | |
uid = str(int(uid)) | |
if len(uid) == 9: | |
# 判断 9 位 UID 首位得到所在服务器 | |
if uid[0] == "1" or uid[0] == "2": | |
return uid, "cn_gf01" | |
if uid[0] == "5": | |
return uid, "cn_qd01" | |
elif len(uid) < 9: | |
# 少于 9 位 UID 自动补成官服形式 | |
uid = str(int(uid.zfill(9)) + 100000000) | |
return uid, "cn_gf01" | |
except: | |
pass | |
# 输入 UID 不合法返回所在服务器为空 | |
return uid, None | |
# 计算文本 MD5 字符串 | |
def md5(text): | |
md5 = hashlib.md5() | |
md5.update(text.encode()) | |
return md5.hexdigest() | |
# 设置 Pillow 绘制字体 | |
def font(size): | |
return ImageFont.truetype(f'{resPath}info/zh-cn.ttf', size=size) | |
# 转换 Image 对象图片为 Base64 编码字符串 | |
def img2Base64(pic: Image.Image) -> str: | |
buf = BytesIO() | |
pic.save(buf, format="PNG", quality=100) | |
base64_str = base64.b64encode(buf.getbuffer()).decode() | |
return "base64://" + base64_str | |
# 转换网络图片为 Image 对象 | |
# 探索派遣的小头像会缓存到 {resPath}ck/character_side_icon/ | |
async def pic2Image(url): | |
localPic = url.replace("https://upload-bbs.mihoyo.com/game_record/genshin/", f"{resPath}ck/") | |
# 本地文件存在,读取 | |
if os.path.exists(localPic): | |
userImage = Image.open(localPic) | |
# 本地文件不存在,从服务器下载并保存 | |
else: | |
async with AsyncClient() as client: | |
try: | |
res = await client.get(url, timeout=20.0) | |
userImage = Image.open(BytesIO(res.content)) | |
userImage.save(localPic, quality=100) | |
except Exception as e: | |
logger.error(str(sys.exc_info()[0]) + '\n' + str(e)) | |
return None | |
# 返回 URL 图片的 Image 对象 | |
return userImage | |
# 绘制实时便笺模版 | |
async def drawNotifyTpl(): | |
## 创建新图片、选择模板 | |
result = Image.new("RGBA", (600, 780), "#f1eae2") | |
txtDraw = ImageDraw.Draw(result) | |
## 绘制可莉图标 | |
avatar = Image.open(f"{resPath}ck/keli.150.png") | |
avatar = avatar.resize((100, 100), Image.ANTIALIAS).convert("RGBA") | |
avatar.save(f"{resPath}ck/keli.100.png", format="PNG", quality=100) | |
result.paste(avatar, (30, 30), avatar) | |
## 绘制原粹树脂,540*100(400*100 + 140*100) | |
startHeight = 150 | |
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0) | |
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0) | |
icon = Image.open(f"{resPath}ck/Item_Fragile_Resin-2.png") | |
icon = icon.resize((50, 50), Image.ANTIALIAS).convert("RGBA") | |
icon.save(f"{resPath}ck/Item_Fragile_Resin.50.png", format="PNG", quality=100) | |
result.paste(icon, (45, startHeight + 25), icon) | |
txtDraw.text((110, startHeight + 20), "原粹树脂", font=font(26), fill="#4f4f4f") | |
## 绘制每日委托,540*100(400*100 + 140*100) | |
startHeight += 100+30 | |
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0) | |
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0) | |
icon = Image.open(f"{resPath}ck/Icon_Commission.png") | |
icon = icon.resize((40, 40), Image.ANTIALIAS).convert("RGBA") | |
icon.save(f"{resPath}ck/Icon_Commission.50.png", format="PNG", quality=100) | |
result.paste(icon, (50, startHeight + 30), icon) | |
txtDraw.text((110, startHeight + 20), "每日委托", font=font(26), fill="#4f4f4f") | |
## 绘制每周消耗减半,540*100(400*100 + 140*100) | |
startHeight += 100+30 | |
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0) | |
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0) | |
icon = Image.open(f"{resPath}ck/45px-秘境logo.png") | |
icon = icon.resize((40, 40), Image.ANTIALIAS).convert("RGBA") | |
icon.save(f"{resPath}ck/Icon_Domain.40.png", format="PNG", quality=100) | |
result.paste(icon, (50, startHeight + 30), icon) | |
txtDraw.text((110, startHeight + 20), "消耗减半", font=font(26), fill="#4f4f4f") | |
## 绘制探索派遣,540*210 | |
startHeight += 100+30 | |
pureBg = ((30, startHeight), (30 + 540 - 1, startHeight + 210 - 1)) | |
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0) | |
txtDraw.text((60, startHeight + 20), "探索派遣", font=font(26), fill="#4f4f4f") | |
result.save(f"{resPath}ck/NotifyTpl.png", quality=100) | |
# 绘制实时便笺 | |
async def drawNotify(uid, jsonData): | |
## 创建新图片、选择模板 | |
resData = jsonData["data"] | |
result = Image.open(f"{resPath}ck/NotifyTpl.png") | |
txtDraw = ImageDraw.Draw(result) | |
## 绘制 UID | |
txtDraw.text((164, 64), f"UID {uid} 实时便笺", font=font(30), fill="#1d1f21") | |
# txt = "* 数据刷新可能存在一定延迟" | |
# txtDraw.text((570 - font(16).getsize(txt)[0], 110), txt, font=font(16), fill="#84603d") | |
## 绘制原粹树脂,540*100 | |
startHeight = 150 | |
if resData["current_resin"] == resData["max_resin"]: | |
txt = "原粹树脂已满,抽空清清吧" | |
else: | |
remainHour = int(resData["resin_recovery_time"]) // 3600 | |
remainMinute = int(resData["resin_recovery_time"]) % 60 | |
remainNote = f"{str(remainHour) + ' 时' if remainHour else ''}{' ' + str(remainMinute) + ' 分' if remainMinute else ''}" | |
txt = f"完全恢复还需 {remainNote}" | |
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1 | |
txt = f"{str(resData['current_resin'])} / {str(resData['max_resin'])}" | |
textWidth, textHeight = font(24).getsize(txt) | |
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855 | |
## 绘制每日委托,540*100 | |
startHeight += 130 | |
if not resData["is_extra_task_reward_received"]: | |
txt = "「每日委托」奖励未领取!" if resData["finished_task_num"] == resData["total_task_num"] else "今日完成委托数量不足" | |
else: | |
txt = "所有奖励均已领取~" | |
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1 | |
txt = f"{str(resData['finished_task_num'])} / {str(resData['total_task_num'])}" | |
textWidth, textHeight = font(24).getsize(txt) | |
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855 | |
## 绘制每周消耗减半,540*100 | |
startHeight += 130 | |
if resData["remain_resin_discount_num"]: | |
txt = "本周剩余消耗减半次数" | |
else: | |
txt = "本周消耗减半次数已耗尽~" | |
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1 | |
txt = f"{str(resData['remain_resin_discount_num'])} / 3" | |
textWidth, textHeight = font(24).getsize(txt) | |
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855 | |
## 绘制探索派遣,540*250 | |
startHeight += 130 | |
txt = f"{str(resData['current_expedition_num'])} / {str(resData['max_expedition_num'])}" | |
txtDraw.text((int(430 + (140 - font(24).getsize(txt)[0]) / 2), startHeight + 20), txt, font=font(26), fill="#84603d") #675855 | |
startWidth = 30 + 15 | |
for avatar in resData["expeditions"]: | |
charImg = await pic2Image(avatar["avatar_side_icon"]) | |
if charImg is None: | |
# 获取角色头像失败则创建与透明图片 | |
charImg = Image.new("RGBA", (90, 90), (0, 0, 0, 0)) | |
charImg = charImg.resize((90, 90), Image.ANTIALIAS).convert("RGBA") | |
result.paste(charImg, (startWidth, startHeight + 60), charImg) | |
if avatar["remained_time"] == "0": | |
txt, color = "探险完成", "#7fbb2f" | |
else: | |
remainHour = int(avatar["remained_time"]) // 3600 | |
remainMinute = int(avatar["remained_time"]) % 60 | |
txt, color = f"{str(remainHour) + 'H' if remainHour else ''}{' ' + str(remainMinute) + 'M' if remainMinute else ''}", "#675855" | |
txtWidth = font(20).getsize(txt)[0] | |
txtDraw.text((int(startWidth + (90 - txtWidth) / 2), startHeight + 60 + 90 + 15), txt, font=font(20), fill=color) | |
startWidth += 90 + 15 | |
## 返回图片 | |
# result.save(f"{resPath}ck/drawNotify.png", quality=100) | |
return result | |
# 请求米游社实时便笺接口 | |
async def queryNotify(uid, server, cookie): | |
mhyUrl = f"https://api-takumi.mihoyo.com/game_record/app/genshin/api/dailyNote?role_id={uid}&server={server}" | |
# 计算拼接请求头 DS 字段 | |
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs" | |
t = str(int(time.time())) | |
r = str(randint(100001, 200000)) | |
c = md5("salt=" + salt + "&t=" + t + "&r=" + r + "&b=" + "&q=role_id=" + uid + "&server=" + server) | |
dsHeader = t + "," + r + "," + c | |
# 组织请求的 Header 信息 | |
mhyHeaders = { | |
"Accept": "application/json, text/plain, */*", | |
"Accept-Encoding": "gzip, deflate", | |
"Accept-Language": "zh-CN,en-US;q=0.8", | |
"DS": dsHeader, | |
"x-rpc-app_version": "2.11.1", | |
"x-rpc-client_type": "5", | |
"X-Requested-With": "com.mihoyo.hyperion", | |
"Cookie": cookie, | |
"Origin": "https://webstatic.mihoyo.com", | |
"Referer": "https://webstatic.mihoyo.com/", | |
"User-Agent": "miHoYoBBS/2.11.1", | |
} | |
# 请求 | |
async with AsyncClient() as client: | |
try: | |
res = await client.get(mhyUrl, headers=mhyHeaders, timeout=10) | |
resJson = res.json() | |
# import json | |
# with open(f"{resPath}ck/res.json", "w", encoding="UTF-8") as f: | |
# json.dump(resJson, f, ensure_ascii=False, indent=2) | |
# f.close() | |
except Exception as e: | |
logger.error(str(sys.exc_info()[0]) + "\n" + str(e)) | |
# 请求米哈游 API 出错返回空 | |
return [] | |
# 返回米哈游 API 查询结果 | |
return resJson | |
# 生成实时便笺消息 | |
async def genrNotify(uid, cookie): | |
uid, server = uidChecker(uid) | |
# UID 检查不合法返回空 | |
if server == None: | |
return f"UID「{uid}」不合法!" | |
resJson = await queryNotify(uid, server, cookie) | |
try: | |
ntfImage = await drawNotify(uid, resJson) | |
b64 = img2Base64(ntfImage) | |
# return f"[CQ:image,file={b64}]" | |
return b64 | |
except (KeyError, TypeError): | |
return "米游社接口返回不完整!" | |
except Exception as e: | |
logger.error("生成实时便笺消息出错:" + str(sys.exc_info()[0]) + "\n" + str(e)) | |
return f"生成实时便笺消息出错\n{str(sys.exc_info()[0])}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
msg = await genrNotify(uid, cookie)
生成消息即可uid
是作为命令输入读进来的,cookie
貌似和原神米游社签到那些用的是一样的,网页用的可能出错图片素材传到 repo 了 https://github.com/monsterxcn/MyBucket/tree/master/Pictures/Genshin/ck