mirror of
https://github.com/xuthus83/LittlePaimon.git
synced 2024-10-21 16:27:15 +08:00
commit
049e6ef265
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/LittlePaimon.iml" filepath="$PROJECT_DIR$/.idea/LittlePaimon.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
160
Paimon_Calender/__init__.py
Normal file
160
Paimon_Calender/__init__.py
Normal file
@ -0,0 +1,160 @@
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from nonebot import require, get_bot, on_command
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent, Bot, Message, MessageSegment, ActionFailed
|
||||
from nonebot.params import CommandArg
|
||||
|
||||
from ..utils.config import config
|
||||
from ..utils.file_handler import load_json, save_json
|
||||
from ..utils.generate import *
|
||||
import re
|
||||
|
||||
HELP_STR = '''
|
||||
原神活动日历
|
||||
原神日历 : 查看本群订阅服务器日历
|
||||
原神日历 on/off : 订阅/取消订阅指定服务器的日历推送
|
||||
原神日历 time 时:分 : 设置日历推送时间
|
||||
原神日历 status : 查看本群日历推送设置
|
||||
'''.strip()
|
||||
|
||||
calendar = on_command('原神日历', aliases={"原神日历", 'ysrl', '原神日程'}, priority=24, block=True)
|
||||
scheduler = require('nonebot_plugin_apscheduler').scheduler
|
||||
|
||||
|
||||
async def send_calendar(group_id, group_data):
|
||||
for server in group_data[str(group_id)]['server_list']:
|
||||
im = await generate_day_schedule(server)
|
||||
base64_str = im2base64str(im)
|
||||
if 'cardimage' not in group_data or not group_data['cardimage']:
|
||||
msg = MessageSegment.image(base64_str)
|
||||
else:
|
||||
msg = f'[CQ:cardimage,file={base64_str}]'
|
||||
|
||||
await get_bot().send_group_msg(group_id=int(group_id), message=msg)
|
||||
|
||||
|
||||
def update_group_schedule(group_id, group_data):
|
||||
group_id = str(group_id)
|
||||
if group_id not in group_data:
|
||||
return
|
||||
|
||||
scheduler.add_job(
|
||||
func=send_calendar,
|
||||
trigger='cron',
|
||||
args=(group_id, group_data),
|
||||
id=f'genshin_calendar_{group_id}',
|
||||
replace_existing=True,
|
||||
hour=group_data[group_id]['hour'],
|
||||
minute=group_data[group_id]['minute'],
|
||||
misfire_grace_time=10
|
||||
)
|
||||
|
||||
|
||||
@calendar.handle()
|
||||
async def _(bot: Bot, event: Union[GroupMessageEvent, MessageEvent], msg: Message = CommandArg()):
|
||||
if event.message_type == 'private':
|
||||
await calendar.finish('仅支持群聊模式下使用本指令')
|
||||
|
||||
group_id = str(event.group_id)
|
||||
group_data = load_json('calender_push.json')
|
||||
server = 'cn'
|
||||
fun = str(msg).strip()
|
||||
action = re.search(r'(?P<action>on|off|time|status|test)', fun)
|
||||
|
||||
if group_id not in config.paimon_calender_group:
|
||||
await calendar.finish(f"尚未在群 {group_id} 开启本功能!", at_sender=True)
|
||||
|
||||
if not fun:
|
||||
im = await generate_day_schedule(server)
|
||||
base64_str = im2base64str(im)
|
||||
group_data = load_json('calender_push.json')
|
||||
|
||||
try:
|
||||
if group_id not in group_data or 'cardimage' not in group_data[group_id] or not group_data[group_id]['cardimage']:
|
||||
await calendar.finish(MessageSegment.image(base64_str))
|
||||
else:
|
||||
await calendar.finish(f'[CQ:cardimage,file={base64_str}]')
|
||||
except ActionFailed as e:
|
||||
await logging.ERROR(e)
|
||||
|
||||
elif action:
|
||||
|
||||
# 添加定时推送任务
|
||||
if action.group('action') == 'on':
|
||||
group_data[group_id] = {
|
||||
'server_list': [
|
||||
str(server)
|
||||
],
|
||||
'hour': 8,
|
||||
'minute': 0,
|
||||
'cardimage': False
|
||||
}
|
||||
if event.message_type == 'guild':
|
||||
await calendar.finish("暂不支持频道内推送~")
|
||||
|
||||
if scheduler.get_job('genshin_calendar_' + group_id):
|
||||
scheduler.remove_job("genshin_calendar_" + group_id)
|
||||
save_json(group_data, 'calender_push.json')
|
||||
|
||||
scheduler.add_job(
|
||||
func=send_calendar,
|
||||
trigger='cron',
|
||||
hour=8,
|
||||
minute=0,
|
||||
id="genshin_calendar_" + group_id,
|
||||
args=(group_id, group_data[group_id]),
|
||||
misfire_grace_time=10
|
||||
)
|
||||
|
||||
await calendar.finish('原神日程推送已开启')
|
||||
|
||||
# 关闭推送功能
|
||||
elif action.group('action') == 'off':
|
||||
del group_data[group_id]
|
||||
if scheduler.get_job("genshin_calendar_" + group_id):
|
||||
scheduler.remove_job("genshin_calendar_" + group_id)
|
||||
await calendar.finish('原神日程推送已关闭')
|
||||
|
||||
# 设置推送时间
|
||||
elif action.group('action') == 'time':
|
||||
match = str(msg).split(" ")
|
||||
time = re.search(r'(\d{1,2}):(\d{2})', match[1])
|
||||
|
||||
if re.match(r'(\d{1,2}):(\d{2})', match[1]):
|
||||
if not time or len(time.groups()) < 2:
|
||||
await calendar.finish("请指定推送时间")
|
||||
else:
|
||||
group_data[group_id]['hour'] = int(time.group(1))
|
||||
group_data[group_id]['minute'] = int(time.group(2))
|
||||
save_json(group_data, 'calender_push.json')
|
||||
update_group_schedule(group_id, group_data)
|
||||
|
||||
await calendar.finish(
|
||||
f"推送时间已设置为: {group_data[group_id]['hour']}:{group_data[group_id]['minute']:02d}")
|
||||
|
||||
else:
|
||||
await calendar.finish("请给出正确的时间,格式为12:00", at_sender=True)
|
||||
# DEBUG
|
||||
elif action.group('action') == 'test':
|
||||
return
|
||||
|
||||
# 查询订阅推送状态
|
||||
elif action.group('action') == "status":
|
||||
message = f"订阅日历: {group_data[group_id]['server_list']}"
|
||||
message += f"\n推送时间: {group_data[group_id]['hour']}:{group_data[group_id]['minute']:02d}"
|
||||
await calendar.finish(message)
|
||||
else:
|
||||
await calendar.finish('指令错误')
|
||||
|
||||
# 自动推送任务
|
||||
for group_id, group_data in load_json('calender_push.json').items():
|
||||
scheduler.add_job(
|
||||
func=send_calendar,
|
||||
trigger='cron',
|
||||
hour=group_data['hour'],
|
||||
minute=group_data['minute'],
|
||||
id="genshin_calendar_" + group_id,
|
||||
args=(group_id, group_data),
|
||||
misfire_grace_time=10
|
||||
)
|
BIN
res/wqy-microhei.ttc
Normal file
BIN
res/wqy-microhei.ttc
Normal file
Binary file not shown.
@ -35,6 +35,8 @@ class PluginConfig(BaseModel):
|
||||
paimon_chat_group: List[int] = []
|
||||
# 派蒙猜语音持续时间
|
||||
paimon_guess_voice: int = 30
|
||||
# 原神日历开启群组
|
||||
paimon_calender_group: List[int] = []
|
||||
|
||||
|
||||
driver = get_driver()
|
||||
|
96
utils/draw.py
Normal file
96
utils/draw.py
Normal file
@ -0,0 +1,96 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
font_Path = res_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'res')
|
||||
font = ImageFont.truetype(os.path.join(res_path, 'wqy-microhei.ttc'), 20)
|
||||
|
||||
width = 500
|
||||
|
||||
color = [
|
||||
{'front': 'black', 'back': 'white'},
|
||||
{'front': 'white', 'back': 'ForestGreen'},
|
||||
{'front': 'white', 'back': 'DarkOrange'},
|
||||
{'front': 'white', 'back': 'BlueViolet'},
|
||||
]
|
||||
|
||||
|
||||
def create_image(item_number):
|
||||
height = item_number * 30
|
||||
im = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
||||
return im
|
||||
|
||||
|
||||
def draw_rec(im, color, x, y, w, h, r):
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.rectangle((x+r, y, x+w-r, y+h), fill=color)
|
||||
draw.rectangle((x, y+r, x+w, y+h-r), fill=color)
|
||||
r = r * 2
|
||||
draw.ellipse((x, y, x+r, y+r), fill=color)
|
||||
draw.ellipse((x+w-r, y, x+w, y+r), fill=color)
|
||||
draw.ellipse((x, y+h-r, x+r, y+h), fill=color)
|
||||
draw.ellipse((x+w-r, y+h-r, x+w, y+h), fill=color)
|
||||
|
||||
|
||||
def draw_text(im, x, y, w, h, text, align, color):
|
||||
draw = ImageDraw.Draw(im)
|
||||
tw, th = draw.textsize(text, font=font)
|
||||
y = y + (h - th) / 2
|
||||
if align == 0: # 居中
|
||||
x = x + (w - tw) / 2
|
||||
elif align == 1: # 左对齐
|
||||
x = x + 5
|
||||
elif align == 2: # 右对齐
|
||||
x = x + w - tw - 5
|
||||
draw.text((x, y), text, fill=color, font=font)
|
||||
|
||||
|
||||
def draw_item(im, n, t, text, days, forever):
|
||||
if t >= len(color):
|
||||
t = 1
|
||||
x = 0
|
||||
y = n * 30
|
||||
height = 28
|
||||
|
||||
draw_rec(im, color[t]['back'], x, y, width, height, 6)
|
||||
|
||||
im1 = Image.new('RGBA', (width - 120, 28), (255, 255, 255, 0))
|
||||
draw_text(im1, 0, 0, width, height, text, 1, color[t]['front'])
|
||||
_, _, _, a = im1.split()
|
||||
im.paste(im1, (x, y), mask=a)
|
||||
|
||||
if days > 0:
|
||||
if forever:
|
||||
text1 = '永久开放'
|
||||
else:
|
||||
text1 = f'{days}天后结束'
|
||||
elif days < 0:
|
||||
text1 = f'{-days}天后开始'
|
||||
else:
|
||||
text1 = '即将结束'
|
||||
draw_text(im, x, y, width, height, text1, 2, color[t]['front'])
|
||||
|
||||
|
||||
def draw_title(im, n, left=None, middle=None, right=None):
|
||||
x = 0
|
||||
y = n * 30
|
||||
height = 28
|
||||
|
||||
draw_rec(im, color[0]['back'], x, y, width, height, 6)
|
||||
if middle:
|
||||
draw_text(im, x, y, width, height, middle, 0, color[0]['front'])
|
||||
if left:
|
||||
draw_text(im, x, y, width, height, left, 1, color[0]['front'])
|
||||
if right:
|
||||
draw_text(im, x, y, width, height, right, 2, color[0]['front'])
|
||||
|
||||
|
||||
def draw_title1(im, n, day_list):
|
||||
x = 0
|
||||
y = n * 30
|
||||
height = 28
|
||||
color = 'black'
|
||||
|
||||
n = len(day_list)
|
||||
for i in range(n):
|
||||
x = width / n * i
|
||||
draw_text(im, x, y, width, height, day_list[i], 1, color)
|
234
utils/event.py
Normal file
234
utils/event.py
Normal file
@ -0,0 +1,234 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import math
|
||||
import functools
|
||||
import re
|
||||
|
||||
# type 0 普通常驻任务深渊 1 新闻 2 蛋池 3 限时活动H5
|
||||
|
||||
event_data = {
|
||||
'cn': [],
|
||||
}
|
||||
|
||||
event_updated = {
|
||||
'cn': '',
|
||||
}
|
||||
|
||||
lock = {
|
||||
'cn': asyncio.Lock(),
|
||||
}
|
||||
|
||||
ignored_key_words = [
|
||||
"修复",
|
||||
"版本内容专题页",
|
||||
"米游社",
|
||||
"调研",
|
||||
"防沉迷"
|
||||
]
|
||||
|
||||
ignored_ann_ids = [
|
||||
495, # 有奖问卷调查开启!
|
||||
1263, # 米游社《原神》专属工具一览
|
||||
423, # 《原神》玩家社区一览
|
||||
422, # 《原神》防沉迷系统说明
|
||||
762, # 《原神》公平运营声明
|
||||
]
|
||||
|
||||
list_api = 'https://hk4e-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnList?game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000'
|
||||
detail_api = 'https://hk4e-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnContent?game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000'
|
||||
|
||||
|
||||
def cache(ttl=timedelta(hours=1), arg_key=None):
|
||||
def wrap(func):
|
||||
cache_data = {}
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args, **kw):
|
||||
nonlocal cache_data
|
||||
default_data = {"time": None, "value": None}
|
||||
ins_key = 'default'
|
||||
if arg_key:
|
||||
ins_key = arg_key + str(kw.get(arg_key, ''))
|
||||
data = cache_data.get(ins_key, default_data)
|
||||
else:
|
||||
data = cache_data.get(ins_key, default_data)
|
||||
|
||||
now = datetime.now()
|
||||
if not data['time'] or now - data['time'] > ttl:
|
||||
try:
|
||||
data['value'] = await func(*args, **kw)
|
||||
data['time'] = now
|
||||
cache_data[ins_key] = data
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return data['value']
|
||||
|
||||
return wrapped
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
@cache(ttl=timedelta(hours=3), arg_key='url')
|
||||
async def query_data(url):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
return await resp.json()
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def load_event_cn():
|
||||
result = await query_data(url=list_api)
|
||||
detail_result = await query_data(url=detail_api)
|
||||
if result and 'retcode' in result and result['retcode'] == 0 and detail_result and 'retcode' in detail_result and detail_result['retcode'] == 0:
|
||||
event_data['cn'] = []
|
||||
event_detail = {}
|
||||
for detail in detail_result['data']['list']:
|
||||
event_detail[detail['ann_id']] = detail
|
||||
|
||||
datalist = result['data']['list']
|
||||
for data in datalist:
|
||||
for item in data['list']:
|
||||
# 1 活动公告 2 游戏公告
|
||||
if item['type'] == 2:
|
||||
ignore = False
|
||||
for ann_id in ignored_ann_ids:
|
||||
if ann_id == item["ann_id"]:
|
||||
ignore = True
|
||||
break
|
||||
if ignore:
|
||||
continue
|
||||
|
||||
for keyword in ignored_key_words:
|
||||
if keyword in item['title']:
|
||||
ignore = True
|
||||
break
|
||||
if ignore:
|
||||
continue
|
||||
|
||||
start_time = datetime.strptime(
|
||||
item['start_time'], r"%Y-%m-%d %H:%M:%S")
|
||||
end_time = datetime.strptime(
|
||||
item['end_time'], r"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 从正文中查找开始时间
|
||||
if event_detail[item["ann_id"]]:
|
||||
content = event_detail[item["ann_id"]]['content']
|
||||
searchObj = re.search(
|
||||
r'(\d+)\/(\d+)\/(\d+)\s(\d+):(\d+):(\d+)', content, re.M | re.I)
|
||||
try:
|
||||
datelist = searchObj.groups() # ('2021', '9', '17')
|
||||
if datelist and len(datelist) >= 6:
|
||||
ctime = datetime.strptime(
|
||||
f'{datelist[0]}-{datelist[1]}-{datelist[2]} {datelist[3]}:{datelist[4]}:{datelist[5]}', r"%Y-%m-%d %H:%M:%S")
|
||||
if start_time < ctime < end_time:
|
||||
start_time = ctime
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
event = {'title': item['title'],
|
||||
'start': start_time,
|
||||
'end': end_time,
|
||||
'forever': False,
|
||||
'type': 0}
|
||||
if '任务' in item['title']:
|
||||
event['forever'] = True
|
||||
if item['type'] == 1:
|
||||
event['type'] = 1
|
||||
if '扭蛋' in item['tag_label']:
|
||||
event['type'] = 2
|
||||
if '倍' in item['title']:
|
||||
event['type'] = 3
|
||||
event_data['cn'].append(event)
|
||||
# 深渊提醒
|
||||
i = 0
|
||||
while i < 2:
|
||||
curmon = datetime.today() + relativedelta(months=i)
|
||||
nextmon = curmon + relativedelta(months=1)
|
||||
event_data['cn'].append({
|
||||
'title': '「深境螺旋」',
|
||||
'start': datetime.strptime(
|
||||
curmon.strftime("%Y/%m/01 04:00"), r"%Y/%m/%d %H:%M"),
|
||||
'end': datetime.strptime(
|
||||
curmon.strftime("%Y/%m/16 03:59"), r"%Y/%m/%d %H:%M"),
|
||||
'forever': False,
|
||||
'type': 3
|
||||
})
|
||||
event_data['cn'].append({
|
||||
'title': '「深境螺旋」',
|
||||
'start': datetime.strptime(
|
||||
curmon.strftime("%Y/%m/16 04:00"), r"%Y/%m/%d %H:%M"),
|
||||
'end': datetime.strptime(
|
||||
nextmon.strftime("%Y/%m/01 03:59"), r"%Y/%m/%d %H:%M"),
|
||||
'forever': False,
|
||||
'type': 3
|
||||
})
|
||||
i = i+1
|
||||
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
async def load_event(server):
|
||||
if server == 'cn':
|
||||
return await load_event_cn()
|
||||
return 1
|
||||
|
||||
|
||||
def get_pcr_now(offset):
|
||||
pcr_now = datetime.now()
|
||||
if pcr_now.hour < 4:
|
||||
pcr_now -= timedelta(days=1)
|
||||
pcr_now = pcr_now.replace(
|
||||
hour=18, minute=0, second=0, microsecond=0) # 用晚6点做基准
|
||||
pcr_now = pcr_now + timedelta(days=offset)
|
||||
return pcr_now
|
||||
|
||||
|
||||
async def get_events(server, offset, days):
|
||||
events = []
|
||||
pcr_now = datetime.now()
|
||||
if pcr_now.hour < 4:
|
||||
pcr_now -= timedelta(days=1)
|
||||
pcr_now = pcr_now.replace(
|
||||
hour=18, minute=0, second=0, microsecond=0) # 用晚6点做基准
|
||||
|
||||
await lock[server].acquire()
|
||||
try:
|
||||
t = pcr_now.strftime('%y%m%d')
|
||||
if event_updated[server] != t:
|
||||
if await load_event(server) == 0:
|
||||
event_updated[server] = t
|
||||
finally:
|
||||
lock[server].release()
|
||||
|
||||
start = pcr_now + timedelta(days=offset)
|
||||
end = start + timedelta(days=days)
|
||||
end -= timedelta(hours=18) # 晚上12点结束
|
||||
|
||||
for event in event_data[server]:
|
||||
if end > event['start'] and start < event['end']: # 在指定时间段内 已开始 且 未结束
|
||||
event['start_days'] = math.ceil(
|
||||
(event['start'] - start) / timedelta(days=1)) # 还有几天开始
|
||||
event['left_days'] = math.floor(
|
||||
(event['end'] - start) / timedelta(days=1)) # 还有几天结束
|
||||
events.append(event)
|
||||
# 按type从大到小 按剩余天数从小到大
|
||||
events.sort(key=lambda item: item["type"]
|
||||
* 100 - item['left_days'], reverse=True)
|
||||
return events
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
async def main():
|
||||
await load_event_cn()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
46
utils/generate.py
Normal file
46
utils/generate.py
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from .event import *
|
||||
from .draw import *
|
||||
|
||||
|
||||
def im2base64str(im):
|
||||
io = BytesIO()
|
||||
im.save(io, 'png')
|
||||
base64_str = f"base64://{base64.b64encode(io.getvalue()).decode()}"
|
||||
return base64_str
|
||||
|
||||
|
||||
async def generate_day_schedule(server='cn'):
|
||||
events = await get_events(server, 0, 15)
|
||||
|
||||
has_prediction = False
|
||||
for event in events:
|
||||
if event['start_days'] > 0:
|
||||
has_prediction = True
|
||||
if has_prediction:
|
||||
im = create_image(len(events) + 2)
|
||||
else:
|
||||
im = create_image(len(events) + 1)
|
||||
|
||||
title = f'原神日历'
|
||||
pcr_now = get_pcr_now(0)
|
||||
draw_title(im, 0, title, pcr_now.strftime('%Y/%m/%d'), '正在进行')
|
||||
|
||||
if len(events) == 0:
|
||||
draw_item(im, 1, 1, '无数据', 0, False)
|
||||
i = 1
|
||||
for event in events:
|
||||
if event['start_days'] <= 0:
|
||||
draw_item(im, i, event['type'], event['title'],
|
||||
event['left_days'], event['forever'])
|
||||
i += 1
|
||||
if has_prediction:
|
||||
draw_title(im, i, right='即将开始')
|
||||
for event in events:
|
||||
if event['start_days'] > 0:
|
||||
i += 1
|
||||
draw_item(im, i, event['type'], event['title'], -
|
||||
event['start_days'], event['forever'])
|
||||
return im
|
Loading…
Reference in New Issue
Block a user