Last active
June 8, 2023 06:30
-
-
Save monsterxcn/218e453d0c1018e33ecfef913dfa82ef to your computer and use it in GitHub Desktop.
NoneBot2 原神充值二维码插件,将 nonebot_plugin_gspay.py 补全后放入插件文件夹下重启 Bot 即可使用
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
""" | |
需要绑定个人 Cookie 的充值插件,不再即插即用,使用前必须自行修改 Cookie 处理等代码,溜了 | |
哦对了,由于我的机机在境外,访问米哈游接口需要梯子,代码里 httpx.AsyncClient 都设置了代理 | |
境内机机使用 / 没有代理的话,务必记得删除文件中的 proxies={"all://": "socks5://127.0.0.1:1080"} | |
参考 https://github.com/KimigaiiWuyi/GenshinUID/pull/463 | |
""" | |
import hmac | |
import json | |
from time import time | |
from io import BytesIO | |
from pathlib import Path | |
from hashlib import md5, sha256 | |
from uuid import NAMESPACE_URL, uuid3 | |
from datetime import datetime, timezone, timedelta | |
from typing import Any, Dict, List, Tuple, Union, Literal | |
from qrcode import QRCode | |
from httpx import AsyncClient | |
from PIL import Image, ImageDraw, ImageFont | |
from nonebot import require | |
from nonebot.log import logger | |
from nonebot.typing import T_State | |
from nonebot.plugin import on_command | |
from nonebot.adapters.onebot.v11.event import MessageEvent | |
from nonebot.adapters.onebot.v11 import Bot, MessageSegment | |
from nonebot.adapters.onebot.v11.exception import ActionFailed | |
require("nonebot_plugin_apscheduler") | |
from nonebot_plugin_apscheduler import scheduler # noqa: E402 | |
# require("plugins.genshin_info") | |
from plugins.genshin_info.mihoyo_api import getUid, retcodeChecker # noqa: E402 | |
RES_PATH = Path("data/niao/pay") | |
FONT_PATH = Path("data/niao/font/zh-cn.ttf") | |
TZ = timezone(timedelta(hours=8)) | |
HK4E_SDK = "https://hk4e-sdk.mihoyo.com" | |
MYS_VER = "2.44.1" | |
METHOD: Literal["weixin", "alipay"] = "weixin" # 默认付款方式 | |
THEME_COLOR = {"weixin": "#29ac66", "alipay": "#1678ff"} | |
_GOODS = [ | |
# await fetchGoods() | |
# ProductIdConfigData.json | |
{ | |
"config_id": 1, | |
"tier_id": "Tier_1", | |
"goods_id": "ys_chn_primogem1ststall_tier1", | |
"itemNameTextMapHash": 592163336, # "60创世结晶" | |
"primNameTextMapHash": 3399738308, # "60枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem1ststall", | |
"goods_name": "创世结晶", | |
"goods_unit": "60", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/0f362595da2e37a7a8fde1bb120656d2_594155779359709441.png", | |
"price": "600", | |
"aliases": ["创世结晶×60", "创世结晶x60", "结晶×60", "结晶x60", "创世结晶60", "结晶60"], | |
}, | |
{ | |
"config_id": 2, | |
"tier_id": "Tier_5", | |
"goods_id": "ys_chn_primogem2ndstall_tier5", | |
"itemNameTextMapHash": 1264426216, # "300创世结晶" | |
"primNameTextMapHash": 2700754972, # "300枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem2ndstall", | |
"goods_name": "创世结晶", | |
"goods_unit": "300", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/830e247bb0cfffa5c74a04e79c0040f5_1814106121630644354.png", | |
"price": "3000", | |
"aliases": ["创世结晶×300", "创世结晶x300", "结晶×300", "结晶x300", "创世结晶300", "结晶300"], | |
}, | |
{ | |
"config_id": 3, | |
"tier_id": "Tier_15", | |
"goods_id": "ys_chn_primogem3rdstall_tier15", | |
"itemNameTextMapHash": 4078387144, # "980创世结晶" | |
"primNameTextMapHash": 320967628, # "980枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem3rdstall", | |
"goods_name": "创世结晶", | |
"goods_unit": "980", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/dfefc92ce56e3b5615ef28d6b1119b8b_5835214000384994274.png", | |
"price": "9800", | |
"aliases": [ | |
"创世结晶×980", | |
"创世结晶x980", | |
"结晶×980", | |
"结晶x980", | |
"创世结晶980", | |
"结晶980", | |
"98", | |
], | |
}, | |
{ | |
"config_id": 4, | |
"tier_id": "Tier_30", | |
"goods_id": "ys_chn_primogem4thstall_tier30", | |
"itemNameTextMapHash": 1175452288, # "1980创世结晶" | |
"primNameTextMapHash": 1588912924, # "1980枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem4thstall", | |
"goods_name": "创世结晶", | |
"goods_unit": "1980", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/e918ecfedcb13113eb627fd944199272_3460055016813877022.png", | |
"price": "19800", | |
"aliases": [ | |
"创世结晶×1980", | |
"创世结晶x1980", | |
"结晶×1980", | |
"结晶x1980", | |
"创世结晶1980", | |
"结晶1980", | |
"198", | |
], | |
}, | |
{ | |
"config_id": 5, | |
"tier_id": "Tier_50", | |
"goods_id": "ys_chn_primogem5thstall_tier50", | |
"itemNameTextMapHash": 982800008, # "3280创世结晶" | |
"primNameTextMapHash": 1861426364, # "3280枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem5thstall", | |
"goods_name": "创世结晶", | |
"goods_unit": "3280", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/70e703a64e8786390ab8b7cdc35dbeeb_6358952826545947027.png", | |
"price": "32800", | |
"aliases": [ | |
"创世结晶×3280", | |
"创世结晶x3280", | |
"结晶×3280", | |
"结晶x3280", | |
"创世结晶3280", | |
"结晶3280", | |
"328", | |
], | |
}, | |
{ | |
"config_id": 6, | |
"tier_id": "Tier_60", | |
"goods_id": "ys_chn_primogem6thstall_tier60", | |
"itemNameTextMapHash": 2819973760, # "6480创世结晶" | |
"primNameTextMapHash": 2491404828, # "6480枚创世结晶" | |
"icon": "UI_Mall_Purchase_Primogem6thstall", | |
"goods_name": "创世结晶", | |
"goods_unit": "6480", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/24fa6b6190ce5da6928e431a832d85c3_5932007685099741224.png", | |
"price": "64800", | |
"aliases": [ | |
"创世结晶×6480", | |
"创世结晶x6480", | |
"结晶×6480", | |
"结晶x6480", | |
"创世结晶6480", | |
"结晶6480", | |
"648", | |
], | |
}, | |
# ProductCardDetailConfigData.json | |
{ | |
"config_id": 101, | |
"tier_id": "Tier_5", | |
"goods_id": "ys_chn_blessofmoon_tier5", | |
"icon": "UI_Mall_Purchase_Blessofmoon", | |
"itemNameTextMapHash": 3486638812, # "空月祝福" | |
"replaceMcoinNum": 330, | |
"goods_name": "空月祝福", | |
"goods_unit": "0", | |
"goods_icon": "https://uploadstatic.mihoyo.com/payment-center/2020/06/08/2da77803b9b2ffc2a2b763a59e9c125f_5219067706372136934.png", | |
"price": "3000", | |
"aliases": ["空月祝福", "空月", "祝福", "月卡", "小月卡"], | |
}, | |
# ProductIdConfigData.json | |
# ProductPlayDetailConfigData.json | |
# PriceTierConfigData.json | |
{ | |
"config_id": 201, | |
"tier_id": "Tier_10", | |
"goods_id": "ys_chn_bp_normal_tier10", | |
"itemNameTextMapHash": 1087749475, # "珍珠纪行" | |
"replaceMcoinNum": 750, | |
"goods_name": "珍珠纪行", | |
"goods_unit": "0", | |
"goods_icon": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
"price": "6800", | |
"aliases": ["珍珠纪行", "纪行", "大月卡", "小纪行", "68"], | |
}, | |
{ | |
"config_id": 202, | |
"tier_id": "Tier_20", | |
"goods_id": "ys_chn_bp_extra_tier20", | |
"itemNameTextMapHash": 374976595, # "珍珠之歌" | |
"replaceMcoinNum": 1410, | |
"goods_name": "珍珠之歌", | |
"goods_unit": "0", | |
"goods_icon": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
"price": "12800", | |
"aliases": ["珍珠之歌", "之歌", "大大月卡", "大纪行", "128"], | |
}, | |
{ | |
"config_id": 203, | |
"tier_id": "Tier_12", | |
"goods_id": "ys_chn_bp_upgrade_tier12", | |
"itemNameTextMapHash": 3178887299, # "纪行成歌" | |
"replaceMcoinNum": 860, | |
"goods_name": "纪行成歌", | |
"goods_unit": "0", | |
"goods_icon": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
"price": "7800", | |
"aliases": ["纪行成歌", "成歌", "月卡升级", "纪行升级", "78"], | |
}, | |
] | |
def font(size: int) -> ImageFont.FreeTypeFont: | |
"""Pillow 绘制字体设置""" | |
return ImageFont.truetype(str(FONT_PATH), size=size) | |
async def fetchGoods() -> Union[List, str]: | |
async with AsyncClient( | |
base_url=HK4E_SDK, proxies={"all://": "socks5://127.0.0.1:1080"} | |
) as client: | |
res = await client.post( | |
"/hk4e_cn/mdk/shopwindow/shopwindow/fetchGoods", | |
json={ | |
"released_flag": True, | |
"game": "hk4e_cn", | |
"region": "cn_gf01", | |
"uid": "1", | |
"account": "1", | |
}, | |
) | |
resJson = res.json() | |
check = retcodeChecker(resJson) | |
return check if isinstance(check, str) else resJson["data"]["goods_list"] | |
def getOrderSign(data: Dict) -> str: | |
data = dict(sorted(data.items(), key=lambda x: x[0])) | |
value = "".join([str(i) for i in data.values()]) | |
return ( | |
hmac.new( | |
"6bdc3982c25f3f3c38668a32d287d16b".encode("utf-8"), | |
value.encode("utf-8"), | |
digestmod=sha256, | |
) | |
.digest() | |
.hex() | |
) | |
async def createOrder( | |
goodData: Dict, | |
uid: str, | |
accountId: str, | |
cookie: str, | |
method: Literal["weixin", "alipay"], | |
) -> Union[Dict, str]: | |
device = f"NB-{md5(uid.encode()).hexdigest()[:5]}" | |
headers = { | |
"User-Agent": ( | |
f"Mozilla/5.0 (Linux; Android 12; {device}) AppleWebKit/537.36 (KHTML, " | |
f"like Gecko) Chrome/99.0.4844.73 Mobile Safari/537.36 miHoYoBBS/{MYS_VER}" | |
), | |
"Referer": "https://webstatic.mihoyo.com/", | |
"Origin": "https://webstatic.mihoyo.com", | |
"Cookie": cookie, | |
"x-rpc-app_version": MYS_VER, | |
"x-rpc-device_id": str(uuid3(NAMESPACE_URL, uid)), | |
"x-rpc-client_type": "4", | |
} | |
order = { | |
"account": accountId, | |
"region": "cn_gf01", | |
"uid": uid, | |
"delivery_url": "", | |
"device": str(uuid3(NAMESPACE_URL, uid)), | |
"channel_id": 1, | |
"client_ip": "", | |
"client_type": 4, | |
"game": "hk4e_cn", | |
"amount": goodData["price"], | |
"goods_num": 1, | |
"goods_id": goodData["goods_id"], | |
"goods_title": f'{goodData["goods_name"]}×{str(goodData["goods_unit"])}' | |
if int(goodData["goods_unit"]) > 0 | |
else goodData["goods_name"], | |
"price_tier": goodData["tier_id"], | |
"currency": "CNY", | |
"pay_plat": method, | |
} | |
async with AsyncClient( | |
base_url=HK4E_SDK, proxies={"all://": "socks5://127.0.0.1:1080"} | |
) as client: | |
res = await client.post( | |
"/hk4e_cn/mdk/atropos/api/createOrder", | |
headers=headers, | |
json={"order": order, "sign": getOrderSign(order)}, | |
) | |
resJson = res.json() | |
check = retcodeChecker(resJson) | |
return check if isinstance(check, str) else resJson["data"] | |
async def getImages( | |
itemId: int, orderData: Dict[str, Any], method: str | |
) -> Tuple[Image.Image, Image.Image]: | |
"""商品图片、二维码图片、账单信息""" | |
async with AsyncClient(proxies={"all://": "socks5://127.0.0.1:1080"}) as client: | |
itemPic = RES_PATH / f"{itemId}.png" | |
if itemPic.exists(): | |
itemImg = Image.open(itemPic) | |
else: | |
_img = await client.get(_GOODS[itemId]["goods_icon"], timeout=10.0) | |
itemImg = Image.open(BytesIO(_img.content)).convert("RGBA") | |
itemImg.save(itemPic, quality=100) | |
_qrcode = QRCode() | |
_qrcode.add_data(orderData["encode_order"]) | |
_qrcode.make() | |
qrcodeImg = _qrcode.make_image(fill=THEME_COLOR[method], back_color="white") | |
return itemImg, qrcodeImg | |
async def draw( | |
uid: str, | |
icon: Image.Image, | |
qrcode: Image.Image, | |
good: Dict[str, Any], | |
order: Dict[str, str], | |
) -> bytes: | |
"""充值图片绘制""" | |
method = "weixin" if order["encode_order"].startswith("weixin") else "alipay" | |
themeColor = THEME_COLOR[method] | |
warning = 20 if good.get("replaceMcoinNum") else 0 | |
res = Image.new("RGBA", (450, 520), themeColor) | |
drawer = ImageDraw.Draw(res) | |
resample = getattr(Image, "Resampling", Image).LANCZOS | |
# 头部矩形背景 | |
drawer.rectangle((75, 50 - warning, 375 - 1, 150 - warning), fill="#E5F9FF", width=0) | |
# 商品图片 | |
item = icon.resize((90, 90), resample=resample) | |
res.paste(item, (80, 55 - warning), item) | |
# 商品名称 | |
drawer.text( | |
( | |
int(175 + (195 - font(25).getlength(good["aliases"][0])) / 2), | |
int(70 - warning + (30 - font(25).getbbox(good["aliases"][0])[-1]) / 2), | |
), | |
good["aliases"][0], | |
fill="#000000", | |
font=font(25), | |
) | |
# 商品充值 UID | |
drawer.text( | |
( | |
int(175 + (195 - font(15).getlength(f"充值到 UID{uid}")) / 2), | |
int(110 - warning + (20 - font(15).getbbox(f"充值到 UID{uid}")[-1]) / 2), | |
), | |
f"充值到 UID{uid}", | |
fill="#333333", | |
font=font(15), | |
) | |
# 二维码图片 | |
qrcode = qrcode.resize((300, 300), resample=resample) | |
res.paste(qrcode, (75, 150 + warning), qrcode) | |
# 月卡及纪行相关商品警告 | |
if warning: | |
# 首部矩形背景 | |
# drawer.rectangle((75, 145, 375 - 1, 155), fill="#ffffff", width=0) | |
drawer.rectangle((75, 130, 375 - 1, 170), fill="#eeeeee", width=0) | |
# 转换警告文字 | |
warning_text = f"特殊情况将直接返还 {good['replaceMcoinNum']} 创世结晶" | |
drawer.text( | |
( | |
int((450 - font(15).getlength(warning_text)) / 2), | |
int(130 + (40 - font(15).getbbox(warning_text)[-1]) / 2), | |
), | |
warning_text, | |
fill="#ff5652", | |
font=font(15), | |
) | |
# 尾部矩形背景 | |
drawer.rectangle( | |
(75, 450 + warning, 375 - 1, 460 + warning), fill="#ffffff", width=0 | |
) | |
# 图片生成时间 | |
timestamp = datetime.fromtimestamp(int(order["create_time"]), TZ).strftime( | |
"%Y-%m-%d %H:%M:%S" | |
) | |
drawer.text( | |
( | |
int((450 - font(15).getlength(timestamp)) / 2), | |
int(450 + warning - font(15).getbbox(timestamp)[-1] - 3), | |
), | |
timestamp, | |
fill=themeColor, | |
font=font(15), | |
) | |
# 账单信息 | |
ticket = f"{'微信支付商户单号' if method == 'weixin' else '支付宝商家订单号'} {order['order_no']}" | |
drawer.text( | |
(int((450 - font(15).getlength(ticket)) / 2), 470 + warning), | |
ticket, | |
fill="#000000", | |
font=font(15), | |
) | |
buf = BytesIO() | |
res.convert("RGB").save(buf, format="PNG") | |
return buf.getvalue() | |
async def withdraw(bot: Bot, message_id: int) -> None: | |
await bot.delete_msg(message_id=message_id) | |
payMatcher = on_command("原神充值", aliases={"充值", "pay"}, priority=11) | |
@payMatcher.handle() | |
async def pay_handle(bot: Bot, event: MessageEvent, state: T_State): | |
arg = str(state["_prefix"]["command_arg"]) | |
qq: str = ( | |
event.message["at"][0].data["qq"] | |
if event.message.get("at") | |
else event.get_user_id() | |
) | |
itemId, uid, method = 0, None, METHOD | |
for s in arg.split(): | |
# 支持指定支付方式 | |
if "微信" in s: | |
method = "weixin" | |
continue | |
elif "支付宝" in s: | |
method = "alipay" | |
continue | |
# 仅支持国内官服 UID | |
if len(s) == 9 and s.isdigit() and s[0] in ["1", "2"]: | |
uid = s | |
continue | |
# 输入物品别名识别 | |
for gIdx, gData in enumerate(_GOODS): | |
if (s == str(gIdx)) or (s in gData["aliases"]): | |
itemId = gIdx | |
break | |
uid = uid or (await getUid(qq)) # 由 QQ 号查询 UID | |
if not uid: | |
await payMatcher.finish("UID 捏?", at_sender=True) | |
# 处理 Cookie | |
configs = json.loads((RES_PATH.parent / "users.json").read_text(encoding="UTF-8")) | |
if not configs.get(uid): | |
await payMatcher.finish(f"缺少 {uid} 的 Cookie 无法创建订单!") | |
cookie = configs[uid]["cookie"] | |
accountId = cookie.split("account_id=", 1)[1].split(";", 1)[0] | |
if not accountId: | |
await payMatcher.finish(f"{uid} 的 Cookie 缺少 account_id!") | |
# 从网页接口获取商品列表(不包含纪行商品) | |
# goodsData = await fetchGoods() | |
# if isinstance(goodsData, str): | |
# await payMatcher.finish(goodsData) | |
# if itemId >= len(goodsData): | |
# await payMatcher.finish(f"暂不支持充值 {_GOODS[itemId]['aliases'][0]}!") | |
# goodData = goodsData[itemId] | |
goodData = _GOODS[itemId] | |
order = await createOrder(goodData, uid, accountId, cookie, method) | |
# (RES_PATH / f"order.{uid}-{itemId}.json").write_text( | |
# json.dumps(order, ensure_ascii=False, indent=4), encoding="UTF-8" | |
# ) | |
if isinstance(order, str): | |
await payMatcher.finish(order) | |
itemImg, qrcodeImg = await getImages(itemId, order, method) | |
try: | |
msg = await payMatcher.send( | |
MessageSegment.image(await draw(uid, itemImg, qrcodeImg, goodData, order)) | |
) | |
# expiry = (3 if method == "weixin" else 1) * 60 | |
scheduler.add_job( | |
withdraw, | |
"date", | |
args=[bot, dict(msg)["message_id"]], | |
run_date=datetime.fromtimestamp(time() + 100, TZ), # 100 秒后撤回 | |
) | |
except ActionFailed: | |
logger.error("充值二维码发送失败!") | |
except Exception as e: | |
logger.opt(exception=e).error("充值二维码撤回任务异常!") |
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
""" | |
NoneBot2 原神充值二维码插件,触发命令:原神充值/充值/pay | |
阉割了指令的 @QQ 功能,其余功能完整 | |
如需启用自动撤回功能,请先安装 nonebot_plugin_apscheduler 插件并定义第 45 行撤回等待秒数 | |
第 43 行需要手动填写 API 地址用于第 138 行拼接,不要问我去哪找 API | |
""" | |
from base64 import b64decode | |
from datetime import datetime | |
from io import BytesIO | |
from pathlib import Path | |
from re import findall | |
from time import localtime, strftime, time | |
from typing import Dict, Literal, Optional, Tuple | |
from httpx import AsyncClient, stream | |
from PIL import Image, ImageDraw, ImageFont | |
from nonebot import require | |
from nonebot.adapters.onebot.v11 import Bot, MessageSegment | |
from nonebot.adapters.onebot.v11.event import MessageEvent | |
from nonebot.adapters.onebot.v11.exception import ActionFailed | |
from nonebot.log import logger | |
from nonebot.plugin import on_command | |
from nonebot.typing import T_State | |
# require("plugins.genshin_info") | |
# from plugins.genshin_info import getUid | |
RES_PATH = Path("data/pay") | |
RES_PATH.mkdir(parents=True, exist_ok=True) | |
FONT_PATH = Path("data/font/zh-cn.ttf") | |
FONT_PATH.parent.mkdir(parents=True, exist_ok=True) | |
if not FONT_PATH.exists(): | |
with stream("GET", "https://cdn.monsterx.cn/bot/zh-cn.ttf", verify=False) as r: | |
with open(FONT_PATH, "wb") as f: | |
for chunk in r.iter_bytes(): | |
f.write(chunk) | |
API = "http://这里填写 API 地址" | |
METHOD: Literal["微信", "支付宝"] = "支付宝" # 默认付款方式 | |
WITHDRAW: int = 0 # 自动撤回等待秒数,为 0 即不自动撤回,非管理员无法撤回 120 秒以上的消息 | |
GOODS = { | |
"0": { | |
"title": "创世结晶×60", | |
"aliases": ["创世结晶x60", "结晶×60", "结晶x60", "创世结晶60", "结晶60"], | |
"cost": 6, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/0f362595da2e37a7a8fde1bb120656d2_594155779359709441.png", | |
}, | |
"1": { | |
"title": "创世结晶×300", | |
"aliases": ["创世结晶x300", "结晶×300", "结晶x300", "创世结晶300", "结晶300"], | |
"cost": 30, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/830e247bb0cfffa5c74a04e79c0040f5_1814106121630644354.png", | |
}, | |
"2": { | |
"title": "创世结晶×980", | |
"aliases": ["创世结晶x980", "结晶×980", "结晶x980", "创世结晶980", "结晶980", "98"], | |
"cost": 98, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/dfefc92ce56e3b5615ef28d6b1119b8b_5835214000384994274.png", | |
}, | |
"3": { | |
"title": "创世结晶×1980", | |
"aliases": ["创世结晶x1980", "结晶×1980", "结晶x1980", "创世结晶1980", "结晶1980", "198"], | |
"cost": 198, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/e918ecfedcb13113eb627fd944199272_3460055016813877022.png", | |
}, | |
"4": { | |
"title": "创世结晶×3280", | |
"aliases": ["创世结晶x3280", "结晶×3280", "结晶x3280", "创世结晶3280", "结晶3280", "328"], | |
"cost": 328, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/70e703a64e8786390ab8b7cdc35dbeeb_6358952826545947027.png", | |
}, | |
"5": { | |
"title": "创世结晶×6480", | |
"aliases": ["创世结晶x6480", "结晶×6480", "结晶x6480", "创世结晶6480", "结晶6480", "648"], | |
"cost": 648, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2022/09/07/24fa6b6190ce5da6928e431a832d85c3_5932007685099741224.png", | |
}, | |
"6": { | |
"title": "空月祝福", | |
"aliases": ["空月", "祝福", "月卡", "小月卡"], | |
"cost": 30, | |
"error": 330, | |
"img": "https://uploadstatic.mihoyo.com/payment-center/2020/06/08/2da77803b9b2ffc2a2b763a59e9c125f_5219067706372136934.png", | |
}, | |
"7": { | |
"title": "珍珠纪行", | |
"aliases": ["纪行", "大月卡", "小纪行", "68"], | |
"cost": 68, | |
"error": 750, | |
"img": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
}, | |
"8": { | |
"title": "珍珠之歌", | |
"aliases": ["之歌", "大大月卡", "大纪行", "128"], | |
"cost": 128, | |
"error": 1410, | |
"img": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
}, | |
"9": { | |
"title": "纪行成歌", | |
"aliases": ["成歌", "月卡升级", "纪行升级", "78"], | |
"cost": 78, | |
"error": 860, | |
"img": "https://cdn.monsterx.cn/bot/Battle_Pass.png", | |
}, | |
} | |
def font(size: int) -> ImageFont.FreeTypeFont: | |
"""Pillow 绘制字体设置""" | |
return ImageFont.truetype(str(FONT_PATH), size=size) | |
async def withdraw(bot: Bot, message_id: int) -> None: | |
"""撤回消息任务""" | |
await bot.delete_msg(message_id=message_id) | |
async def getImages( | |
itemId: str, uid: str, method: str | |
) -> Tuple[Image.Image, Optional[Image.Image], Dict[str, str]]: | |
"""商品图片、二维码图片、账单信息""" | |
async with AsyncClient() as client: | |
itemPic = RES_PATH / f"{itemId}.png" | |
if itemPic.exists(): | |
itemImg = Image.open(itemPic) | |
else: | |
_img = await client.get(GOODS[itemId]["img"], timeout=10.0) | |
itemImg = Image.open(BytesIO(_img.content)).convert("RGBA") | |
itemImg.save(itemPic, quality=100) | |
api = f"{API}/{itemId}/{uid}/{method}" | |
html = (await client.get(api, timeout=10.0)).text | |
qrcode = findall(r'data:;base64,(.*)"\salt', html) | |
if not qrcode: | |
return itemImg, None, {} | |
orderId = findall(r"order:(\d+)", html) | |
orderUrl = findall(r"<br>url:(.*)</p>", html) | |
info = { | |
"uid": uid, | |
"id": orderId[0].strip(), | |
"url": orderUrl[0].strip(), | |
"time": time(), | |
} | |
byteData = b64decode(qrcode[0]) | |
qrcodeImg = Image.open(BytesIO(byteData)) | |
return itemImg, qrcodeImg, info | |
async def draw( | |
itemId: str, item: Image.Image, qrcode: Image.Image, info: Dict[str, str] | |
) -> bytes: | |
"""充值图片绘制""" | |
themeColor = "#29ac66" if info["url"].startswith("weixin") else "#1678ff" | |
warning = 20 if GOODS[itemId].get("error") else 0 | |
res = Image.new("RGBA", (450, 520), themeColor) | |
drawer = ImageDraw.Draw(res) | |
resample = getattr(Image, "Resampling", Image).LANCZOS | |
# 头部矩形背景 | |
drawer.rectangle((75, 50 - warning, 375 - 1, 150 - warning), fill="#E5F9FF", width=0) | |
# 商品图片 | |
item = item.resize((90, 90), resample=resample) | |
res.paste(item, (80, 55 - warning), item) | |
# 商品名称 | |
drawer.text( | |
( | |
int(175 + (195 - font(25).getlength(GOODS[itemId]["title"])) / 2), | |
int(70 - warning + (30 - font(25).getbbox(GOODS[itemId]["title"])[-1]) / 2), | |
), | |
GOODS[itemId]["title"], | |
fill="#000000", | |
font=font(25), | |
) | |
# 商品充值 UID | |
drawer.text( | |
( | |
int(175 + (195 - font(15).getlength(f"充值到 UID{info['uid']}")) / 2), | |
int(110 - warning + (20 - font(15).getbbox(f"充值到 UID{info['uid']}")[-1]) / 2), | |
), | |
f"充值到 UID{info['uid']}", | |
fill="#333333", | |
font=font(15), | |
) | |
# 二维码图片 | |
qrcode = qrcode.resize((300, 300), resample=resample) | |
res.paste(qrcode, (75, 150 + warning), qrcode) | |
# 月卡及纪行相关商品警告 | |
if warning: | |
# 首部矩形背景 | |
drawer.rectangle((75, 130, 375 - 1, 170), fill="#eeeeee", width=0) | |
# 转换警告文字 | |
warning_text = f"特殊情况将直接返还 {GOODS[itemId]['error']} 创世结晶" | |
drawer.text( | |
( | |
int((450 - font(15).getlength(warning_text)) / 2), | |
int(130 + (40 - font(15).getbbox(warning_text)[-1]) / 2), | |
), | |
warning_text, | |
fill="#ff5652", | |
font=font(15), | |
) | |
# 尾部矩形背景 | |
drawer.rectangle((75, 450 + warning, 375 - 1, 460 + warning), fill="#ffffff", width=0) | |
# 图片生成时间 | |
timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime(int(info["time"]))) | |
drawer.text( | |
( | |
int((450 - font(15).getlength(timestamp)) / 2), | |
int(450 + warning - font(15).getbbox(timestamp)[-1] - 3), | |
), | |
timestamp, | |
fill=themeColor, | |
font=font(15), | |
) | |
# 账单信息 | |
ticket = ( | |
f"{'微信支付商户单号' if info['url'].startswith('weixin') else '支付宝商家订单号'} {info['id']}" | |
) | |
drawer.text( | |
(int((450 - font(15).getlength(ticket)) / 2), 470 + warning), | |
ticket, | |
fill="#000000", | |
font=font(15), | |
) | |
buf = BytesIO() | |
res.convert("RGB").save(buf, format="PNG") | |
return buf.getvalue() | |
payMatcher = on_command("原神充值", aliases={"充值", "pay"}, priority=11) | |
@payMatcher.handle() | |
async def pay_handle(bot: Bot, event: MessageEvent, state: T_State): | |
arg = str(state["_prefix"]["command_arg"]) | |
# qq: str = ( | |
# event.message["at"][0].data["qq"] | |
# if event.message.get("at") | |
# else event.get_user_id() | |
# ) | |
itemId, uid, method = "0", None, ("0" if METHOD == "支付宝" else "1") | |
for s in arg.split(): | |
# 支持指定支付方式 | |
if "微信" in s: | |
method = "1" | |
continue | |
elif "支付宝" in s: | |
method = "0" | |
continue | |
# 仅支持国内官服 UID | |
if len(s) == 9 and s.isdigit() and s[0] in ["1", "2"]: | |
uid = s | |
continue | |
# 输入物品别名识别 | |
for gId, gData in GOODS.items(): | |
if (s == gId) or (s in gData["aliases"]): | |
itemId = gId | |
break | |
uid = uid # or (await getUid(qq)) # 由 QQ 号查询 UID | |
if not uid: | |
await payMatcher.finish("UID 捏?", at_sender=True) | |
debugMsg = f"UID[{uid}] ITEM[{itemId}] METHOD[{method}]" | |
itemImg, qrcodeImg, info = await getImages(itemId, uid, method) | |
if not qrcodeImg: | |
logger.info(f"未能从 API 获取二维码!{debugMsg}") | |
await payMatcher.finish("未能从 API 获取二维码!") | |
try: | |
msg = await payMatcher.send( | |
MessageSegment.image(await draw(itemId, itemImg, qrcodeImg, info)) | |
) | |
if WITHDRAW: | |
require("nonebot_plugin_apscheduler") | |
from nonebot_plugin_apscheduler import scheduler | |
scheduler.add_job( | |
withdraw, | |
"date", | |
args=[bot, dict(msg)["message_id"]], | |
run_date=datetime.fromtimestamp(time() + WITHDRAW), | |
) | |
except ActionFailed: | |
logger.error(f"充值二维码发送失败!{debugMsg}") | |
except Exception as e: | |
logger.opt(exception=e).error(f"充值二维码撤回任务异常!{debugMsg}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
通过加载
src/plugins/GenshinUID
文件夹方式安装 GenshinUID 的可以按下面格式修改,以启用 ”不填 UID 时自动使用发送消息的 QQ 绑定的 UID“ 及@QQ
功能