From 81ddbc30f40b0c1a82e0a61f302b04390c23b834 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 21 Aug 2023 00:31:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor=F0=9F=8E=A8:=20=E9=87=8D=E6=9E=84`?= =?UTF-8?q?=E7=BA=A2=E5=8C=85`=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- plugins/gold_redbag/__init__.py | 470 +++++++++---------- plugins/gold_redbag/config.py | 311 ++++++++++++ plugins/gold_redbag/data_source.py | 170 ++++--- plugins/open_cases/models/open_cases_user.py | 4 +- plugins/open_cases/open_cases_c.py | 28 +- services/db_context.py | 6 +- utils/utils.py | 10 +- 8 files changed, 671 insertions(+), 335 deletions(-) create mode 100644 plugins/gold_redbag/config.py diff --git a/README.md b/README.md index 5206db95..19483d49 100644 --- a/README.md +++ b/README.md @@ -331,9 +331,14 @@ PS: **ARM平台** 请使用全量版 同时 **如果你的机器 RAM < 1G 可能 ## 更新 -### 2022/8/20 +### 2023/8/28 + +* 重构`红包`功能,允许一个群聊中有多个用户发起的红包,发送`开`等命令会开启群中所有条件允许的红包,新增`红包结算排行`,在红包退回或抢完时统计,在`塞红包`时at可以发送专属红包 + +### 2023/8/20 * 修复词条回答包含at时使用模糊|正则等问时无法正确匹配问题 +* 修复开箱时最后开箱日期数据未更新 ### 2023/8/7 diff --git a/plugins/gold_redbag/__init__.py b/plugins/gold_redbag/__init__.py index ef9522f0..1cf9733c 100755 --- a/plugins/gold_redbag/__init__.py +++ b/plugins/gold_redbag/__init__.py @@ -1,6 +1,8 @@ import random +import re import time from datetime import datetime, timedelta +from typing import Dict, List, Optional from apscheduler.jobstores.base import JobLookupError from nonebot import on_command, on_notice @@ -21,15 +23,16 @@ from nonebot.rule import to_me from configs.config import NICKNAME from configs.path_config import IMAGE_PATH from services.log import logger -from utils.message_builder import image +from utils.depends import AtList, GetConfig +from utils.message_builder import at, image from utils.utils import is_number, scheduler +from .config import FESTIVE_KEY, GroupRedBag, RedBag from .data_source import ( + build_open_result_image, check_gold, - generate_open_redbag_pic, - generate_send_redbag_pic, - open_redbag, - return_gold, + end_festive_red_bag, + random_red_bag_background, ) __zx_plugin_name__ = "金币红包" @@ -37,7 +40,7 @@ __plugin_usage__ = """ usage: 在群内发送指定金额的红包,拼手气项目 指令: - 塞红包 [金币数] ?[红包数=5]: 塞入红包 + 塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包 开/抢/*戳一戳*: 打开红包 退回: 退回未开完的红包,必须在一分钟后使用 示例:塞红包 1000 @@ -51,7 +54,7 @@ usage: """.strip() __plugin_des__ = "运气项目又来了" __plugin_cmd__ = [ - "塞红包 [金币数] ?[红包数=5]", + "塞红包 [金币数] ?[红包数=5] ?[at指定人]", "开/抢", "退回", "节日红包 [金额] [数量] ?[祝福语] ?[指定群] [_superuser]", @@ -64,14 +67,35 @@ __plugin_settings__ = { "limit_superuser": False, "cmd": ["金币红包", "塞红包"], } -__plugin_resources__ = {"prts": IMAGE_PATH} +__plugin_cd_limit__ = {"rst": "急什么急什么,待会再发!"} +__plugin_configs__ = { + "DEFAULT_TIMEOUT": { + "value": 600, + "help": "普通红包默认超时时间", + "default_value": 600, + "type": int, + }, + "DEFAULT_INTERVAL": { + "value": 60, + "help": "用户发送普通红包最小间隔时间", + "default_value": 60, + "type": int, + }, + "RANK_NUM": { + "value": 10, + "help": "结算排行显示前N位", + "default_value": 10, + "type": int, + }, +} +# __plugin_resources__ = {"prts": IMAGE_PATH} async def rule(event: GroupMessageEvent) -> bool: return check_on_gold_red(event) -gold_redbag = on_command( +gold_red_bag = on_command( "塞红包", aliases={"金币红包"}, priority=5, block=True, permission=GROUP ) @@ -87,167 +111,197 @@ festive_redbag = on_command( "节日红包", priority=5, block=True, permission=SUPERUSER, rule=to_me() ) -redbag_data = {} +GROUP_DATA: Dict[int, GroupRedBag] = {} -festive_redbag_data = {} +PATTERN = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~,。;‘、""" # 阻断其他poke -@run_preprocessor +# @run_preprocessor +# async def _( +# matcher: Matcher, +# event: PokeNotifyEvent, +# ): +# try: +# if matcher.type == "notice" and event.self_id == event.target_id: +# flag = check_on_gold_red(event) +# if flag: +# if matcher.plugin_name == "poke": +# raise IgnoredException("目前正在抢红包...") +# else: +# if matcher.plugin_name == "gold_red_bag": +# raise IgnoredException("目前没有红包...") +# except AttributeError: +# pass + + +@gold_red_bag.handle() async def _( - matcher: Matcher, - event: PokeNotifyEvent, + bot: Bot, + event: GroupMessageEvent, + arg: Message = CommandArg(), + at_list: List[int] = AtList(), + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), ): - try: - if matcher.type == "notice" and event.self_id == event.target_id: - flag = check_on_gold_red(event) - if flag: - if matcher.plugin_name == "poke": - raise IgnoredException("目前正在抢红包...") - else: - if matcher.plugin_name == "gold_redbag": - raise IgnoredException("目前没有红包...") - except AttributeError: - pass - - -@gold_redbag.handle() -async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()): - global redbag_data, festive_redbag_data - try: - if time.time() - redbag_data[event.group_id]["time"] > 60: - amount = ( - redbag_data[event.group_id]["amount"] - - redbag_data[event.group_id]["open_amount"] - ) - await return_gold( - redbag_data[event.group_id]["user_id"], str(event.group_id), amount - ) - await gold_redbag.send( - f'{redbag_data[event.group_id]["nickname"]}的红包过时未开完,退还{amount}金币...' - ) - redbag_data[event.group_id] = {} - else: - await gold_redbag.finish( - f'目前 {redbag_data[event.group_id]["nickname"]} 的红包还没有开完噢,' - f'还剩下 {len(redbag_data[event.group_id]["redbag"])} 个红包!' - f'(或等待{str(60 - time.time() + redbag_data[event.group_id]["time"])[:2]}秒红包过时)' - ) - except KeyError: - pass - msg = arg.extract_plain_text().strip() - msg = msg.split() + group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) + if not group_red_bag: + group_red_bag = GroupRedBag(event.group_id) + GROUP_DATA[event.group_id] = group_red_bag + # 剩余过期时间 + time_remaining = group_red_bag.check_timeout(event.user_id) + if time_remaining != -1: + # 判断用户红包是否存在且是否过时覆盖 + if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): + now = time.time() + if now < user_red_bag.start_time + default_interval: + await gold_red_bag.finish( + f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." + f"(或等待{time_remaining}秒红包cd)" + ) + msg = arg.extract_plain_text().strip().split() + if not msg: + await gold_red_bag.finish("不塞钱发什么红包!") + amount = msg[0] if len(msg) == 1: - flag, amount = await check_gold(str(event.user_id), str(event.group_id), msg[0]) + flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) if not flag: - await gold_redbag.finish(str(amount)) + await gold_red_bag.finish(tip, at_sender=True) num = 5 else: - amount = msg[0] num = msg[1] if not is_number(num) or int(num) < 1: - await gold_redbag.finish("红包个数给我输正确啊!", at_sender=True) - flag, amount = await check_gold(str(event.user_id), str(event.group_id), amount) + await gold_red_bag.finish("红包个数给我输正确啊!", at_sender=True) + flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) if not flag: - await gold_redbag.finish(str(amount), at_sender=True) + await gold_red_bag.finish(tip, at_sender=True) group_member_num = (await bot.get_group_info(group_id=event.group_id))[ "member_count" ] num = int(num) if num > group_member_num: - await gold_redbag.send("你发的红包数量也太多了,已经为你修改成与本群人数相同的红包数量...") + await gold_red_bag.send("你发的红包数量也太多了,已经为你修改成与本群人数相同的红包数量...") num = group_member_num nickname = event.sender.card or event.sender.nickname - flag, result = init_redbag( + await group_red_bag.add_red_bag( + f"{nickname}的红包", + int(amount), + 1 if at_list else num, + nickname or "", str(event.user_id), - str(event.group_id), - nickname or str(event.user_id), - amount, - num, - int(bot.self_id), + assigner=str(at_list[0]) if at_list else None, + ) + await gold_red_bag.send( + f"{nickname}发起了金币红包\n金额: {amount}\n数量: {num}\n" + + image(await random_red_bag_background(event.user_id)) ) - if not flag: - await gold_redbag.finish(result, at_sender=True) - else: - await gold_redbag.send( - f"{nickname}发起了金币红包\n金额:{amount}\n数量:{num}\n" - + image( - b64=await generate_send_redbag_pic( - redbag_data[str(event.group_id)]["user_id"] - ) - ) - ) logger.info(f"塞入 {num} 个红包,共 {amount} 金币", "金币红包", event.user_id, event.group_id) @open_.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - global redbag_data, festive_redbag_data - msg = arg.extract_plain_text().strip() - msg = ( - msg.replace("!", "") - .replace("!", "") - .replace(",", "") - .replace(",", "") - .replace(".", "") - .replace("。", "") - ) - if msg: +async def _( + event: GroupMessageEvent, + arg: Message = CommandArg(), + rank_num: int = GetConfig(config="RANK_NUM"), +): + if msg := arg.extract_plain_text().strip(): + msg = re.sub(PATTERN, "", msg) if "红包" not in msg: return - try: - await open_.send( - image(b64=await get_redbag_img(str(event.user_id), str(event.group_id))), - at_sender=True, - ) - except KeyError: - await open_.finish("真贪心,明明已经开过这个红包了的说...", at_sender=True) + group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) + if group_red_bag: + open_data, settlement_list = await group_red_bag.open(event.user_id) + send_msg = "" + for _, item in open_data.items(): + amount, red_bag = item + result_image = await build_open_result_image(red_bag, event.user_id, amount) + send_msg += ( + f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" + + image(result_image) + + "\n" + ) + logger.info( + f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", + "开红包", + event.user_id, + event.group_id, + ) + send_msg = send_msg[:-1] if send_msg else "没有红包给你开!" + await open_.send(send_msg, at_sender=True) + if settlement_list: + for red_bag in settlement_list: + await open_.send( + f"{red_bag.name}已结算\n" + + image(await red_bag.build_amount_rank(rank_num)) + ) -@poke_.handle() -async def _poke_(event: PokeNotifyEvent): - global redbag_data, festive_redbag_data - if event.self_id == event.target_id: - flag = check_on_gold_red(event) - if not flag: - return - await poke_.send( - image(b64=await get_redbag_img(str(event.user_id), str(event.group_id))), - at_sender=True, - ) +# @poke_.handle() +# async def _poke_(event: PokeNotifyEvent): +# group_id = getattr(event, "group_id", None) +# if event.self_id == event.target_id and group_id: +# is_open = check_on_gold_red(event) +# if not is_open: +# return +# group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(group_id) +# if group_red_bag: +# open_data, settlement_list = await group_red_bag.open(event.user_id) +# send_msg = "" +# for _, item in open_data.items(): +# amount, red_bag = item +# result_image = await build_open_result_image( +# red_bag, event.user_id, amount +# ) +# send_msg += ( +# f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" +# + image(result_image) +# + "\n" +# ) +# logger.info( +# f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", +# "开红包", +# event.user_id, +# event.group_id, +# ) +# if send_msg: +# await open_.send(send_msg, at_sender=True) +# if settlement_list: +# for red_bag in settlement_list: +# await open_.send( +# f"{red_bag.name}已结算\n" +# + image(await red_bag.build_amount_rank()) +# ) @return_.handle() -async def _(event: GroupMessageEvent): - global redbag_data - try: - if redbag_data[event.group_id]["user_id"] != event.user_id: - await return_.finish("不是你的红包你退回什么!", at_sender=True) - if time.time() - redbag_data[event.group_id]["time"] <= 60: - await return_.finish( - f'你的红包还没有过时,在 {str(60 - time.time() + redbag_data[event.group_id]["time"])[:2]} ' - f"秒后可以退回..", - at_sender=True, - ) - await return_gold( - str(event.user_id), - str(event.group_id), - redbag_data[event.group_id]["amount"] - - redbag_data[event.group_id]["open_amount"], - ) - await return_.send( - f"已成功退还了 " - f"{redbag_data[event.group_id]['amount'] - redbag_data[event.group_id]['open_amount']} " - f"金币", - at_sender=True, - ) - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 退回了" - f"红包 {redbag_data[event.group_id]['amount'] - redbag_data[event.group_id]['open_amount']} 金币" - ) - redbag_data[event.group_id] = {} - except KeyError: - await return_.finish("目前没有红包可以退回...", at_sender=True) +async def _( + event: GroupMessageEvent, + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), + rank_num: int = GetConfig(config="RANK_NUM"), +): + group_red_bag: GroupRedBag = GROUP_DATA[event.group_id] + if group_red_bag: + if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): + now = time.time() + if now - user_red_bag.start_time < default_interval: + await return_.finish( + f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " + f"秒后可以退回...", + at_sender=True, + ) + user_red_bag = group_red_bag.get_user_red_bag(event.user_id) + if user_red_bag and ( + return_amount := await group_red_bag.settlement(event.user_id) + ): + logger.info( + f"退回了红包 {return_amount} 金币", "红包退回", event.user_id, event.group_id + ) + await return_.send( + f"已成功退还了 " + f"{return_amount} 金币\n" + + image(await user_red_bag.build_amount_rank(rank_num)), + at_sender=True, + ) + await return_.send("目前没有红包可以退回...", at_sender=True) @festive_redbag.handle() @@ -280,138 +334,40 @@ async def _(bot: Bot, arg: Message = CommandArg()): gl = await bot.get_group_list() gl = [g["group_id"] for g in gl] for g in gl: + group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(g) + if not group_red_bag: + group_red_bag = GroupRedBag(g) + GROUP_DATA[g] = group_red_bag try: - scheduler.remove_job(f"festive_redbag_{g}") - await end_festive_redbag(bot, g) + scheduler.remove_job(f"{FESTIVE_KEY}_{g}") + await end_festive_red_bag(bot, group_red_bag) except JobLookupError: pass - init_redbag(bot.self_id, g, f"{NICKNAME}", amount, num, int(bot.self_id), 1) + await group_red_bag.add_red_bag( + f"{NICKNAME}的红包", int(amount), num, NICKNAME, FESTIVE_KEY, True + ) scheduler.add_job( - end_festive_redbag, + end_festive_red_bag, "date", - run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), - id=f"festive_redbag_{g}", - args=[bot, g], + # run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), + run_date=(datetime.now() + timedelta(seconds=30)).replace( + microsecond=0 + ), + id=f"{FESTIVE_KEY}_{g}", + args=[bot, group_red_bag], ) try: await bot.send_group_msg( group_id=g, message=f"{NICKNAME}发起了金币红包\n金额:{amount}\n数量:{num}\n" - + image( - b64=await generate_send_redbag_pic(int(bot.self_id), greetings) - ), + + image(await random_red_bag_background(bot.self_id, greetings)), ) + logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) except ActionFailed: - logger.warning(f"节日红包 GROUP {g} 发送失败..") - pass - - -# 红包数据初始化 -def init_redbag( - user_id: str, - group_id: str, - nickname: str, - amount: int, - num: int, - bot_self_id: int, - mode: int = 0, -): - global redbag_data, festive_redbag_data - data = redbag_data if mode == 0 else festive_redbag_data - if not data.get(group_id): - data[group_id] = {} - try: - if data[group_id]["user_id"] and user_id != bot_self_id: - return False, f'{data[group_id]["nickname"]}的红包还没抢完呢...' - except KeyError: - pass - data[group_id]["user_id"] = user_id - data[group_id]["nickname"] = nickname - data[group_id]["amount"] = amount - data[group_id]["num"] = num - data[group_id]["open_amount"] = 0 - data[group_id]["time"] = time.time() - data[group_id]["redbag"] = random_redbag(amount, num) - data[group_id]["open_user"] = [] - if mode == 0: - redbag_data = data - else: - festive_redbag_data = data - return True, "" - - -# 随机红包排列 -def random_redbag(amount: int, num: int) -> list: - redbag_lst = [] - for _ in range(num - 1): - tmp = int(amount / random.choice(range(3, num + 3))) - redbag_lst.append(tmp) - amount -= tmp - redbag_lst.append(amount) - return redbag_lst - - -# 返回开红包图片 -async def get_redbag_img(user_id: str, group_id: str): - global redbag_data, festive_redbag_data - data = redbag_data - mode = 0 - if festive_redbag_data.get(group_id): - try: - if user_id not in festive_redbag_data[group_id]["open_user"]: - data = festive_redbag_data - mode = 1 - except KeyError: - pass - amount, data = await open_redbag(user_id, group_id, data) - text = ( - f"已领取" - f'{data[group_id]["num"] - len(data[group_id]["redbag"])}' - f'/{data[group_id]["num"]}个,' - f'共{data[group_id]["open_amount"]}/{data[group_id]["amount"]}金币' - ) - logger.info( - f"USER {user_id} GROUP {group_id} 抢到了 {data[group_id]['user_id']} 的红包,获取了{amount}金币" - ) - b64 = await generate_open_redbag_pic( - data[group_id]["user_id"], data[group_id]["nickname"], amount, text - ) - if data[group_id]["open_amount"] == data[group_id]["amount"]: - data[group_id] = {} - if mode == 0: - redbag_data = data - else: - festive_redbag_data = data - return b64 - - -async def end_festive_redbag(bot: Bot, group_id: int): - global festive_redbag_data - message = ( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{festive_redbag_data[group_id]['num'] - len(festive_redbag_data[group_id]['redbag'])}" - f" 个红包,共 {festive_redbag_data[group_id]['open_amount']} 金币" - ) - await bot.send_group_msg(group_id=group_id, message=message) - festive_redbag_data[group_id] = {} + logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) def check_on_gold_red(event) -> bool: - flag1 = True - flag2 = True - try: - if festive_redbag_data[str(event.group_id)]["user_id"]: - if event.user_id in festive_redbag_data[str(event.group_id)]["open_user"]: - flag1 = False - except KeyError: - flag1 = False - try: - if redbag_data[str(event.group_id)]["user_id"]: - if event.user_id in redbag_data[str(event.group_id)]["open_user"]: - flag2 = False - except KeyError: - flag2 = False - if flag1 or flag2: - return True - else: - return False + if group_red_bag := GROUP_DATA.get(event.group_id): + return group_red_bag.check_open(event.user_id) + return False diff --git a/plugins/gold_redbag/config.py b/plugins/gold_redbag/config.py new file mode 100644 index 00000000..a2fe4c55 --- /dev/null +++ b/plugins/gold_redbag/config.py @@ -0,0 +1,311 @@ +import random +import time +from datetime import datetime +from io import BytesIO +from typing import Dict, List, Optional, Tuple, Union, overload + +from pydantic import BaseModel + +from models.bag_user import BagUser +from models.group_member_info import GroupInfoUser +from plugins.gold_redbag.model import RedbagUser +from utils.image_utils import BuildImage +from utils.utils import get_user_avatar + +FESTIVE_KEY = "FESTIVE" +"""节日红包KEY""" + + +class RedBag(BaseModel): + + """ + 红包 + """ + + group_id: str + """所属群聊""" + name: str + """红包名称""" + amount: int + """总金币""" + num: int + """红包数量""" + promoter: str + """发起人昵称""" + promoter_id: str + """发起人id""" + is_festival: bool + """是否为节日红包""" + timeout: int + """过期时间""" + assigner: Optional[str] = None + """指定人id""" + start_time: float + """红包发起时间""" + open_user: Dict[str, int] = {} + """开启用户""" + red_bag_list: List[int] + + async def build_amount_rank(self, num: int = 10) -> BuildImage: + """生成结算红包图片 + + 参数: + num: 查看的排名数量. + + 返回: + BuildImage: 结算红包图片 + """ + user_image_list = [] + if self.open_user: + sort_data = sorted( + self.open_user.items(), key=lambda item: item[1], reverse=True + ) + num = num if num < len(self.open_user) else len(self.open_user) + user_id_list = [sort_data[i][0] for i in range(num)] + group_user_list = await GroupInfoUser.filter( + group_id=self.group_id, user_id__in=user_id_list + ).all() + for i in range(num): + user_background = BuildImage(600, 100, font_size=30) + user_id, amount = sort_data[i] + user_ava_bytes = await get_user_avatar(user_id) + user_ava = None + if user_ava_bytes: + user_ava = BuildImage(80, 80, background=BytesIO(user_ava_bytes)) + else: + user_ava = BuildImage(80, 80) + await user_ava.acircle_corner(10) + await user_background.apaste(user_ava, (130, 10), True) + no_image = BuildImage(100, 100, font_size=65, font="CJGaoDeGuo.otf") + await no_image.atext((0, 0), f"{i+1}", center_type="center") + await no_image.aline((99, 10, 99, 90), "#b9b9b9") + await user_background.apaste(no_image) + name = [ + user.user_name + for user in group_user_list + if user_id == user.user_id + ] + await user_background.atext((225, 15), name[0] if name else "") + amount_image = BuildImage( + 0, 0, plain_text=f"{amount} 元", font_size=30, font_color="#cdac72" + ) + await user_background.apaste( + amount_image, (user_background.w - amount_image.w - 20, 50), True + ) + await user_background.aline((225, 99, 590, 99), "#b9b9b9") + user_image_list.append(user_background) + background = BuildImage(600, 150 + len(user_image_list) * 100) + top = BuildImage(600, 100, color="#f55545", font_size=30) + promoter_ava_bytes = await get_user_avatar(self.promoter_id) + promoter_ava = None + if promoter_ava_bytes: + promoter_ava = BuildImage(60, 60, background=BytesIO(promoter_ava_bytes)) + else: + promoter_ava = BuildImage(60, 60) + await promoter_ava.acircle() + await top.apaste(promoter_ava, (10, 0), True, "by_height") + await top.atext((80, 33), self.name, (255, 255, 255)) + right_text = BuildImage(150, 100, color="#f55545", font_size=30) + await right_text.atext((10, 33), "结算排行", (255, 255, 255)) + await right_text.aline((4, 10, 4, 90), (255, 255, 255), 2) + await top.apaste(right_text, (460, 0)) + await background.apaste(top) + cur_h = 110 + for user_image in user_image_list: + await background.apaste(user_image, (0, cur_h)) + cur_h += user_image.h + return background + + +class GroupRedBag: + + """ + 群组红包管理 + """ + + def __init__(self, group_id: Union[int, str]): + self.group_id = str(group_id) + self._data: Dict[str, RedBag] = {} + """红包列表""" + + def get_user_red_bag(self, user_id: Union[str, int]) -> Optional[RedBag]: + """获取用户塞红包数据 + + 参数: + user_id: 用户id + + 返回: + Optional[RedBag]: RedBag + """ + return self._data.get(str(user_id)) + + def check_open(self, user_id: Union[str, int]) -> bool: + """检查是否有可开启的红包 + + 参数: + user_id: 用户id + + 返回: + bool: 是否有可开启的红包 + """ + user_id = str(user_id) + for _, red_bag in self._data.items(): + if red_bag.assigner: + if red_bag.assigner == user_id: + return True + else: + if user_id not in red_bag.open_user: + return True + return False + + def check_timeout(self, user_id: Union[int, str]) -> int: + """判断用户红包是否过期 + + 参数: + user_id: 用户id + + 返回: + int: 距离过期时间 + """ + user_id = str(user_id) + if user_id in self._data: + reg_bag = self._data[user_id] + now = time.time() + if now < reg_bag.timeout + reg_bag.start_time: + return int(reg_bag.timeout + reg_bag.start_time - now) + return -1 + + async def open( + self, user_id: Union[int, str] + ) -> Tuple[Dict[str, Tuple[int, RedBag]], List[RedBag]]: + """开启红包 + + 参数: + user_id: 用户id + + 返回: + Dict[str, Tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag + List[RedBag]: 开完的红包 + """ + user_id = str(user_id) + open_data = {} + settlement_list: List[RedBag] = [] + for _, red_bag in self._data.items(): + if red_bag.num > len(red_bag.open_user): + is_open = False + if red_bag.assigner: + is_open = red_bag.assigner == user_id + else: + is_open = user_id not in red_bag.open_user + if is_open: + random_amount = red_bag.red_bag_list.pop() + await RedbagUser.add_redbag_data( + user_id, self.group_id, "get", random_amount + ) + await BagUser.add_gold(user_id, self.group_id, random_amount) + red_bag.open_user[user_id] = random_amount + open_data[red_bag.promoter_id] = (random_amount, red_bag) + if red_bag.num == len(red_bag.open_user): + # 红包开完,结算 + settlement_list.append(red_bag) + if settlement_list: + for uid in [red_bag.promoter_id for red_bag in settlement_list]: + if uid in self._data: + del self._data[uid] + return open_data, settlement_list + + def festive_red_bag_expire(self) -> Optional[RedBag]: + """节日红包过期 + + 返回: + Optional[RedBag]: 过期的节日红包 + """ + if FESTIVE_KEY in self._data: + red_bag = self._data[FESTIVE_KEY] + del self._data[FESTIVE_KEY] + return red_bag + return None + + async def settlement( + self, user_id: Optional[Union[int, str]] = None + ) -> Optional[int]: + """红包退回 + + 参数: + user_id: 用户id, 指定id时结算指定用户红包. + + 返回: + int: 退回金币 + """ + user_id = str(user_id) + if user_id: + if red_bag := self._data.get(user_id): + del self._data[user_id] + if red_bag.red_bag_list: + # 退还剩余金币 + if amount := sum(red_bag.red_bag_list): + await BagUser.add_gold(user_id, self.group_id, amount) + return amount + return None + + async def add_red_bag( + self, + name: str, + amount: int, + num: int, + promoter: str, + promoter_id: str, + is_festival: bool = False, + timeout: int = 60, + assigner: Optional[str] = None, + ): + """添加红包 + + 参数: + name: 红包名称 + amount: 金币数量 + num: 红包数量 + promoter: 发起人昵称 + promoter_id: 发起人id + is_festival: 是否为节日红包. + timeout: 超时时间. + assigner: 指定人. + """ + user_gold = await BagUser.get_gold(promoter_id, self.group_id) + if not is_festival and (amount < 1 or user_gold < amount): + raise ValueError("红包金币不足或用户金币不足") + red_bag_list = self._random_red_bag(amount, num) + if not is_festival: + await BagUser.spend_gold(promoter_id, self.group_id, amount) + await RedbagUser.add_redbag_data(promoter_id, self.group_id, "send", amount) + self._data[promoter_id] = RedBag( + group_id=self.group_id, + name=name, + amount=amount, + num=num, + promoter=promoter, + promoter_id=promoter_id, + is_festival=is_festival, + timeout=timeout, + start_time=time.time(), + assigner=assigner, + red_bag_list=red_bag_list, + ) + + def _random_red_bag(self, amount: int, num: int) -> List[int]: + """初始化红包金币 + + 参数: + amount: 金币数量 + num: 红包数量 + + 返回: + List[int]: 红包列表 + """ + red_bag_list = [] + for _ in range(num - 1): + tmp = int(amount / random.choice(range(3, num + 3))) + red_bag_list.append(tmp) + amount -= tmp + red_bag_list.append(amount) + return red_bag_list diff --git a/plugins/gold_redbag/data_source.py b/plugins/gold_redbag/data_source.py index 1eb8de59..e2e02bc7 100755 --- a/plugins/gold_redbag/data_source.py +++ b/plugins/gold_redbag/data_source.py @@ -1,19 +1,51 @@ -import asyncio import os import random from io import BytesIO -from typing import Union +from typing import Tuple, Union +from nonebot.adapters.onebot.v11 import Bot + +from configs.config import NICKNAME, Config from configs.path_config import IMAGE_PATH from models.bag_user import BagUser from utils.image_utils import BuildImage +from utils.message_builder import image from utils.utils import get_user_avatar, is_number -from .model import RedbagUser +from .config import FESTIVE_KEY, GroupRedBag, RedBag -# 检查金币数量合法性,并添加记录数据 -async def check_gold(user_id: str, group_id: str, amount: Union[str, int]): +async def end_festive_red_bag(bot: Bot, group_red_bag: GroupRedBag): + """结算节日红包 + + 参数: + bot: Bot + group_red_bag: GroupRedBag + """ + if festive_red_bag := group_red_bag.festive_red_bag_expire(): + rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 + rank_image = await festive_red_bag.build_amount_rank(rank_num) + message = ( + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" + image(rank_image) + ) + await bot.send_group_msg(group_id=int(group_red_bag.group_id), message=message) + + +async def check_gold( + user_id: str, group_id: str, amount: Union[str, int] +) -> Tuple[bool, str]: + """检查金币数量是否合法 + + 参数: + user_id: 用户id + group_id: 群聊id + amount: 金币数量 + + 返回: + Tuple[bool, str]: 是否合法以及提示语 + """ if is_number(amount): amount = int(amount) user_gold = await BagUser.get_gold(user_id, group_id) @@ -21,88 +53,102 @@ async def check_gold(user_id: str, group_id: str, amount: Union[str, int]): return False, "小气鬼,要别人倒贴金币给你嘛!" if user_gold < amount: return False, "没有金币的话请不要发红包..." - await BagUser.spend_gold(user_id, group_id, amount) - await RedbagUser.add_redbag_data(user_id, group_id, "send", amount) - return True, amount + return True, "" else: return False, "给我好好的输入红包里金币的数量啊喂!" -# 金币退回 -async def return_gold(user_id: str, group_id: str, amount: int): - await BagUser.add_gold(user_id, group_id, amount) +async def random_red_bag_background( + user_id: Union[str, int], msg="恭喜发财 大吉大利" +) -> BuildImage: + """构造发送红包图片 + 参数: + user_id: 用户id + msg: 红包消息. -# 开红包 -async def open_redbag(user_id: str, group_id: str, redbag_data: dict): - amount = random.choice(redbag_data[group_id]["redbag"]) - redbag_data[group_id]["redbag"].remove(amount) - redbag_data[group_id]["open_user"].append(user_id) - redbag_data[group_id]["open_amount"] += amount - await RedbagUser.add_redbag_data(user_id, group_id, "get", amount) - await BagUser.add_gold(user_id, group_id, amount) - return amount, redbag_data + 异常: + ValueError: 图片背景列表为空 - -# 随机红包图片 -async def generate_send_redbag_pic(user_id: int, msg: str = "恭喜发财 大吉大利"): - random_redbag = random.choice(os.listdir(f"{IMAGE_PATH}/prts/redbag_2")) + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) redbag = BuildImage( - 0, 0, font_size=38, background=f"{IMAGE_PATH}/prts/redbag_2/{random_redbag}" + 0, 0, font_size=38, background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag ) - ava = BuildImage(65, 65, background=BytesIO(await get_user_avatar(user_id))) - await asyncio.get_event_loop().run_in_executor(None, ava.circle) - redbag.text( + ava_byte = await get_user_avatar(user_id) + ava = None + if ava_byte: + ava = BuildImage(65, 65, background=BytesIO(ava_byte)) + else: + ava = BuildImage(65, 65, color=(0, 0, 0), is_alpha=True) + await ava.acircle() + await redbag.atext( (int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210), msg, (240, 218, 164) ) - redbag.paste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130), True) - return redbag.pic2bs4() + await redbag.apaste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130), True) + return redbag -# 开红包图片 -async def generate_open_redbag_pic( - user_id: str, send_user_nickname: str, amount: int, text: str -): - return await asyncio.create_task( - _generate_open_redbag_pic(user_id, send_user_nickname, amount, text) - ) +async def build_open_result_image( + red_bag: RedBag, user_id: Union[int, str], amount: int +) -> BuildImage: + """构造红包开启图片 + 参数: + red_bag: RedBag + user_id: 开启红包用户id + amount: 开启红包获取的金额 -# 开红包图片 -async def _generate_open_redbag_pic( - user_id: str, send_user_nickname: str, amount: int, text: str -): - send_user_nickname += "的红包" - amount_str = str(amount) - random_redbag = random.choice(os.listdir(f"{IMAGE_PATH}/prts/redbag_1")) + 异常: + ValueError: 图片背景列表为空 + + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) head = BuildImage( 1000, 980, font_size=30, - background=f"{IMAGE_PATH}/prts/redbag_1/{random_redbag}", + background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag, ) - size = BuildImage(0, 0, font_size=50).getsize(send_user_nickname) - # QQ头像 + size = BuildImage(0, 0, font_size=50).getsize(red_bag.name) ava_bk = BuildImage(100 + size[0], 66, is_alpha=True, font_size=50) - ava = BuildImage( - 66, 66, is_alpha=True, background=BytesIO(await get_user_avatar(user_id)) - ) - ava_bk.paste(ava) - ava_bk.text((100, 7), send_user_nickname) - # ava_bk.show() + + ava_byte = await get_user_avatar(user_id) + ava = None + if ava_byte: + ava = BuildImage(66, 66, is_alpha=True, background=BytesIO(ava_byte)) + else: + ava = BuildImage(66, 66, color=(0, 0, 0), is_alpha=True) + await ava_bk.apaste(ava) + ava_bk.text((100, 7), red_bag.name) ava_bk_w, ava_bk_h = ava_bk.size - head.paste(ava_bk, (int((1000 - ava_bk_w) / 2), 300), alpha=True) - # 金额 - size = BuildImage(0, 0, font_size=150).getsize(amount_str) - price = BuildImage(size[0], size[1], is_alpha=True, font_size=150) - price.text((0, 0), amount_str, fill=(209, 171, 108)) + await head.apaste(ava_bk, (int((1000 - ava_bk_w) / 2), 300), alpha=True) + size = BuildImage(0, 0, font_size=150).getsize(amount) + amount_image = BuildImage(size[0], size[1], is_alpha=True, font_size=150) + await amount_image.atext((0, 0), str(amount), fill=(209, 171, 108)) # 金币中文 - head.paste(price, (int((1000 - size[0]) / 2) - 50, 460), alpha=True) - head.text( + await head.apaste(amount_image, (int((1000 - size[0]) / 2) - 50, 460), alpha=True) + await head.atext( (int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70), "金币", fill=(209, 171, 108), ) # 剩余数量和金额 - head.text((350, 900), text, (198, 198, 198)) - return head.pic2bs4() + text = ( + f"已领取" + f"{red_bag.num - len(red_bag.open_user)}" + f"/{red_bag.num}个," + f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币" + ) + await head.atext((350, 900), text, (198, 198, 198)) + return head diff --git a/plugins/open_cases/models/open_cases_user.py b/plugins/open_cases/models/open_cases_user.py index 6f3482a1..3b488717 100755 --- a/plugins/open_cases/models/open_cases_user.py +++ b/plugins/open_cases/models/open_cases_user.py @@ -1,3 +1,5 @@ +from datetime import datetime + from tortoise import fields from services.db_context import Model @@ -39,7 +41,7 @@ class OpenCasesUser(Model): """赚取金币""" today_open_total: int = fields.IntField(default=0) """今日开箱数量""" - open_cases_time_last = fields.DatetimeField() + open_cases_time_last: datetime = fields.DatetimeField() """最后开箱日期""" knifes_name: str = fields.TextField(default="") """已获取金色""" diff --git a/plugins/open_cases/open_cases_c.py b/plugins/open_cases/open_cases_c.py index be2d9596..3bbea16d 100755 --- a/plugins/open_cases/open_cases_c.py +++ b/plugins/open_cases/open_cases_c.py @@ -122,7 +122,10 @@ async def open_case(user_id: str, group_id: int, case_name: str) -> Union[str, M case_price = case_skin.sell_min_price user.today_open_total += 1 user.total_count += 1 - await user.save(update_fields=["today_open_total", "total_count"]) + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) add_count(user, skin, case_price) ridicule_result = random.choice(RESULT_MESSAGE[skin.color]) price_result = skin.sell_min_price @@ -203,7 +206,10 @@ async def open_multiple_case( now = datetime.now() user.today_open_total += num user.total_count += num - await user.save(update_fields=["today_open_total", "total_count"]) + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) case_price = 0 if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): case_price = case_skin.sell_min_price @@ -275,8 +281,12 @@ def _handle_is_MAX_COUNT() -> str: return f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" -async def total_open_statistics(user_id: str, group: str) -> str: - user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group) +async def total_open_statistics( + user_id: Union[str, int], group_id: Union[str, int] +) -> str: + user, _ = await OpenCasesUser.get_or_create( + user_id=str(user_id), group_id=str(group_id) + ) return ( f"开箱总数:{user.total_count}\n" f"今日开箱:{user.today_open_total}\n" @@ -296,8 +306,8 @@ async def total_open_statistics(user_id: str, group: str) -> str: ) -async def group_statistics(group: str): - user_list = await OpenCasesUser.filter(group_id=group).all() +async def group_statistics(group_id: Union[int, str]): + user_list = await OpenCasesUser.filter(group_id=str(group_id)).all() # lan zi fen hong jin pricei uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0] for user in user_list: @@ -332,7 +342,9 @@ async def group_statistics(group: str): ) -async def get_my_knifes(user_id: str, group_id: str) -> Union[str, MessageSegment]: +async def get_my_knifes( + user_id: Union[str, int], group_id: Union[str, int] +) -> Union[str, MessageSegment]: """获取我的金色 Args: @@ -342,7 +354,7 @@ async def get_my_knifes(user_id: str, group_id: str) -> Union[str, MessageSegmen Returns: Union[str, MessageSegment]: 回复消息或图片 """ - data_list = await get_old_knife(user_id, group_id) + data_list = await get_old_knife(str(user_id), str(group_id)) data_list += await OpenCasesLog.filter( user_id=user_id, group_id=group_id, color="KNIFE" ).all() diff --git a/services/db_context.py b/services/db_context.py index 7d6aec07..20afb41d 100755 --- a/services/db_context.py +++ b/services/db_context.py @@ -48,7 +48,11 @@ async def init(): if not i_bind: i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" try: - await Tortoise.init(db_url=i_bind, modules={"models": MODELS}) + await Tortoise.init( + db_url=i_bind, + modules={"models": MODELS}, + # timezone="Asia/Shanghai" + ) await Tortoise.generate_schemas() logger.info(f"Database loaded successfully!") except Exception as e: diff --git a/utils/utils.py b/utils/utils.py index 1a9ba826..aca0abbc 100755 --- a/utils/utils.py +++ b/utils/utils.py @@ -411,7 +411,7 @@ def is_chinese(word: str) -> bool: return True -async def get_user_avatar(qq: int) -> Optional[bytes]: +async def get_user_avatar(qq: Union[int, str]) -> Optional[bytes]: """ 说明: 快捷获取用户头像 @@ -423,8 +423,8 @@ async def get_user_avatar(qq: int) -> Optional[bytes]: for _ in range(3): try: return (await client.get(url)).content - except TimeoutError: - pass + except Exception as e: + logger.error("获取用户头像错误", "Util", target=qq) return None @@ -440,8 +440,8 @@ async def get_group_avatar(group_id: int) -> Optional[bytes]: for _ in range(3): try: return (await client.get(url)).content - except TimeoutError: - pass + except Exception as e: + logger.error("获取群头像错误", "Util", target=group_id) return None