diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ac26fb9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Paimon_Calender/__init__.py b/Paimon_Calender/__init__.py new file mode 100644 index 0000000..45bea90 --- /dev/null +++ b/Paimon_Calender/__init__.py @@ -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'(?Pon|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 + ) diff --git a/res/wqy-microhei.ttc b/res/wqy-microhei.ttc new file mode 100644 index 0000000..2c9bc2d Binary files /dev/null and b/res/wqy-microhei.ttc differ diff --git a/utils/config.py b/utils/config.py index 3495489..67d985f 100644 --- a/utils/config.py +++ b/utils/config.py @@ -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() diff --git a/utils/draw.py b/utils/draw.py new file mode 100644 index 0000000..2a37248 --- /dev/null +++ b/utils/draw.py @@ -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) diff --git a/utils/event.py b/utils/event.py new file mode 100644 index 0000000..0fe8436 --- /dev/null +++ b/utils/event.py @@ -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()) diff --git a/utils/generate.py b/utils/generate.py new file mode 100644 index 0000000..6699fe9 --- /dev/null +++ b/utils/generate.py @@ -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