async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member, image: ElementResult, resize: RegexResult) -> Optional[MessageItem]: @Waiter.create_using_function(listening_events=[GroupMessage]) async def image_waiter(waiter_group: Group, waiter_member: Member, waiter_message: MessageChain): if waiter_group.id == group.id and waiter_member.id == member.id: if waiter_message.has(Image): return await waiter_message.getFirst(Image).get_bytes() else: return False if not enable: return MessageItem(MessageChain.create([Plain(text="功能未开启!")]), QuoteSource()) if image.matched: image_data = await image.result.get_bytes() else: try: await app.sendMessage(group, MessageChain.create("请在30s内发送要处理的图片"), quote=message[Source][0]) image_data = await asyncio.wait_for(inc.wait(image_waiter), 30) if not image_data: return MessageItem( MessageChain.create([Plain(text="未检测到图片,请重新发送,进程退出")]), QuoteSource()) except asyncio.TimeoutError: return MessageItem( MessageChain.create([Plain(text="图片等待超时,进程退出")]), QuoteSource()) if SuperResolution.processing: return MessageItem( MessageChain.create([Plain(text="有任务正在处理中,请稍后重试")]), QuoteSource()) await mutex.acquire() SuperResolution.processing = True mutex.release() await app.sendMessage(group, MessageChain.create([Plain(text="已收到图片,启动处理进程") ]), quote=message[Source][0]) try: return await SuperResolution.super_resolution( image_data, resize.matched) except RuntimeError as e: await mutex.acquire() SuperResolution.processing = False mutex.release() return MessageItem(MessageChain.create([Plain(text=str(e))]), QuoteSource())
async def swallowed(image: Union[int, str]) -> MessageItem: avatar = await AvatarFunPic.get_pil_avatar(image) frame_locs = [(180, 60, 100, 100), (184, 75, 100, 100), (183, 98, 100, 100), (179, 118, 110, 100), (156, 194, 150, 48), (178, 136, 122, 69), (175, 66, 122, 85), (170, 42, 130, 96), (175, 34, 118, 95), (179, 35, 110, 93), (180, 54, 102, 93), (183, 58, 97, 92), (174, 35, 120, 94), (179, 35, 109, 93), (181, 54, 101, 92), (182, 59, 98, 92), (183, 71, 90, 96), (180, 131, 92, 101)] raw_frames = [f"{os.getcwd()}/statics/SwallowedFrames/frame{i}.png" for i in range(23)] raw_frames = [IMG.open(i).convert('RGBA') for i in raw_frames] avatar_frames = [] for i in range(len(frame_locs)): frame = IMG.new('RGBA', (480, 400), (255, 255, 255, 0)) x, y, l, w = frame_locs[i] avatar_resized = avatar.resize((l, w), IMG.ANTIALIAS) frame.paste(avatar_resized, (x, y)) img = raw_frames[i] frame.paste(img, mask=img) avatar_frames.append(frame) frames = [] for i in range(2): frames.extend(avatar_frames[0:12]) frames.extend(avatar_frames[0:8]) frames.extend(avatar_frames[12:18]) frames.extend(raw_frames[18:23]) output = BytesIO() imageio.mimsave(output, frames, format='gif', duration=0.06) return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member, content: RegexResult, image: ElementResult): msg = content.result.asDisplay() img = await image.result.get_bytes() msg = await get_translate(msg) w2b = BuildImage(0, 0, background=BytesIO(img)) w2b.convert("L") msg_sp = msg.split("<|>") w, h = w2b.size add_h, font_size = init_h_font_size(h) bg = BuildImage(w, h + add_h, color="black", font_size=font_size) bg.paste(w2b) chinese_msg = formalization_msg(msg) if not bg.check_font_size(chinese_msg): if len(msg_sp) == 1: centered_text(bg, chinese_msg, add_h) else: centered_text(bg, chinese_msg + "<|>" + msg_sp[1], add_h) elif not bg.check_font_size(msg_sp[0]): centered_text(bg, msg, add_h) else: ratio = (bg.getsize(msg_sp[0])[0] + 20) / bg.w add_h = add_h * ratio bg.resize(ratio) centered_text(bg, msg, add_h) return MessageItem( MessageChain.create([Image(data_bytes=bg.pic2bytes())]), QuoteSource())
async def super_resolution(image_data: bytes, resize: bool = False) -> MessageItem: start = time.time() image = IMG.open(BytesIO(image_data)) image_size = image.size[0] * image.size[1] if image_size > max_size: if not resize: await mutex.acquire() SuperResolution.processing = False mutex.release() return MessageItem( MessageChain.create([ Plain( text= "图片尺寸过大!请发送1080p以内即像素数小于 1920×1080=2073600的照片!\n"), Plain( text= f"此图片尺寸为:{image.size[0]}×{image.size[1]}={image_size}!" ) ]), QuoteSource()) length = 1 for b in str(max_size / image_size).split('.')[1]: if b == '0': length += 1 else: break magnification = round(max_size / image_size, length + 1) image = image.resize((round(image.size[0] * magnification), round(image.size[1] * magnification))) image_array: np.ndarray = image.__array__() output, _ = await loop.run_in_executor(None, upsampler.enhance, image_array, 2) result = BytesIO() img = IMG.fromarray(output) img.save(result, format='PNG') # format: PNG / JPEG end = time.time() use_time = round(end - start, 2) await mutex.acquire() SuperResolution.processing = False mutex.release() return MessageItem( MessageChain.create([ Plain(text=f"超分完成!处理用时:{use_time}s\n"), Plain(text=f"由于像素过大,图片已进行缩放,结果可能不如原图片清晰\n" if resize else ""), Image(data_bytes=result.getvalue()) ]), QuoteSource())
async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member): if message.asDisplay().startswith("/fake "): content = "".join(i.text for i in message.get(Plain))[6:] if not message.has(At): return MessageItem(MessageChain.create([Plain(text="未指定目标!")]), Normal()) sender = message.get(At)[0] forward_nodes = [ ForwardNode( senderId=sender.target, time=datetime.now(), senderName=(await app.getMember(group, sender.target)).name, messageChain=MessageChain.create(Plain(text=content)), ) ] return MessageItem( MessageChain.create(Forward(nodeList=forward_nodes)), Normal())
async def wrapper(*args, **kwargs): try: if asyncio.iscoroutinefunction(func): result = await func(*args, **kwargs) else: result = func(*args, **kwargs) return result except: return MessageItem( MessageChain.create([Plain(text=traceback.format_exc())]), QuoteSource())
async def support(image: Union[int, str]) -> MessageItem: avatar = await AvatarFunPic.get_pil_avatar(image) support = IMG.open(f'{os.getcwd()}/statics/support.png') frame = IMG.new('RGBA', (1293, 1164), (255, 255, 255, 0)) avatar = avatar.resize((815, 815), IMG.ANTIALIAS).rotate(23, expand=True) frame.paste(avatar, (-172, -17)) frame.paste(support, mask=support) frame = frame.convert('RGB') output = BytesIO() frame.save(output, format='jpeg') return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member): if message.asDisplay() == "搜番": await update_user_call_count_plus(group, member, UserCalledCount.search, "search") if not await group_setting.get_setting(group.id, Setting.bangumi_search): return MessageItem(MessageChain.create([Plain(text="搜番功能未开启呐~请联系管理员哦~")]), Normal()) try: await app.sendGroupMessage(group, MessageChain.create([ At(member.id), Plain("请在30秒内发送要搜索的图片呐~") ])) except AccountMuted: logger.error(f"Bot 在群 <{group.name}> 被禁言,无法发送!") return None image_get = None message_received = None @Waiter.create_using_function([GroupMessage]) def waiter( event: GroupMessage, waiter_group: Group, waiter_member: Member, waiter_message: MessageChain ): nonlocal image_get nonlocal message_received if time.time() - start_time < 30: if all([ waiter_group.id == group.id, waiter_member.id == member.id, len(waiter_message[Image]) == len(waiter_message.__root__) - 1 ]): image_get = True message_received = waiter_message return event else: logger.warning("等待用户超时!BangumiSearchHandler进程推出!") return event inc = InterruptControl(bcc) start_time = time.time() await inc.wait(waiter) if image_get: logger.success("收到用户图片,启动搜索进程!") try: await app.sendGroupMessage( group, await BangumiSearcher.search_bangumi(message_received[Image][0]), quote=message_received[Source][0] ) except AccountMuted: logger.error(f"Bot 在群 <{group.name}> 被禁言,无法发送!") pass return None else: return None
async def ripped(image: Union[int, str]) -> MessageItem: ripped = IMG.open(f"{os.getcwd()}/statics/ripped.png") frame = IMG.new('RGBA', (1080, 804), (255, 255, 255, 0)) avatar = await AvatarFunPic.get_pil_avatar(image) left = avatar.resize((385, 385)).rotate(24, expand=True) right = avatar.resize((385, 385)).rotate(-11, expand=True) frame.paste(left, (-5, 355)) frame.paste(right, (649, 310)) frame.paste(ripped, mask=ripped) frame = frame.convert('RGB') output = BytesIO() frame.save(output, format='jpeg') return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def wrapper(*args, **kwargs): member_id = -1 group_id = -1 for i in args: if isinstance(i, Member): member_id = i.id if isinstance(i, Group): group_id = i.id if member_id == -1 or group_id == -1 or not await group_setting.get_setting( group_id, Setting.frequency_limit): if asyncio.iscoroutinefunction(func): return await func(*args, **kwargs) return func(*args, **kwargs) frequency_limit_instance = GlobalFrequencyLimitDict() await frequency_limit_instance.add_record(group_id, member_id, weight) if frequency_limit_instance.blacklist_judge(group_id, member_id): if not frequency_limit_instance.announce_judge( group_id, member_id): await frequency_limit_instance.blacklist_announced( group_id, member_id) return MessageItem( MessageChain.create([Plain(text="检测到大量请求,加入黑名单一小时!")]), QuoteSource()) else: return MessageItem(MessageChain.create([Plain("")]), DoNothing()) if frequency_limit_instance.get(group_id, member_id, func.__name__) + weight >= 10: return MessageItem( MessageChain.create([Plain(text="超过频率调用限制!")]), QuoteSource()) else: await frequency_limit_instance.update(group_id, weight) if asyncio.iscoroutinefunction(func): return await func(*args, **kwargs) return func(*args, **kwargs)
async def github_schedule(self, **kwargs): if not self.initialize: self.update_cache() self.initialize = True try: app = None manual = False repo = None per_page = 30 page = 1 for name, arg in kwargs.items(): if name == "manual" and isinstance(arg, bool): manual = arg if name == "repo" and isinstance(arg, tuple): repo = arg if name == "per_page" and isinstance(arg, int): per_page = arg if name == "page" and isinstance(arg, int): page = arg if isinstance(arg, Ariadne): app = arg if not app: logger.error("无法获得 Ariadne 实例") return None if self.__is_running: if manual: return MessageItem( MessageChain.create( [Plain(text="Github 订阅插件正在进行其他操作,请稍后再试。")]), QuoteSource()) return None if self.__status and repo: res = [] if events := await self.get_repo_event(repo, per_page, page): if isinstance(events, list): self.__cached[repo]['last_id'] = int(events[0]['id']) if resp := await self.a_generate_plain(events[0]): res.append(resp) else: res.append(Plain(text=events["message"])) if not res: return None res.insert(0, Plain(text=f"仓库:{repo[0]}/{repo[1]}\n")) res.append( Plain( text= f"----------\n获取时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" )) return MessageChain.create(res)
async def throw(image: Union[int, str]) -> MessageItem: avatar = await AvatarFunPic.get_pil_avatar(image) mask = IMG.new('L', avatar.size, 0) draw = ImageDraw.Draw(mask) offset = 1 draw.ellipse((offset, offset, avatar.size[0] - offset, avatar.size[1] - offset), fill=255) mask = mask.filter(ImageFilter.GaussianBlur(0)) avatar.putalpha(mask) avatar = avatar.rotate(random.randint(1, 360), IMG.BICUBIC) avatar = avatar.resize((143, 143), IMG.ANTIALIAS) throw = IMG.open(f"{os.getcwd()}/statics/throw.png") throw.paste(avatar, (15, 178), mask=avatar) throw = throw.convert('RGB') output = BytesIO() throw.save(output, format='jpeg') return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def wolfram_alpha(app: Ariadne, message: MessageChain, group: Group, content: RegexResult): question = content.result.asDisplay() if not api_key or api_key == "wolfram_alpha_key": return MessageItem( MessageChain.create([Plain(text="尚未配置wolfram_alpha_key!")]), QuoteSource()) url = f"https://api.wolframalpha.com/v1/simple?i={question.replace('+', '%2B')}&appid={api_key}" async with get_running(Adapter).session.get(url=url) as resp: if resp.status == 200: res = await resp.read() await app.sendGroupMessage(group, MessageChain([Image(data_bytes=res)]), quote=message.getFirst(Source)) else: await app.sendGroupMessage(group, MessageChain(await resp.text()), quote=message.getFirst(Source))
async def crawl(image: Union[int, str]) -> MessageItem: avatar = await AvatarFunPic.get_pil_avatar(image) mask = IMG.new('L', avatar.size, 0) draw = ImageDraw.Draw(mask) offset = 1 draw.ellipse((offset, offset, avatar.size[0] - offset, avatar.size[1] - offset), fill=255) mask = mask.filter(ImageFilter.GaussianBlur(0)) avatar.putalpha(mask) images = [i for i in os.listdir(f"{os.getcwd()}/statics/crawl")] crawl = IMG.open(f"{os.getcwd()}/statics/crawl/{random.choice(images)}").resize( (500, 500), IMG.ANTIALIAS) avatar = avatar.resize((100, 100), IMG.ANTIALIAS) crawl.paste(avatar, (0, 400), mask=avatar) crawl = crawl.convert('RGB') output = BytesIO() crawl.save(output, format='jpeg') return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def rub(operator_image: Union[int, str], target_image: Union[int, str]) -> MessageItem: user_locs = [(39, 91, 75, 75, 0), (49, 101, 75, 75, 0), (67, 98, 75, 75, 0), (55, 86, 75, 75, 0), (61, 109, 75, 75, 0), (65, 101, 75, 75, 0)] self_locs = [(102, 95, 70, 80, 0), (108, 60, 50, 100, 0), (97, 18, 65, 95, 0), (65, 5, 75, 75, -20), (95, 57, 100, 55, -70), (109, 107, 65, 75, 0)] frames = [] self_img = await AvatarFunPic.get_pil_avatar(operator_image) user_img = await AvatarFunPic.get_pil_avatar(target_image) for i in range(6): frame = IMG.open(f'{os.getcwd()}/statics/RubFrames/frame{i}.png').convert('RGBA') x, y, w, h, angle = user_locs[i] user_img_new = (await AvatarFunPic.resize_img(user_img, w, h, angle)).convert("RGBA") frame.paste(user_img_new, (x, y), mask=user_img_new) x, y, w, h, angle = self_locs[i] self_img_new = (await AvatarFunPic.resize_img(self_img, w, h, angle)).convert("RGBA") frame.paste(self_img_new, (x, y), mask=self_img_new) frames.append(frame) output = BytesIO() imageio.mimsave(output, frames, format='gif', duration=0.05) return MessageItem(MessageChain.create([Image(data_bytes=output.getvalue())]), Normal())
async def kiss(operator_image: Union[int, str], target_image: Union[int, str]) -> MessageItem: """ Author: https://github.com/SuperWaterGod """ gif_frames = [] operator = await AvatarFunPic.get_pil_avatar(operator_image) target = await AvatarFunPic.get_pil_avatar(target_image) operator = operator.resize((40, 40), IMG.ANTIALIAS) size = operator.size r2 = min(size[0], size[1]) circle = IMG.new('L', (r2, r2), 0) draw = ImageDraw.Draw(circle) draw.ellipse((0, 0, r2, r2), fill=255) alpha = IMG.new('L', (r2, r2), 255) alpha.paste(circle, (0, 0)) operator.putalpha(alpha) target = target.resize((50, 50), IMG.ANTIALIAS) size = target.size r2 = min(size[0], size[1]) circle = IMG.new('L', (r2, r2), 0) draw = ImageDraw.Draw(circle) draw.ellipse((0, 0, r2, r2), fill=255) alpha = IMG.new('L', (r2, r2), 255) alpha.paste(circle, (0, 0)) target.putalpha(alpha) md5 = hashlib.md5(str(str(operator_image) + str(target_image)).encode("utf-8")).hexdigest() for i in range(1, 14): gif_frames.append(await AvatarFunPic.kiss_make_frame(operator, target, i)) await AvatarFunPic.save_gif(gif_frames, f"{os.getcwd()}/statics/temp/tempKiss-{md5}.gif", fps=25) with open(f"{os.getcwd()}/statics/temp/tempKiss-{md5}.gif", 'rb') as r: img_content = r.read() os.remove(f"{os.getcwd()}/statics/temp/tempKiss-{md5}.gif") return MessageItem(MessageChain.create([Image(data_bytes=img_content)]), Normal())
async def petpet(image: Union[int, str], flip=False, squish=0, fps=20) -> MessageItem: """生成PetPet 将输入的头像生成为所需的 PetPet 并输出 参数 path: str 为头像路径 flip: bool 为是否横向反转头像 squish: float 为一个 [0, 1] 之间的数,为挤压量 fps: int 为输出 gif 每秒显示的帧数 返回 bool 但是会输出一个符合参数的 gif """ gif_frames = [] avatar = await AvatarFunPic.get_pil_avatar(image) # 生成每一帧 for i in range(5): gif_frames.append(await AvatarFunPic.make_frame(avatar, i, squish=squish, flip=flip)) if not os.path.exists(f"{os.getcwd()}/statics/temp/"): os.mkdir(f"{os.getcwd()}/statics/temp/") md5 = hashlib.md5(str(image).encode("utf-8")).hexdigest() await AvatarFunPic.save_gif(gif_frames, f"{os.getcwd()}/statics/temp/tempPetPet-{md5}.gif", fps=fps) with open(f"{os.getcwd()}/statics/temp/tempPetPet-{md5}.gif", "rb") as r: image_bytes = r.read() os.remove(f"{os.getcwd()}/statics/temp/tempPetPet-{md5}.gif") return MessageItem(MessageChain.create([Image(data_bytes=image_bytes)]), Normal())
async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member): if re.match(pattern, message.asDisplay()): if not await user_permission_require(group, member, 2): return MessageItem( MessageChain.create([Plain(text="你没有权限,爬!")]), Normal()) image_type = re.findall(r"添加(.*?)图片.*(\[图片].*)+", message.asDisplay(), re.S)[0][0] if image_type not in legal_type: return MessageItem( MessageChain.create([ Plain( text=f"非法图片类型!\n合法image_type:{'、'.join(legal_type)}" ) ]), QuoteSource()) if path := image_paths.get(image_type): if os.path.exists(path): try: await ImageAdder.add_image(path, message.get(Image)) except: logger.error(traceback.format_exc()) return MessageItem( MessageChain.create( [Plain(text="出错了呐~请查看日志/控制台输出!")]), Normal()) return MessageItem( MessageChain.create([ Plain( text=f"保存成功!共保存了{len(message.get(Image))}张图片!") ]), Normal()) else: return MessageItem( MessageChain.create([ Image( path= f"{os.getcwd()}/statics/error/path_not_exists.png" ) ]), QuoteSource()) else: return MessageItem( MessageChain.create([Plain(text=f"无{image_type}项!请检查配置!") ]), QuoteSource())
async def real_handle(self, app: Ariadne, message: MessageChain, group: Group = None, member: Member = None, friend: Friend = None) -> MessageItem: commands = { "enable": { "permission": [3, []], "permission_nl": "3 级权限", "manual": "/github-watch enable", "description": "启用 Github 订阅功能", "func": self.enable }, "disable": { "permission": [3, []], "permission_nl": "3 级权限", "manual": "/github-watch disable", "description": "禁用 Github 订阅功能", "func": self.disable }, "add": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch add {repo} [repo]+", "description": "订阅仓库变动,可同时订阅多个仓库", "func": self.add }, "remove": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch remove {repo} [repo]+", "description": "取消订阅仓库变动,可同时取消订阅多个仓库", "func": self.remove }, "check": { "permission": [ 1, (MemberPerm.Member, MemberPerm.Administrator, MemberPerm.Owner) ], "permission_nl": "任何人", "manual": "/github-watch check", "description": "手动查看仓库订阅列表", "func": self.check }, "cache": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch cache {update/store}", "description": "更新/储存缓存", "func": self.cache } } if message.asDisplay().startswith("/github-watch"): if not self.initialize: self.update_cache() for repo in self.__cached.keys(): self.__cached[repo]['enabled'] = True self.store_cache() self.initialize = True args = message.asDisplay().split(" ", maxsplit=1) if len(args) == 1: msg = [Plain(text="缺少参数\n\n")] for func in commands.keys(): msg.append( Plain(text=( f"/github-watch {func}\n" f" 描述:{commands[func]['description']}\n" f" 用法:{commands[func]['manual']}\n" f" 权限:{commands[func]['permission_nl']}\n"))) return MessageItem( await MessageChainUtils.messagechain_to_img( MessageChain.create(msg)), QuoteSource()) _, args = args name = args.split(" ", maxsplit=1)[0] arg = ''.join(args.split(" ", maxsplit=1)[1:]) if name not in commands.keys(): return MessageItem( MessageChain.create([Plain(text=f"未知指令:{arg}")]), QuoteSource()) if member and group: permission = commands[name]['permission'] if not await user_permission_require(group, member, permission[0]) \ and not (member.permission in permission[1]): return MessageItem( MessageChain.create([ Plain( text=f"权限不足,你需要 {permission[0]} 级权限" f"{('或来自 ' + str(permission[1][0]) + ' 的权限') if permission[1] else ''}" ) ]), QuoteSource()) arg = arg.strip() return MessageItem( await commands[name]['func'](app=app, group=group, friend=friend, arg=arg), QuoteSource())
class GithubWatcher(object): __name__ = "GithubWatcher" __description__ = "Github 订阅 Handler" __usage__ = "None" __cached = {} if config.functions['github'][ 'username'] != "username" and config.functions['github'][ 'token'] != 'token': __auth = True __session = aiohttp.ClientSession( auth=BasicAuth(login=config.functions['github']['username'], password=config.functions['github']['token'])) else: __auth = False __session = aiohttp.ClientSession() __first_warned = False __status = True __base_url = "https://api.github.com" __events_url = "/repos/{owner}/{repo}/events" __is_running = False initialize = False @switch() @blacklist() async def real_handle(self, app: Ariadne, message: MessageChain, group: Group = None, member: Member = None, friend: Friend = None) -> MessageItem: commands = { "enable": { "permission": [3, []], "permission_nl": "3 级权限", "manual": "/github-watch enable", "description": "启用 Github 订阅功能", "func": self.enable }, "disable": { "permission": [3, []], "permission_nl": "3 级权限", "manual": "/github-watch disable", "description": "禁用 Github 订阅功能", "func": self.disable }, "add": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch add {repo} [repo]+", "description": "订阅仓库变动,可同时订阅多个仓库", "func": self.add }, "remove": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch remove {repo} [repo]+", "description": "取消订阅仓库变动,可同时取消订阅多个仓库", "func": self.remove }, "check": { "permission": [ 1, (MemberPerm.Member, MemberPerm.Administrator, MemberPerm.Owner) ], "permission_nl": "任何人", "manual": "/github-watch check", "description": "手动查看仓库订阅列表", "func": self.check }, "cache": { "permission": [2, (MemberPerm.Administrator, MemberPerm.Owner)], "permission_nl": "2 级或群管理员及以上权限", "manual": "/github-watch cache {update/store}", "description": "更新/储存缓存", "func": self.cache } } if message.asDisplay().startswith("/github-watch"): if not self.initialize: self.update_cache() for repo in self.__cached.keys(): self.__cached[repo]['enabled'] = True self.store_cache() self.initialize = True args = message.asDisplay().split(" ", maxsplit=1) if len(args) == 1: msg = [Plain(text="缺少参数\n\n")] for func in commands.keys(): msg.append( Plain(text=( f"/github-watch {func}\n" f" 描述:{commands[func]['description']}\n" f" 用法:{commands[func]['manual']}\n" f" 权限:{commands[func]['permission_nl']}\n"))) return MessageItem( await MessageChainUtils.messagechain_to_img( MessageChain.create(msg)), QuoteSource()) _, args = args name = args.split(" ", maxsplit=1)[0] arg = ''.join(args.split(" ", maxsplit=1)[1:]) if name not in commands.keys(): return MessageItem( MessageChain.create([Plain(text=f"未知指令:{arg}")]), QuoteSource()) if member and group: permission = commands[name]['permission'] if not await user_permission_require(group, member, permission[0]) \ and not (member.permission in permission[1]): return MessageItem( MessageChain.create([ Plain( text=f"权限不足,你需要 {permission[0]} 级权限" f"{('或来自 ' + str(permission[1][0]) + ' 的权限') if permission[1] else ''}" ) ]), QuoteSource()) arg = arg.strip() return MessageItem( await commands[name]['func'](app=app, group=group, friend=friend, arg=arg), QuoteSource()) async def enable(self, **kwargs): self.__status = True return MessageChain.create([Plain(text="已开启 Github 仓库订阅")]) async def disable(self, **kwargs): self.__status = False return MessageChain.create([Plain(text="已关闭 Github 仓库订阅")]) async def add(self, **kwargs): if not self.__status: return MessageChain.create([Plain(text="Github 仓库订阅功能已关闭")]) repos = None group = None friend = None app = None for name, arg in kwargs.items(): if name == "arg" and isinstance(arg, str): repos = arg if isinstance(arg, Group): group = arg if isinstance(arg, Friend): friend = arg if isinstance(arg, Ariadne): app = arg err = [] if not group and not friend: err = err.extend([Plain(text="无法获取 Group 或 Friend 实例")]) if not app: err = err.extend([Plain(text="无法获取 Ariadne 实例")]) if not repos: err = err.extend([Plain(text="未填写需要订阅的仓库")]) if err: return MessageChain.create(err) repos = repos.split(" ") failed = [] duplicated = [] success_count = 0 for repo in repos: url = f"https://api.github.com/search/repositories?q={repo}" async with self.__session.get(url=url, proxy=proxy) as resp: try: resp.raise_for_status() except aiohttp.ClientError as e: logger.error(e) logger.error(f"暂时无法取得仓库 {repo} 的更新(状态码 {resp.status})") continue result = (await resp.json())["items"] if not result: failed.append(repo) continue repo = result[0]['full_name'] repo = repo.split("/") repo = (repo[0], repo[1]) if repo not in self.__cached.keys(): self.__cached[repo] = { "group": [], "friend": [], "last_id": -1, "enabled": True } if group: if group.id in self.__cached[repo]['group']: duplicated.append(f"{repo[0]}/{repo[1]}") else: self.__cached[repo][ 'group'] = self.__cached[repo]['group'] + [group.id] if friend: if friend.id in self.__cached[repo]['friend']: duplicated.append(f"{repo[0]}/{repo[1]}") else: self.__cached[repo]['friend'] = self.__cached[repo][ 'friend'] + [friend.id] if self.__cached[repo]['last_id'] == -1: await self.github_schedule(app=app, manuel=True, per_page=1, page=1, repo=repo) success_count += 1 res = [Plain(text=f"{success_count} 个仓库订阅成功")] if failed: res.append( Plain(text=f"\n{len(failed)} 个仓库订阅失败" f"\n失败的仓库有:{' '.join(failed)}")) if duplicated: res.append( Plain(text=f"\n{len(duplicated)} 个仓库已在订阅列表中" f"\n重复的仓库有:{' '.join(duplicated)}")) try: self.store_cache(manual=False) self.update_cache(manual=False) return MessageChain.create(res) except Exception as e: logger.error(e) res.append(Plain(text="\n\n刷新缓存失败")) return MessageChain.create(res) async def remove(self, **kwargs): if not self.__status: return MessageChain.create([Plain(text=f"Github 仓库订阅功能已关闭")]) repos = None group = None friend = None err = [] for name, arg in kwargs.items(): if name == "arg" and isinstance(arg, str): repos = arg if isinstance(arg, Group): group = arg if isinstance(arg, Friend): friend = arg if not group and not friend: err = err.extend([Plain(text=f"无法获取 Group 或 Friend 实例")]) if not repos: err = err.extend([Plain(text="未填写需要取消订阅的仓库")]) if err: return MessageChain.create(err) repos = repos.split(" ") failed = [] success_count = 0 for repo in repos: repo = repo.split("/") if len(repo) != 2: failed.append("/".join(repo)) continue repo = (repo[0], repo[1]) if repo not in self.__cached.keys(): failed.append("/".join(repo)) continue if group: self.__cached[repo]['group'] = [ group_id for group_id in self.__cached[repo]['group'] if group_id != group.id ] if friend: self.__cached[repo]['friend'] = [ friend_id for friend_id in self.__cached[repo]['group'] if friend_id != friend.id ] if not (self.__cached[repo]['group'] and self.__cached[repo]['friend']): self.__cached.pop(repo) success_count += 1 res = [Plain(text=f"{success_count} 个仓库取消订阅成功")] if failed: res.append( Plain(text=f"\n{len(failed)} 个仓库取消订阅失败" f"\n失败的仓库有:{' '.join(failed)}")) try: self.store_cache(manual=False) self.update_cache(manual=False) return MessageChain.create(res) except Exception as e: logger.error(e) res.append(Plain(text="\n\n刷新缓存失败")) return MessageChain.create(res) async def cache(self, **kwargs): accepted = ['update', 'store'] command = None for name, arg in kwargs.items(): if name == "arg" and isinstance(arg, str): command = arg if not command: return MessageChain.create([Plain(text=f"未填写参数")]) if command not in accepted: return MessageChain.create([Plain(text=f"未知参数:{command}")]) if command == 'update': return self.update_cache(manual=True) if command == 'store': return self.store_cache(manual=True) def update_cache(self, manual: bool = False): try: with open(str(Path(__file__).parent.joinpath("watcher_data.json")), "r") as r: data = json.loads(r.read()) cache = {} for key in data.keys(): owner, repo = key.split("/") cache[(owner, repo)] = data[key] self.__cached = cache return MessageChain.create([Plain( text="更新缓存成功")]) if manual else None except (FileNotFoundError, JSONDecodeError): return MessageChain.create( [Plain(text="无法更新缓存,请检查是否删除了缓存文件并重新储存缓存")]) def store_cache(self, manual: bool = False): with open(str(Path(__file__).parent.joinpath("watcher_data.json")), "w") as w: cache = {} for key in self.__cached.keys(): new_key = f"{key[0]}/{key[1]}" cache[new_key] = self.__cached[key] w.write(json.dumps(cache, indent=4)) return MessageChain.create([Plain(text="写入缓存成功")]) if manual else None async def check(self, **kwargs) -> MessageChain: group = None friend = None for name, arg in kwargs.items(): if isinstance(arg, Group): group = arg if isinstance(arg, Friend): friend = arg if not group and not friend: return MessageChain.create([Plain(text=f"无法获取 Group 或 Friend 实例")]) watched = [] target = group if group else friend field = 'group' if group else 'friend' for repo in self.__cached.keys(): if target.id in self.__cached[repo][field]: watched.append(f"{repo[0]}/{repo[1]}") res = [ Plain(text=f"{'本群' if group else '你'}订阅的仓库有:\n" f"{' '.join(watched)}") ] return MessageChain.create(res) async def get_repo_event(self, repo: tuple, per_page: int = 30, page: int = 1): url = self.__base_url \ + self.__events_url.replace('{owner}', repo[0]).replace('{repo}', repo[1]) \ + f'?per_page={per_page}&page={page}' res = None try: res = await self.__session.get(url=url, proxy=proxy) res.raise_for_status() res = await res.json() if isinstance(res, list): return res elif isinstance(res, dict): if "message" in res.keys(): if "API rate limit exceeded" in res["message"]: logger.error("GitHub API 超出速率限制") if not self.__auth: logger.error("请设置 GitHub 用户名和 OAuth Token 以提高限制") self.__first_warned = True elif res["message"] == "Not Found": logger.error(f"无法找到仓库 {repo[0]}/{repo[1]}") self.__cached[repo]['enabled'] = False return res except aiohttp.ClientError as e: logger.error(e) logger.error( f"暂时无法取得仓库 {repo[0]}/{repo[1]} 的更新" f"{'' if not res else '(状态码 ' + str(res.status) + ')'}") return None except Exception as e: logger.error(e) return None async def a_generate_plain(self, event: dict): return await asyncio.get_event_loop().run_in_executor( None, self.generate_plain, event) @staticmethod def generate_plain(event: dict): actor = event['actor']['display_login'] event_time = (datetime.strptime(event['created_at'], '%Y-%m-%dT%H:%M:%SZ') + timedelta(hours=8)) \ .strftime('%Y-%m-%d %H:%M:%S') resp = None if event['type'] == 'IssuesEvent': if event['payload']['action'] == 'opened': title = event['payload']['issue']['title'] number = event['payload']['issue']['number'] body = event['payload']['issue']['body'] if body: if len(body) > 100: body = body[:100] + "......" body = body + "\n" link = event['payload']['issue']['html_url'] resp = Plain(text=f"----------\n" f"[新 Issue]\n" f"#{number} {title}\n" f"{body}\n" f"\n" f"发布人:{actor}\n" f"时间:{event_time}\n" f"链接:{link}\n") elif event['type'] == 'IssueCommentEvent': if event['payload']['action'] == 'created': title = event['payload']['issue']['title'] number = event['payload']['issue']['number'] body = event['payload']['comment']['body'] if body: if len(body) > 100: body = body[:100] + "......" body = body + "\n" link = event['payload']['comment']['html_url'] resp = Plain(text=f"----------\n" f"[新 Comment]\n" f"#{number} {title}\n" f"{body}" f"\n" f"发布人:{actor}\n" f"时间:{event_time}\n" f"链接:{link}\n") elif event['type'] == 'PullRequestEvent': if event['payload']['action'] == 'opened': title = event['payload']['pull_request']['title'] number = event['payload']['pull_request']['number'] body = event['payload']['pull_request']['body'] if body: if len(body) > 100: body = body[:100] + "......" body = body + "\n" head = event['payload']['pull_request']['head']['label'] base = event['payload']['pull_request']['base']['label'] commits = event['payload']['pull_request']['commits'] link = event['payload']['pull_request']['html_url'] resp = Plain(text=f"----------\n" f"[新 PR]\n" f"#{number} {title}\n" f"{body}" f"\n" f"{head} → {base}\n" f"提交数:{commits}\n" f"发布人:{actor}\n" f"时间:{event_time}\n" f"链接:{link}\n") elif event['type'] == 'PushEvent': commits = [] for commit in event['payload']['commits']: commits.append( f"· [{commit['author']['name']}] {commit['message']}") resp = Plain(text=f"----------\n" f"[新 Push]\n" + "\n".join(commits) + f"\n" f"提交数:{len(commits)}\n" f"发布人:{actor}\n" f"时间:{event_time}\n") elif event['type'] == 'CommitCommentEvent': body = event['payload']['comment']['body'] if body: if len(body) > 100: body = body[:100] + "......" body = body + "\n" link = event['payload']['comment']['html_url'] resp = Plain(text=f"----------\n" f"[新 Comment]\n" f"{body}" f"\n" f"发布人:{actor}\n" f"时间:{event_time}\n" f"链接:{link}\n") return resp if resp else None async def github_schedule(self, **kwargs): if not self.initialize: self.update_cache() self.initialize = True try: app = None manual = False repo = None per_page = 30 page = 1 for name, arg in kwargs.items(): if name == "manual" and isinstance(arg, bool): manual = arg if name == "repo" and isinstance(arg, tuple): repo = arg if name == "per_page" and isinstance(arg, int): per_page = arg if name == "page" and isinstance(arg, int): page = arg if isinstance(arg, Ariadne): app = arg if not app: logger.error("无法获得 Ariadne 实例") return None if self.__is_running: if manual: return MessageItem( MessageChain.create( [Plain(text="Github 订阅插件正在进行其他操作,请稍后再试。")]), QuoteSource()) return None if self.__status and repo: res = [] if events := await self.get_repo_event(repo, per_page, page): if isinstance(events, list): self.__cached[repo]['last_id'] = int(events[0]['id']) if resp := await self.a_generate_plain(events[0]): res.append(resp) else: res.append(Plain(text=events["message"])) if not res: return None res.insert(0, Plain(text=f"仓库:{repo[0]}/{repo[1]}\n")) res.append( Plain( text= f"----------\n获取时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" )) return MessageChain.create(res) if self.__status: self.__is_running = True for repo in self.__cached.keys(): if not self.__cached[repo]['enabled']: continue res = [] if events := await self.get_repo_event( repo, per_page, page): if isinstance(events, list): last_id = self.__cached[repo]['last_id'] new_last_id = last_id for index, event in enumerate(events): if index == 0: new_last_id = int(event['id']) if int(event['id']) <= last_id: break if resp := await self.a_generate_plain(event): res.append(resp) else: continue self.__cached[repo]['last_id'] = new_last_id self.store_cache() else: res.append(Plain(text=events["message"])) if res: if res[0].asDisplay() == "Bad credentials": self.__is_running = False self.__status = False await app.sendFriendMessage( config.host_qq, MessageChain.create([ Plain( text="凭据无效,请检查是否已更改或吊销 Github Token\n" "已自动关闭 Github Watcher") ])) raise Exception("凭据无效,请检查是否已更改或吊销 Github Token") res.insert(0, Plain(text=f"仓库:{repo[0]}/{repo[1]}\n")) res.append( Plain( text= f"----------\n获取时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" )) res = MessageChain.create(res) # fwd_nodes = [ # ForwardNode( # senderId=config.bot_qq, # time=datetime.now(), # senderName="Github 订阅", # messageChain=MessageChain.create(Plain(text=f"仓库:{repo[0]}/{repo[1]}\n")), # ) # ] # for index, element in enumerate(res): # fwd_nodes.append( # ForwardNode( # senderId=config.bot_qq, # time=datetime.now() + timedelta(minutes=index + 1), # senderName="Github 订阅", # messageChain=MessageChain.create(element), # ) # ) # fwd_nodes.append( # ForwardNode( # senderId=config.bot_qq, # time=datetime.now(), # senderName="Github 订阅", # messageChain=MessageChain.create(Plain( # text=f"获取时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") # ), # ) # ) # res = MessageChain.create(Forward(nodeList=fwd_nodes)) if manual: self.__is_running = False return MessageItem(res, Normal()) for group in self.__cached[repo]['group']: try: await app.sendGroupMessage(group, res) except (AccountMuted, UnknownTarget): pass for friend in self.__cached[repo]['friend']: try: await app.sendFriendMessage(friend, res) except UnknownTarget: pass
return MessageItem(res, Normal()) for group in self.__cached[repo]['group']: try: await app.sendGroupMessage(group, res) except (AccountMuted, UnknownTarget): pass for friend in self.__cached[repo]['friend']: try: await app.sendFriendMessage(friend, res) except UnknownTarget: pass self.__is_running = False else: if manual: return MessageItem( MessageChain.create([Plain(text="Github 订阅插件已关闭。")]), QuoteSource()) except Exception as e: logger.error(e) self.__is_running = False gw = GithubWatcher() @channel.use(SchedulerSchema(timer=timers.every_minute())) async def github_schedule(app: Ariadne): try: await gw.github_schedule(app=app, manual=False) except: pass
async def handle(app: Ariadne, message: MessageChain, group: Group, member: Member) -> MessageItem: # global saya_data if message.asDisplay().strip() == "已加载插件": loaded_channels = SayaManager.get_loaded_channels() keys = list(loaded_channels.keys()) keys.sort() return MessageItem( await MessageChainUtils.messagechain_to_img( MessageChain.create([Plain(text="目前加载插件:\n")] + [ Plain( text=f"{i + 1}. {loaded_channels[keys[i]]._name}\n" ) for i in range(len(keys)) ] + [Plain(text="发送 `插件详情 [编号|名称]` 可查看插件详情")])), QuoteSource()) elif re.match(r"插件详情 .+", message.asDisplay()): target = message.asDisplay()[5:].strip() loaded_channels = SayaManager.get_loaded_channels() keys = list(loaded_channels.keys()) if target.isdigit(): keys.sort() if not 0 <= int(target) - 1 < len(keys): return MessageItem( MessageChain.create([Plain(text="错误的编号!请检查后再发送!")]), QuoteSource()) channel = loaded_channels[keys[int(target) - 1]] channel_path = keys[int(target) - 1] else: for lchannel in loaded_channels.keys(): if loaded_channels[lchannel]._name == target: channel = loaded_channels[lchannel] channel_path = lchannel break else: return MessageItem( MessageChain.create([Plain(text="错误的名称!请检查后再发送!")]), QuoteSource()) return MessageItem( MessageChain.create([ Plain(text=f"插件名称:{channel._name}\n"), Plain(text=f"插件作者:{'、'.join(channel._author)}\n"), Plain(text=f"插件描述:{channel._description}\n"), Plain(text=f"插件包名:{channel_path}") ]), QuoteSource()) elif message.asDisplay() == "未加载插件": if not await user_permission_require(group, member, 3): return MessageItem(MessageChain.create([Plain(text="爬,权限不足")]), QuoteSource()) unloaded_channels = SayaManager.get_unloaded_channels() unloaded_channels.sort() return MessageItem( MessageChain.create([Plain(text="目前未加载插件:\n")] + [ Plain(text=f"{i + 1}. {unloaded_channels[i]}\n") for i in range(len(unloaded_channels)) ] + [Plain(text="发送 `[加载|卸载|重载]插件 [编号|名称]` 可加载/卸载/重载插件\n")]), QuoteSource()) elif re.match(r"加载插件 .+", message.asDisplay()): if not await user_permission_require(group, member, 3): return MessageItem(MessageChain.create([Plain(text="爬,权限不足")]), QuoteSource()) target = message.asDisplay()[5:].strip() unloaded_channels = SayaManager.get_unloaded_channels() if target.isdigit(): unloaded_channels.sort() if not 0 <= int(target) - 1 < len(unloaded_channels): return MessageItem( MessageChain.create([Plain(text="错误的编号!请检查后再发送!")]), QuoteSource()) channel = unloaded_channels[int(target) - 1] else: for ulchannel in unloaded_channels: if ulchannel == target: channel = ulchannel break else: return MessageItem( MessageChain.create([Plain(text="错误的名称!请检查后再发送!")]), QuoteSource()) await app.sendMessage( group, MessageChain.create( [Plain(text=f"你确定要加载插件 `{channel}` 吗?(是/否)")])) @Waiter.create_using_function([GroupMessage]) def confirm_waiter(waiter_group: Group, waiter_member: Member, waiter_message: MessageChain): if all([ waiter_group.id == group.id, waiter_member.id == member.id ]): if re.match(r"[是否]", waiter_message.asDisplay()): return waiter_message.asDisplay() else: return "" result = await inc.wait(confirm_waiter) if not result: return MessageItem( MessageChain.create([Plain(text="非预期回复,进程退出")]), QuoteSource()) elif result == "是": result = SayaManager.load_channel(channel) if result: return MessageItem( MessageChain.create( [Plain(text=f"发生错误:{result[channel]}")]), QuoteSource()) else: return MessageItem( MessageChain.create([Plain(text="加载成功")]), QuoteSource()) else: return MessageItem(MessageChain.create([Plain(text="进程退出")]), QuoteSource()) elif re.match(r"[卸重]载插件 .+", message.asDisplay()): if not await user_permission_require(group, member, 3): return MessageItem(MessageChain.create([Plain(text="爬,权限不足")]), QuoteSource()) load_type = "reload" if message.asDisplay()[0] == "重" else "unload" target = message.asDisplay()[5:].strip() loaded_channels = SayaManager.get_loaded_channels() keys = list(loaded_channels.keys()) keys.sort() if target.isdigit(): if not 0 <= int(target) - 1 < len(keys): return MessageItem( MessageChain.create([Plain(text="错误的编号!请检查后再发送!")]), QuoteSource()) channel = loaded_channels[keys[int(target) - 1]] channel_path = keys[int(target) - 1] else: for lchannel in loaded_channels.keys(): if loaded_channels[lchannel]._name == target: channel = loaded_channels[lchannel] channel_path = lchannel break else: return MessageItem( MessageChain.create([Plain(text="错误的名称!请检查后再发送!")]), QuoteSource()) await app.sendMessage( group, MessageChain.create([ Plain( text= f"你确定要{message.asDisplay()[0]}载插件 `{channel._name}` 吗?(是/否)" ) ])) @Waiter.create_using_function([GroupMessage]) def confirm_waiter(waiter_group: Group, waiter_member: Member, waiter_message: MessageChain): if all([ waiter_group.id == group.id, waiter_member.id == member.id ]): if re.match(r"[是否]", waiter_message.asDisplay()): return waiter_message.asDisplay() else: return "" result = await inc.wait(confirm_waiter) if not result: return MessageItem( MessageChain.create([Plain(text="非预期回复,进程退出")]), QuoteSource()) elif result == "是": result = SayaManager.unload_channel( channel_path ) if load_type == "unload" else SayaManager.reload_channel( channel_path) if result: return MessageItem( MessageChain.create( [Plain(text=f"发生错误:{result[channel_path]}")]), QuoteSource()) else: return MessageItem( MessageChain.create( [Plain(text=f"{message.asDisplay()[0]}载成功")]), QuoteSource()) else: return MessageItem(MessageChain.create([Plain(text="进程退出")]), QuoteSource()) elif re.match(r"(打开|关闭)插件 .+", message.asDisplay()): if not await user_permission_require(group, member, 3): return MessageItem(MessageChain.create([Plain(text="爬,权限不足")]), QuoteSource()) switch_type = "on" if message.asDisplay()[:2] == "打开" else "off" target = message.asDisplay()[5:].strip() loaded_channels = SayaManager.get_loaded_channels() keys = list(loaded_channels.keys()) keys.sort() channel_path = "" if target.isdigit(): if not 0 <= int(target) - 1 < len(keys): return MessageItem( MessageChain.create([Plain(text="错误的编号!请检查后再发送!")]), QuoteSource()) channel_path = keys[int(target) - 1] else: for lchannel in loaded_channels.keys(): if loaded_channels[lchannel]._name == target: channel_path = lchannel break saya_data.switch_on( channel_path, group) if switch_type == "on" else saya_data.switch_off( channel_path, group) return MessageItem( MessageChain.create([ Plain(text=f"插件{channel_path}已{message.asDisplay()[:2]}!") ]), QuoteSource())