├── .gitignore ├── LICENSE ├── README.md ├── configdemo.json ├── main.py ├── modules ├── 5000zhao │ ├── __init__.py │ ├── fonts │ │ ├── ArialEnUnicodeBold.ttf │ │ ├── STKAITI.TTF │ │ ├── notobk-subset.otf │ │ ├── notoserifbk-subset.otf │ │ └── simsunb.ttf │ └── utils.py ├── AbbreviatedPrediction.py ├── BangumiInfoSearcher │ └── __init__.py ├── BiliResolve │ ├── __init__.py │ ├── configdemo.json │ ├── requirements.txt │ └── utils.py ├── BilibiliBangumiSchedule.py ├── ChatBot │ ├── README.md │ ├── __init__.py │ ├── configdemo.json │ └── utils.py ├── GarbageClassification │ ├── __init__.py │ └── requirements.txt ├── GithubHotSearch.py ├── GroupWordCloudGenerator │ ├── README.md │ ├── STKAITI.TTF │ ├── Sqlite3Manager.py │ ├── __init__.py │ ├── back.jpg │ ├── requirements.txt │ └── simsunb.ttf ├── HeadSplicer │ ├── __init__.py │ ├── statics │ │ ├── haarcascade_frontalface_alt.xml │ │ ├── head │ │ │ ├── 1 │ │ │ │ ├── 1.png │ │ │ │ └── dat.json │ │ │ └── 2 │ │ │ │ ├── 2.png │ │ │ │ └── dat.json │ │ ├── lbpcascade_animeface.xml │ │ ├── 接头失败.png │ │ ├── 没找到头.png │ │ ├── 猫猫头_0.png │ │ ├── 猫猫头_1.png │ │ ├── 猫猫头_2.png │ │ └── 猫猫头_3.png │ └── utils.py ├── ImageSender │ ├── README.md │ ├── Sqlite3Manager.py │ ├── __init__.py │ ├── config.json │ ├── exceptions.py │ └── utils.py ├── KeywordDetection │ ├── DFA.py │ ├── Sqlite3Manager.py │ ├── __init__.py │ ├── keywordDetection.db │ └── utils.py ├── KeywordReply │ ├── DFA.py │ ├── Sqlite3Manager.py │ ├── __init__.py │ ├── keywordAppender.py │ └── utils.py ├── KissKiss │ ├── KissFrames │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ └── Kiss.gif │ ├── README.md │ ├── __init__.py │ ├── avatar.png │ └── requirements.txt ├── LeetcodeInfoCrawer │ ├── __init__.py │ ├── leetcode_daily_question_crawer.py │ └── leetcode_user_info_crawer.py ├── MessagePrinter.py ├── NetworkCompiler.py ├── NiBuNengXXMa │ ├── ArialEnUnicodeBold.ttf │ ├── BasicImage.jpg │ ├── __init__.py │ ├── requirements.txt │ └── 示例图片.jpg ├── PdfSearcher.py ├── PetPet │ ├── PetPetFrames │ │ ├── frame0.png │ │ ├── frame1.png │ │ ├── frame2.png │ │ ├── frame3.png │ │ ├── frame4.png │ │ └── template.gif │ ├── README.md │ ├── __init__.py │ └── requirements.txt ├── PhantomTank │ ├── __init__.py │ └── utils.py ├── PixivImageSearcher │ └── __init__.py ├── PornhubStyleLogoGenerator │ ├── __init__.py │ └── ttf │ │ └── ArialEnUnicodeBold.ttf ├── Repeater.py ├── SteamGameSearcher │ └── __init__.py ├── Text2QrcodeGenerator.py ├── Weather │ ├── README.md │ ├── __init__.py │ ├── config_demo.py │ ├── requirements.txt │ └── utils.py ├── WeiboHotSearch.py ├── WyySongOrderer │ ├── __init__.py │ ├── silk_v3_encoder.exe │ └── utils.py ├── ZhihuHotSearch.py └── __init__.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen 10 | 11 | /temp/ 12 | /.vscode/ 13 | __pycache__/ 14 | temp/ 15 | test.* 16 | config.* 17 | modules/PixivImageSearcher/tempSavedImage.png 18 | modules/SteamGameSearcher/game_cover_cache 19 | modules/GarbageClassification/img.jpg 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个Graia-Saya的插件仓库 2 | 3 | 这是一个存储基于 [Graia-Saya](https://github.com/GraiaProject/Saya) 的插件的仓库 4 | 5 | 如果您有这类项目,欢迎提交 Pull request 将您的项目添加到这里(注意,本仓库仅接受开源项目的仓库地址) 6 | 7 | ```diff 8 | 注意:本仓库仅提供插件存储,对插件内容并没有具体审查,请自行甄别 9 | ``` 10 | 11 | ## 如何使用 12 | 13 | 本仓库中所有自带插件都在modules中 14 | 15 | 若您想单独使用,可以将其下载并放入自己的module文件夹中 16 | 17 | 若您想开箱即用,您可以直接clone整个仓库并使用 `python main.py` 命令执行本仓库自带的启动程序 18 | 19 | 注意,若使用本仓库自带启动程序,您需要先将 `configdemo.json` 文件改名为 `config.json` 并填入其中的必要信息 20 | 21 | ## 插件列表 22 | 插件名|作者|功能描述|注意事项 23 | :--:|:--:|:--|:-- 24 | [MessagePrinter](modules/MessagePrinter.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个示例插件,输出所有收到的消息| 25 | [WeiboHotSearch](modules/WeiboHotSearch.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|获取当前微博热搜50条|本插件依赖于本仓库下 `utils.py` 中的 `messagechain_to_img` 函数 26 | [ZhihuHotSearch](modules/ZhihuHotSearch.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|获取当前知乎热搜50条|本插件依赖于本仓库下 `utils.py` 中的 `messagechain_to_img` 函数 27 | [GithubHotSearch](modules/GithubHotSearch.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|获取当前github热搜25条|本插件依赖于本仓库下 `utils.py` 中的 `messagechain_to_img` 函数 28 | [Repeater](modules/Repeater.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个复读插件| 29 | [PetPet](modules/PetPet)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|生成摸头gif| 30 | [PixivImageSearcher](modules/PixivImageSearcher)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个链接saucenao的以图搜图插件|请自行配置 saucenao cookie 31 | [PdfSearcher](modules/PdfSearcher.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以搜索pdf的插件| 32 | [NetworkCompiler](modules/NetworkCompiler.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|网络编译器(菜鸟教程)| 33 | [Text2QrcodeGenerator](modules/Text2QrcodeGenerator.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以将文字转为二维码的插件| 34 | [GroupWordCloudGenerator](modules/GroupWordCloudGenerator)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以记录聊天记录并生成个人/群组词云的插件| 35 | [BilibiliBangumiSchedule](modules/BilibiliBangumiSchedule.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以获取一周内B站新番时间表的插件| 36 | [KeywordReply](modules/KeywordReply)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个支持自定义回复的插件| 37 | [SteamGameSearcher](modules/SteamGameSearcher)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以搜索steam游戏的插件| 38 | [BangumiInfoSearcher](modules/BangumiInfoSearcher)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以搜索番剧信息的插件| 39 | [PornhubStyleLogoGenerator](modules/PornhubStyleLogoGenerator)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以生成 pornhub style logo 的插件| 40 | [AbbreviatedPrediction](modules/AbbreviatedPrediction.py)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以获取字母缩写内容的插件| 41 | [LeetcodeInfoCrawer](modules/LeetcodeInfoCrawer)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个可以获取leetcode信息的插件| 42 | [ImageSender](modules/ImageSender)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个图片~~(setu)~~发送插件| 43 | [HeadSplicer](modules/HeadSplicer)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个接头霸王插件| 44 | [WyySongOrderer](modules/WyySongOrderer)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个(全损音质x)网易云源的点歌插件| 45 | [5000Zhao](modules/5000zhao)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个 5000兆円欲しい! style的图片生成器| 46 | [KeywordDetection](modules/KeywordDetection)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个敏感词过滤插件(自带数据库)| 47 | [PhantomTank](modules/PhantomTank)|[SAGIRI-kawaii](https://github.com/SAGIRI-kawaii)|一个幻影坦克生成器| 48 | [NiBuNengXXMa](modules/NiBuNengXXMa)| [eeehhheee](https://github.com/eeehhheee) |生成如示例样式的图片|安装Pillow 49 | [BiliResolve](modules/BiliResolve)|[EnkanSakura](https://github.com/EnkanSakura)|B站视频分享解析| 50 | [ChatBot](modules/ChatBot)|[Roc136](https://github.com/Roc136)|聊天机器人|需要自行配置所用的机器人及所需的key 51 | [GarbageClassification](modules/GarbageClassification)|[Roc136](https://github.com/Roc136)|获取垃圾分类信息| 52 | [KissKiss](modules/KissKiss)|[SuperWaterGod](https://github.com/SuperWaterGod)|生成头像互亲的gif| 53 | [Weather](modules/Weather)|[Roc136](https://github.com/Roc136)|天气预报|需要自行配置`KEY` 54 | 55 | ## 其他 56 | 57 | 目前正在进行 SAGIRI-BOT 的重构工作,暂时无法更新插件,若您有好的插件或有好的想法,欢迎 Pr 或提 ISSUE 58 | 59 | -------------------------------------------------------------------------------- /configdemo.json: -------------------------------------------------------------------------------- 1 | { 2 | "BotQQ": 0, 3 | "authKey": "1234567890", 4 | "miraiHost": "http://localhost:8080" 5 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from graia.saya import Saya 5 | from graia.broadcast import Broadcast 6 | from graia.saya.builtins.broadcast import BroadcastBehaviour 7 | from graia.application import GraiaMiraiApplication, Session 8 | 9 | from utils import load_config 10 | 11 | loop = asyncio.get_event_loop() 12 | bcc = Broadcast(loop=loop) 13 | saya = Saya(bcc) 14 | saya.install_behaviours(BroadcastBehaviour(bcc)) 15 | 16 | configs = load_config() 17 | 18 | app = GraiaMiraiApplication( 19 | broadcast=bcc, 20 | connect_info=Session( 21 | host=configs["miraiHost"], 22 | authKey=configs["authKey"], 23 | account=configs["BotQQ"], 24 | websocket=True 25 | ) 26 | ) 27 | 28 | ignore = ["__init__.py", "__pycache__"] 29 | 30 | with saya.module_context(): 31 | for module in os.listdir("modules"): 32 | if module in ignore: 33 | continue 34 | try: 35 | if os.path.isdir(module): 36 | saya.require(f"modules.{module}") 37 | else: 38 | saya.require(f"modules.{module.split('.')[0]}") 39 | except ModuleNotFoundError: 40 | pass 41 | 42 | app.launch_blocking() 43 | 44 | try: 45 | loop.run_forever() 46 | except KeyboardInterrupt: 47 | exit() 48 | -------------------------------------------------------------------------------- /modules/5000zhao/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.application.message.chain import MessageChain 2 | from graia.application.message.elements.internal import Plain 3 | from graia.application.message.elements.internal import Image 4 | from graia.application import GraiaMiraiApplication 5 | from graia.saya import Saya, Channel 6 | from graia.saya.builtins.broadcast.schema import ListenerSchema 7 | from graia.application.exceptions import AccountMuted 8 | from graia.application.event.messages import GroupMessage, Group, Member 9 | from graia.application.message.parser.kanata import Kanata 10 | from graia.application.message.parser.signature import RegexMatch 11 | 12 | from .utils import genImage 13 | 14 | # 插件信息 15 | __name__ = "5000ZhaoStyleImageGenerator" 16 | __description__ = "一个 5000兆円欲しい! style的图片生成器" 17 | __author__ = "SAGIRI-kawaii" 18 | __usage__ = "发送 `5000兆 text1 text2` 即可" 19 | 20 | saya = Saya.current() 21 | channel = Channel.current() 22 | 23 | channel.name(__name__) 24 | channel.description(f"{__description__}\n使用方法:{__usage__}") 25 | channel.author(__author__) 26 | 27 | 28 | @channel.use(ListenerSchema( 29 | listening_events=[GroupMessage], 30 | inline_dispatchers=[Kanata([RegexMatch('5000兆 .* .*')])] 31 | )) 32 | async def pornhub_style_logo_generator( 33 | app: GraiaMiraiApplication, 34 | message: MessageChain, 35 | group: Group 36 | ): 37 | try: 38 | _, left_text, right_text = message.asDisplay().split(" ") 39 | try: 40 | try: 41 | genImage(word_a=left_text, word_b=right_text).save("./modules/5000zhao/test.png") 42 | except TypeError: 43 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="不支持的内容!不要给我一些稀奇古怪的东西!")])) 44 | return None 45 | await app.sendGroupMessage(group, MessageChain.create([Image.fromLocalFile("./modules/5000zhao/test.png")])) 46 | except AccountMuted: 47 | pass 48 | except ValueError: 49 | try: 50 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="参数非法!使用格式:5000兆 text1 text2")])) 51 | except AccountMuted: 52 | pass 53 | -------------------------------------------------------------------------------- /modules/5000zhao/fonts/ArialEnUnicodeBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/5000zhao/fonts/ArialEnUnicodeBold.ttf -------------------------------------------------------------------------------- /modules/5000zhao/fonts/STKAITI.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/5000zhao/fonts/STKAITI.TTF -------------------------------------------------------------------------------- /modules/5000zhao/fonts/notobk-subset.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/5000zhao/fonts/notobk-subset.otf -------------------------------------------------------------------------------- /modules/5000zhao/fonts/notoserifbk-subset.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/5000zhao/fonts/notoserifbk-subset.otf -------------------------------------------------------------------------------- /modules/5000zhao/fonts/simsunb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/5000zhao/fonts/simsunb.ttf -------------------------------------------------------------------------------- /modules/5000zhao/utils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import numpy as np 3 | from decimal import Decimal, ROUND_HALF_UP 4 | from math import radians, tan, cos, sin 5 | _round = lambda f, r=ROUND_HALF_UP: int(Decimal(str(f)).quantize(Decimal("0"), rounding=r)) 6 | rgb = lambda r, g, b: (r, g, b) 7 | 8 | 9 | def get_gradient_2d(start, stop, width, height, is_horizontal=False): 10 | if is_horizontal: 11 | return np.tile(np.linspace(start, stop, width), (height, 1)) 12 | else: 13 | return np.tile(np.linspace(start, stop, height), (width, 1)).T 14 | 15 | 16 | def getTextWidth(text, font, width=100, height=500, recursive=False): 17 | print(text) 18 | step = 100 19 | img = Image.new("L", (width, height)) 20 | draw = ImageDraw.Draw(img) 21 | draw.text((0, 0), text, font=font, fill=255) 22 | box = img.getbbox() 23 | if box[2] < width-step or (recursive and box[2] == width-step): 24 | return box[2] 25 | else: 26 | return getTextWidth(text=text, font=font, width=width+step, height=height, recursive=True) 27 | 28 | 29 | def get_gradient_3d(width, height, start_list, stop_list, is_horizontal_list=(False, False, False)): 30 | result = np.zeros((height, width, len(start_list)), dtype=float) 31 | for i, (start, stop, is_horizontal) in enumerate(zip(start_list, stop_list, is_horizontal_list)): 32 | result[:, :, i] = get_gradient_2d(start, stop, width, height, is_horizontal) 33 | return result 34 | 35 | 36 | def createLinearGradient(steps, width, height): 37 | result = np.zeros((0, width, len(steps[0])), dtype=float) 38 | for i, k in enumerate(steps.keys()): 39 | if i == 0: 40 | continue 41 | pk = list(steps.keys())[i-1] 42 | h = _round(height*(k-pk)) 43 | array = get_gradient_3d(width, h, steps[pk], steps[k]) 44 | result = np.vstack([result, array]) 45 | return result 46 | 47 | 48 | def genBaseImage(width=1500, height=150): 49 | downerSilverArray = createLinearGradient({ 50 | 0.0: rgb(0, 15, 36), 51 | 0.10: rgb(255, 255, 255), 52 | 0.18: rgb(55, 58, 59), 53 | 0.25: rgb(55, 58, 59), 54 | 0.5: rgb(200, 200, 200), 55 | 0.75: rgb(55, 58, 59), 56 | 0.85: rgb(25, 20, 31), 57 | 0.91: rgb(240, 240, 240), 58 | 0.95: rgb(166, 175, 194), 59 | 1: rgb(50, 50, 50) 60 | }, width=width, height=height) 61 | goldArray = createLinearGradient({ 62 | 0: rgb(253, 241, 0), 63 | 0.25: rgb(245, 253, 187), 64 | 0.4: rgb(255, 255, 255), 65 | 0.75: rgb(253, 219, 9), 66 | 0.9: rgb(127, 53, 0), 67 | 1: rgb(243, 196, 11) 68 | }, width=width, height=height) 69 | redArray = createLinearGradient({ 70 | 0: rgb(230, 0, 0), 71 | 0.5: rgb(123, 0, 0), 72 | 0.51: rgb(240, 0, 0), 73 | 1: rgb(5, 0, 0) 74 | }, width=width, height=height) 75 | strokeRedArray = createLinearGradient({ 76 | 0: rgb(255, 100, 0), 77 | 0.5: rgb(123, 0, 0), 78 | 0.51: rgb(240, 0, 0), 79 | 1: rgb(5, 0, 0) 80 | }, width=width, height=height) 81 | silver2Array = createLinearGradient({ 82 | 0: rgb(245, 246, 248), 83 | 0.15: rgb(255, 255, 255), 84 | 0.35: rgb(195, 213, 220), 85 | 0.5: rgb(160, 190, 201), 86 | 0.51: rgb(160, 190, 201), 87 | 0.52: rgb(196, 215, 222), 88 | 1.0: rgb(255, 255, 255) 89 | }, width=width, height=height) 90 | navyArray = createLinearGradient({ 91 | 0: rgb(16, 25, 58), 92 | 0.03: rgb(255, 255, 255), 93 | 0.08: rgb(16, 25, 58), 94 | 0.2: rgb(16, 25, 58), 95 | 1: rgb(16, 25, 58) 96 | }, width=width, height=height) 97 | result = { 98 | "downerSilver": Image.fromarray(np.uint8(downerSilverArray)).crop((0, 0, width, height)), 99 | "gold": Image.fromarray(np.uint8(goldArray)).crop((0, 0, width, height)), 100 | "red": Image.fromarray(np.uint8(redArray)).crop((0, 0, width, height)), 101 | "strokeRed": Image.fromarray(np.uint8(strokeRedArray)).crop((0, 0, width, height)), 102 | "silver2": Image.fromarray(np.uint8(silver2Array)).crop((0, 0, width, height)), 103 | "strokeNavy": Image.fromarray(np.uint8(navyArray)).crop((0, 0, width, height)), # Width: 7 104 | "baseStrokeBlack": Image.new("RGBA", (width, height), rgb(0, 0, 0)).crop((0, 0, width, height)), # Width: 17 105 | "strokeBlack": Image.new("RGBA", (width, height), rgb(16, 25, 58)).crop((0, 0, width, height)), # Width: 17 106 | "strokeWhite": Image.new("RGBA", (width, height), rgb(221, 221, 221)).crop((0, 0, width, height)), # Width: 8 107 | "baseStrokeWhite": Image.new("RGBA", (width, height), rgb(255, 255, 255)).crop((0, 0, width, height)) # Width: 8 108 | } 109 | for k in result.keys(): 110 | result[k].putalpha(255) 111 | return result 112 | 113 | 114 | def genImage(word_a="5000兆円", word_b="欲しい!", default_width=1500, height=500, 115 | bg="white", subset=250, default_base=None): 116 | # width = max_width 117 | alpha = (0, 0, 0, 0) 118 | leftmargin = 50 119 | font_upper = ImageFont.truetype("./modules/5000zhao/fonts/STKAITI.TTF", _round(height/3)) 120 | font_downer = ImageFont.truetype("./modules/5000zhao/fonts/STKAITI.TTF", _round(height/3)) 121 | 122 | # Prepare Width 123 | upper_width = max([default_width, 124 | getTextWidth(word_a, font_upper, width=default_width, 125 | height=_round(height/2))]) + 300 126 | downer_width = max([default_width, 127 | getTextWidth(word_b, font_upper, width=default_width, 128 | height=_round(height/2))]) + 300 129 | 130 | # Prepare base - Upper (if required) 131 | if default_width == upper_width: 132 | upper_base = default_base 133 | else: 134 | upper_base = genBaseImage(width=upper_width, height=_round(height/2)) 135 | 136 | # Prepare base - Downer (if required) 137 | downer_base = genBaseImage(width=downer_width+leftmargin, height=_round(height/2)) 138 | # if default_width == downer_width: 139 | # downer_base = default_base 140 | # else: 141 | 142 | # Prepare mask - Upper 143 | upper_mask_base = Image.new("L", (upper_width, _round(height/2)), 0) 144 | 145 | mask_img_upper = list() 146 | upper_data = [ 147 | [ 148 | (4, 4), (4, 4), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0) 149 | ], [ 150 | 22, 20, 16, 10, 6, 6, 4, 0 151 | ], [ 152 | "baseStrokeBlack", 153 | "downerSilver", 154 | "baseStrokeBlack", 155 | "gold", 156 | "baseStrokeBlack", 157 | "baseStrokeWhite", 158 | "strokeRed", 159 | "red", 160 | ] 161 | ] 162 | for pos, stroke, color in zip(upper_data[0], upper_data[1], upper_data[2]): 163 | mask_img_upper.append(upper_mask_base.copy()) 164 | mask_draw_upper = ImageDraw.Draw(mask_img_upper[-1]) 165 | mask_draw_upper.text((pos[0], pos[1]), word_a, 166 | font=font_upper, fill=255, 167 | stroke_width=_round(stroke*height/500)) 168 | 169 | # Prepare mask - Downer 170 | downer_mask_base = Image.new("L", (downer_width+leftmargin, _round(height/2)), 0) 171 | mask_img_downer = list() 172 | downer_data = [ 173 | [ 174 | (5, 2), (5, 2), (0, 0), (0, 0), (0, 0), (0, -3) 175 | ], [ 176 | 22, 19, 17, 8, 7, 0 177 | ], [ 178 | "baseStrokeBlack", 179 | "downerSilver", 180 | "strokeBlack", 181 | "strokeWhite", 182 | "strokeNavy", 183 | "silver2" 184 | ] 185 | ] 186 | for pos, stroke, color in zip(downer_data[0], downer_data[1], downer_data[2]): 187 | mask_img_downer.append(downer_mask_base.copy()) 188 | mask_draw_downer = ImageDraw.Draw(mask_img_downer[-1]) 189 | mask_draw_downer.text((pos[0]+leftmargin, pos[1]), word_b, 190 | font=font_downer, fill=255, 191 | stroke_width=_round(stroke*height/500)) 192 | 193 | # Draw text - Upper 194 | img_upper = Image.new("RGBA", (upper_width, _round(height/2)), alpha) 195 | 196 | for i, (pos, stroke, color) in enumerate(zip(upper_data[0], upper_data[1], upper_data[2])): 197 | img_upper_part = Image.new("RGBA", (upper_width, _round(height/2)), alpha) 198 | img_upper_part.paste(upper_base[color], (0, 0), mask=mask_img_upper[i]) 199 | img_upper.alpha_composite(img_upper_part) 200 | 201 | # Draw text - Downer 202 | img_downer = Image.new("RGBA", (downer_width+leftmargin, _round(height/2)), alpha) 203 | for i, (pos, stroke, color) in enumerate(zip(downer_data[0], downer_data[1], downer_data[2])): 204 | img_downer_part = Image.new("RGBA", (downer_width+leftmargin, _round(height/2)), alpha) 205 | img_downer_part.paste(downer_base[color], (0, 0), mask=mask_img_downer[i]) 206 | img_downer.alpha_composite(img_downer_part) 207 | 208 | # tilt image 209 | tiltres = list() 210 | angle = 20 211 | for img in [img_upper, img_downer]: 212 | dist = img.height * tan(radians(angle)) 213 | data = (1, tan(radians(angle)), -dist, 0, 1, 0) 214 | imgc = img.crop((0, 0, img.width+dist, img.height)) 215 | imgt = imgc.transform(imgc.size, Image.AFFINE, data, Image.BILINEAR) 216 | tiltres.append(imgt) 217 | 218 | # finish 219 | previmg = Image.new("RGBA", (max([upper_width, downer_width])+leftmargin+300 + 100, height + 100), (255,255,255,0)) 220 | # previmg.paste(tiltres[0], (0, 0)) 221 | # previmg.paste(tiltres[1], (subset, _round(height/2))) 222 | previmg.alpha_composite(tiltres[0], (0, 50), (0, 0)) 223 | previmg.alpha_composite(tiltres[1], (subset, _round(height/2) + 50), (0, 0)) 224 | previmg.save("./modules/5000zhao/test1.png") 225 | croprange = previmg.getbbox() 226 | img = previmg.crop(croprange) 227 | final_image = Image.new("RGB", (img.size[0] + 100, img.size[1] + 100), bg) 228 | final_image.paste(img, (50, 50)) 229 | 230 | return final_image 231 | 232 | 233 | # genImage(word_a="你爸", word_b="你爸").save("./temp.png") 234 | -------------------------------------------------------------------------------- /modules/AbbreviatedPrediction.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from graia.application.message.chain import MessageChain 4 | from graia.application.message.elements.internal import Plain 5 | from graia.saya import Saya, Channel 6 | from graia.saya.builtins.broadcast.schema import ListenerSchema 7 | from graia.application import GraiaMiraiApplication 8 | from graia.application.event.messages import GroupMessage, Group 9 | from graia.application.message.parser.kanata import Kanata 10 | from graia.application.message.parser.signature import RegexMatch 11 | from graia.application.exceptions import AccountMuted 12 | 13 | # 插件信息 14 | __name__ = "AbbreviatedPrediction" 15 | __description__ = "一个可以获取字母缩写内容的插件" 16 | __author__ = "SAGIRI-kawaii" 17 | __usage__ = "在群内发送 `缩 缩写` 即可" 18 | 19 | saya = Saya.current() 20 | channel = Channel.current() 21 | 22 | channel.name(__name__) 23 | channel.description(f"{__description__}\n使用方法:{__usage__}") 24 | channel.author(__author__) 25 | 26 | 27 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch('缩 .*')])])) 28 | async def abbreviated_prediction(app: GraiaMiraiApplication, message: MessageChain, group: Group): 29 | if abbreviation := message.asDisplay()[2:]: 30 | try: 31 | if abbreviation.isalnum(): 32 | await app.sendGroupMessage(group, await get_abbreviation_explain(abbreviation)) 33 | else: 34 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="缩写部分只能为英文/数字!")])) 35 | except AccountMuted: 36 | pass 37 | 38 | 39 | async def get_abbreviation_explain(abbreviation: str) -> MessageChain: 40 | url = "https://lab.magiconch.com/api/nbnhhsh/guess" 41 | headers = { 42 | "referer": "https://lab.magiconch.com/nbnhhsh/" 43 | } 44 | data = { 45 | "text": abbreviation 46 | } 47 | 48 | async with aiohttp.ClientSession() as session: 49 | async with session.post(url=url, headers=headers, data=data) as resp: 50 | res = await resp.json() 51 | # print(res) 52 | result = "可能的结果:\n\n" 53 | has_result = False 54 | for i in res: 55 | if "trans" in i: 56 | if i["trans"]: 57 | has_result = True 58 | result += f"{i['name']} => {','.join(i['trans'])}\n\n" 59 | else: 60 | result += f"{i['name']} => 没找到结果!\n\n" 61 | else: 62 | if i["inputting"]: 63 | has_result = True 64 | result += f"{i['name']} => {','.join(i['inputting'])}\n\n" 65 | else: 66 | result += f"{i['name']} => 没找到结果!\n\n" 67 | 68 | if has_result: 69 | return MessageChain.create([Plain(text=result)]) 70 | else: 71 | return MessageChain.create([Plain(text="没有找到结果哦~")]) 72 | -------------------------------------------------------------------------------- /modules/BangumiInfoSearcher/__init__.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import os 3 | from PIL import Image as IMG 4 | from io import BytesIO 5 | import urllib.parse as parse 6 | 7 | from graia.application.message.chain import MessageChain 8 | from graia.application.message.elements.internal import Plain 9 | from graia.application.message.elements.internal import At 10 | from graia.application.message.elements.internal import Image 11 | from graia.application import GraiaMiraiApplication 12 | from graia.saya import Saya, Channel 13 | from graia.saya.builtins.broadcast.schema import ListenerSchema 14 | from graia.application.exceptions import AccountMuted 15 | from graia.application.event.messages import GroupMessage, Group, Member 16 | from graia.application.message.parser.kanata import Kanata 17 | from graia.application.message.parser.signature import RegexMatch 18 | 19 | from utils import messagechain_to_img 20 | 21 | # 插件信息 22 | __name__ = "BangumiInfoSearcher" 23 | __description__ = "一个可以搜索番剧信息的插件" 24 | __author__ = "SAGIRI-kawaii" 25 | __usage__ = "发送 `番剧 番剧名` 即可" 26 | 27 | saya = Saya.current() 28 | channel = Channel.current() 29 | 30 | channel.name(__name__) 31 | channel.description(f"{__description__}\n使用方法:{__usage__}") 32 | channel.author(__author__) 33 | 34 | 35 | @channel.use(ListenerSchema( 36 | listening_events=[GroupMessage], 37 | inline_dispatchers=[Kanata([RegexMatch('番剧 .*')])] 38 | )) 39 | async def bangumi_info_searcher( 40 | app: GraiaMiraiApplication, 41 | message: MessageChain, 42 | group: Group, 43 | member: Member 44 | ): 45 | keyword = message.asDisplay()[3:] 46 | try: 47 | if keyword: 48 | await app.sendGroupMessage(group, await get_bangumi_info(keyword, member.id)) 49 | else: 50 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="请输入你要搜索的关键词")])) 51 | except AccountMuted: 52 | pass 53 | 54 | 55 | async def get_bangumi_info(keyword: str, sender: int) -> MessageChain: 56 | headers = { 57 | "user-agent": 58 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36" 59 | } 60 | url = "https://api.bgm.tv/search/subject/%s?type=2&responseGroup=Large&max_results=1" % parse.quote(keyword) 61 | # print(url) 62 | async with aiohttp.ClientSession() as session: 63 | async with session.post(url=url, headers=headers) as resp: 64 | data = await resp.json() 65 | 66 | if "code" in data.keys() and data["code"] == 404 or not data["list"]: 67 | return MessageChain.create([At(target=sender), Plain(text=f"番剧 {keyword} 未搜索到结果!")]) 68 | 69 | bangumi_id = data["list"][0]["id"] 70 | url = "https://api.bgm.tv/subject/%s?responseGroup=medium" % bangumi_id 71 | # print(url) 72 | 73 | async with aiohttp.ClientSession() as session: 74 | async with session.post(url=url, headers=headers) as resp: 75 | data = await resp.json() 76 | # print(data) 77 | name = data["name"] 78 | cn_name = data["name_cn"] 79 | summary = data["summary"] 80 | img_url = data["images"]["large"] 81 | score = data["rating"]["score"] 82 | rank = data["rank"] if "rank" in data.keys() else None 83 | rating_total = data["rating"]["total"] 84 | path = f"./modules/BangumiInfoSearcher/bangumi_cover_cache/{name}.jpg" 85 | if not os.path.exists("./modules/BangumiInfoSearcher/bangumi_cover_cache"): 86 | os.mkdir("./modules/BangumiInfoSearcher/bangumi_cover_cache") 87 | if not os.path.exists(path): 88 | async with aiohttp.ClientSession() as session: 89 | async with session.get(url=img_url) as resp: 90 | img_content = await resp.read() 91 | image = IMG.open(BytesIO(img_content)) 92 | image.save(path) 93 | message = MessageChain.create([ 94 | Plain(text="查询到以下信息:\n"), 95 | Image.fromLocalFile(path), 96 | Plain(text=f"名字:{name}\n\n中文名字:{cn_name}\n\n"), 97 | Plain(text=f"简介:{summary}\n\n"), 98 | Plain(text=f"bangumi评分:{score}(参与评分{rating_total}人)"), 99 | Plain(text=f"\n\nbangumi排名:{rank}" if rank else "") 100 | ]) 101 | return await messagechain_to_img(message=message, max_width=1080, img_fixed=True) 102 | -------------------------------------------------------------------------------- /modules/BiliResolve/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lxml import etree 3 | 4 | from graia.application import GraiaMiraiApplication 5 | from graia.application.event.messages import GroupMessage, Group 6 | from graia.application.exceptions import AccountMuted 7 | from graia.application.message.chain import MessageChain 8 | from graia.application.message.elements import internal as Msg_element 9 | from graia.saya import Channel, Saya 10 | from graia.saya.builtins.broadcast.schema import ListenerSchema 11 | 12 | from .utils import get_config, gen_video_info_dict 13 | 14 | 15 | # 插件信息 16 | __name__ = "BiliResolve" 17 | __description__ = "解析B站视频分享链接" 18 | __author__ = "EnkanSakura" 19 | __usage__ = "在群内分享B站视频即可" 20 | 21 | 22 | saya = Saya.current() 23 | channel = Channel.current() 24 | 25 | channel.name(__name__) 26 | channel.description(f"{__description__}\n使用方法:{__usage__}") 27 | channel.author(__author__) 28 | 29 | config = get_config() 30 | 31 | 32 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 33 | async def bili_resolve( 34 | app: GraiaMiraiApplication, 35 | message: MessageChain, 36 | group: Group 37 | ): 38 | # print(config) 39 | if group.id not in config['group'] or not config: 40 | return None 41 | url = '' 42 | # print(group.id) 43 | if Msg_element.App in message: 44 | json_msg = json.loads(message.get(Msg_element.App)[0].content) 45 | try: 46 | desc = json_msg['desc'] 47 | except KeyError: 48 | pass 49 | else: 50 | if desc == '哔哩哔哩': 51 | url = json_msg['meta']['detail_1']['qqdocurl'] 52 | elif desc == '新闻': 53 | try: 54 | tag = json_msg['meta']['news']['tag'] 55 | except KeyError: 56 | pass 57 | else: 58 | url = json_msg['meta']['news']['jumpUrl'] 59 | # print('receive App\turl=', url) 60 | elif Msg_element.Xml in message: 61 | xml_msg = etree.fromstring( 62 | message.get(Msg_element.Xml)[0].xml.encode('utf-8') 63 | ) 64 | try: 65 | url = xml_msg.xpath('/msg/@url')[0] 66 | except IndexError: 67 | pass 68 | else: 69 | pass 70 | # print('receive Xml\turl=', url) 71 | else: 72 | try: 73 | url = message.get(Msg_element.Plain)[0] 74 | except IndexError: 75 | pass 76 | else: 77 | url = url.to_string() 78 | # print(message) 79 | if url.find('https://') != -1: 80 | if video_info := gen_video_info_dict(url): 81 | await app.sendGroupMessage( 82 | group=group, 83 | message=MessageChain.create([ 84 | Msg_element.Image.fromNetworkAddress(video_info['image']), 85 | Msg_element.Plain(video_info['plain']) 86 | ]) 87 | ) 88 | -------------------------------------------------------------------------------- /modules/BiliResolve/configdemo.json: -------------------------------------------------------------------------------- 1 | { 2 | "master": 123456, 3 | "group": [ 4 | 1111111, 5 | 22222222 6 | ] 7 | } -------------------------------------------------------------------------------- /modules/BiliResolve/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.25.1 2 | lxml==4.6.3 3 | bilibili_api==2.1.4 4 | -------------------------------------------------------------------------------- /modules/BiliResolve/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | import requests 5 | from lxml import etree 6 | from bilibili_api.video import get_video_info 7 | from bilibili_api.utils import aid2bvid 8 | 9 | 10 | def get_config(): 11 | pwd = os.getcwd().replace('\\', '/') 12 | with open(pwd+'/modules/BiliResolve/config.json', 'r', encoding='utf-8') as j: 13 | config = json.load(j) 14 | return config 15 | return None 16 | 17 | 18 | def get_bvid(url: str): 19 | b23_pattern = re.compile(r"https://b23.tv/[A-Za-z0-9]+") 20 | bv_pattern = re.compile(r"[Bb][Vv][A-Za-z0-9]+") 21 | av_pattern = re.compile(r"[Aa][Vv][0-9]+") 22 | bvid = '' 23 | # print(url) 24 | if result := bv_pattern.search(url): 25 | # print('bv') 26 | bvid = result.group() 27 | elif result := av_pattern.search(url): 28 | bvid = aid2bvid(int(re.sub('[Aa][Vv]', '', result.group()))) 29 | elif result := b23_pattern.search(url): 30 | # print('b23') 31 | b23_url = result.group() 32 | try: 33 | resp = requests.get(b23_url, allow_redirects=False) 34 | except: 35 | pass 36 | else: 37 | if result := bv_pattern.search(resp.text): 38 | bvid = result.group() 39 | # print(bvid) 40 | return bvid 41 | 42 | 43 | def gen_video_info_dict(url: str): 44 | if (bvid := get_bvid(url)) == '': 45 | return None 46 | # print('bvid=', bvid) 47 | info = get_video_info(bvid=bvid) 48 | return { 49 | 'image': info['pic'], 50 | 'plain': '标题:{title}\nUP主:{author}\nAV号:{aid} BV号:{bvid}\n\n简介:\n{intro}\n\n播放:{play}\n弹幕:{danmaku}\n回复:{reply}\n获赞:{like}\n投币:{coin}\n收藏:{favorite}\n分享:{share}\n\n视频链接:{link}' 51 | .format( 52 | title=info['title'], 53 | author=info['owner']['name'], 54 | aid=info['aid'], 55 | bvid=info['bvid'], 56 | intro=info['desc'], 57 | play=info['stat']['view'], 58 | danmaku=info['stat']['view'], 59 | reply=info['stat']['reply'], 60 | like=info['stat']['like'], 61 | coin=info['stat']['coin'], 62 | favorite=info['stat']['favorite'], 63 | share=info['stat']['share'], 64 | link='https://www.bilibili.com/video/{}'.format(info['bvid']) 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /modules/BilibiliBangumiSchedule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import aiohttp 3 | import datetime 4 | 5 | from graia.application.message.elements.internal import Plain 6 | from graia.application import GraiaMiraiApplication 7 | from graia.saya import Saya, Channel 8 | from graia.saya.builtins.broadcast.schema import ListenerSchema 9 | from graia.application.event.messages import * 10 | from graia.application.message.parser.kanata import Kanata 11 | from graia.application.message.parser.signature import RegexMatch 12 | from graia.application.exceptions import AccountMuted 13 | 14 | from utils import messagechain_to_img 15 | 16 | # 插件信息 17 | __name__ = "BilibiliBangumiSchedule" 18 | __description__ = "获取一周内B站新番时间表" 19 | __author__ = "SAGIRI-kawaii" 20 | __usage__ = "在群内发送 [1-7]日内新番 即可" 21 | 22 | saya = Saya.current() 23 | channel = Channel.current() 24 | 25 | channel.name(__name__) 26 | channel.description(f"{__description__}\n使用方法:{__usage__}") 27 | channel.author(__author__) 28 | 29 | 30 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch('[1-7]日内新番')])])) 31 | async def bilibili_bangumi_schedule( 32 | app: GraiaMiraiApplication, 33 | message: MessageChain, 34 | group: Group 35 | ): 36 | days = message.asDisplay()[0] 37 | try: 38 | await app.sendGroupMessage(group, await formatted_output_bangumi(int(days))) 39 | except AccountMuted: 40 | pass 41 | 42 | 43 | async def get_new_bangumi_json() -> dict: 44 | """ 45 | Get json data from bilibili 46 | 47 | Args: 48 | 49 | Examples: 50 | data = await get_new_bangumi_json() 51 | 52 | Return: 53 | dict:data get from bilibili 54 | """ 55 | url = "https://bangumi.bilibili.com/web_api/timeline_global" 56 | headers = { 57 | "accept": "application/json, text/plain, */*", 58 | "accept-encoding": "gzip, deflate, br", 59 | "accept-language": "zh-CN,zh;q=0.9", 60 | "origin": "https://www.bilibili.com", 61 | "referer": "https://www.bilibili.com/", 62 | "sec-fetch-dest": "empty", 63 | "sec-fetch-mode": "cors", 64 | "sec-fetch-site": "same-site", 65 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36" 66 | } 67 | async with aiohttp.ClientSession() as session: 68 | async with session.post(url=url, headers=headers) as resp: 69 | result = await resp.json() 70 | return result 71 | 72 | 73 | async def get_formatted_new_bangumi_json() -> list: 74 | """ 75 | Format the json data 76 | 77 | Args: 78 | 79 | Examples: 80 | data = get_formatted_new_bangumi_json() 81 | 82 | Returns: 83 | { 84 | "title": str, 85 | "cover": str, 86 | "pub_index": str, 87 | "pub_time": str, 88 | "url": str 89 | } 90 | """ 91 | all_bangumi_data = await get_new_bangumi_json() 92 | all_bangumi_data = all_bangumi_data["result"][-7:] 93 | formatted_bangumi_data = list() 94 | 95 | for bangumi_data in all_bangumi_data: 96 | temp_bangumi_data_list = list() 97 | for data in bangumi_data["seasons"]: 98 | temp_bangumi_data_dict = dict() 99 | temp_bangumi_data_dict["title"] = data["title"] 100 | temp_bangumi_data_dict["cover"] = data["cover"] 101 | temp_bangumi_data_dict["pub_index"] = data["pub_index"] 102 | temp_bangumi_data_dict["pub_time"] = data["pub_time"] 103 | temp_bangumi_data_dict["url"] = data["url"] 104 | temp_bangumi_data_list.append(temp_bangumi_data_dict) 105 | formatted_bangumi_data.append(temp_bangumi_data_list) 106 | 107 | return formatted_bangumi_data 108 | 109 | 110 | async def formatted_output_bangumi(days: int) -> MessageChain: 111 | """ 112 | Formatted output json data 113 | 114 | Args: 115 | days: The number of days to output(1-7) 116 | 117 | Examples: 118 | data_str = formatted_output_bangumi(7) 119 | 120 | Return: 121 | MessageChain 122 | """ 123 | formatted_bangumi_data = await get_formatted_new_bangumi_json() 124 | temp_output_substring = ["------BANGUMI------\n\n"] 125 | now = datetime.datetime.now() 126 | for index in range(days): 127 | temp_output_substring.append(now.strftime("%m-%d")) 128 | temp_output_substring.append("即将播出:") 129 | for data in formatted_bangumi_data[index]: 130 | temp_output_substring.append("\n%s %s %s\n" % (data["pub_time"], data["title"], data["pub_index"])) 131 | # temp_output_substring.append("url:%s\n" % (data["url"])) 132 | temp_output_substring.append("\n\n----------------\n\n") 133 | now += datetime.timedelta(days=1) 134 | 135 | content = "".join(temp_output_substring) 136 | return await messagechain_to_img(MessageChain.create([Plain(text=content)])) 137 | -------------------------------------------------------------------------------- /modules/ChatBot/README.md: -------------------------------------------------------------------------------- 1 | # 基于 saya 的QQ聊天机器人 2 | 3 | 使用前需要自行修改配置,复制或重命名 `configdemo.json` 为 `config.json`,修改 `bot` 为自己选用的机器人,并在下面的设置中添加 appKey 或 apiKey 等信息 4 | 5 | 目前已接入的机器人: 6 | 7 | + [青云客机器人](https://api.qingyunke.com/) 8 | + [如意机器人](https://ruyi.ai/) 9 | + [图灵机器人](http://www.tuling123.com/) -------------------------------------------------------------------------------- /modules/ChatBot/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Saya, Channel 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.application.event.messages import * 4 | from graia.application.event.mirai import * 5 | from graia.application import GraiaMiraiApplication 6 | from graia.application.message.elements.internal import Plain, At, Image, Voice 7 | from graia.application import session 8 | from graia.application.message.elements.internal import MessageChain 9 | from .utils import get_reply 10 | 11 | 12 | # 插件信息 13 | __name__ = "ChatBot" 14 | __description__ = "QQ聊天机器人,目前已接入青云客、如意和图灵三种机器人" 15 | __author__ = "Roc" 16 | __usage__ = "At机器人并发送消息即可" 17 | 18 | 19 | saya = Saya.current() 20 | channel = Channel.current() 21 | 22 | channel.name(__name__) 23 | channel.description(f"{__description__}\n使用方法:{__usage__}") 24 | channel.author(__author__) 25 | 26 | 27 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 28 | async def group_message_listener(app:GraiaMiraiApplication, message: MessageChain, member: Member, group: Group): 29 | if message.has(At) and message.getFirst(At).target == app.connect_info.account: 30 | reply, img_path, voice_path = await get_reply(''.join([p.text for p in message.get(Plain)])) 31 | if voice_path is not None: 32 | msg = MessageChain.create([Voice.fromLocalFile(voice_path)]) 33 | elif img_path is not None: 34 | msg = MessageChain.create([At(member.id), Plain(reply), Image.fromLocalFile(img_path)]) 35 | else: 36 | msg = MessageChain.create([At(member.id), Plain(reply)]) 37 | try: 38 | await app.sendGroupMessage( 39 | group, msg 40 | ) 41 | except AccountMuted: 42 | pass 43 | -------------------------------------------------------------------------------- /modules/ChatBot/configdemo.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": "ruyi", 3 | "qingyunke": 4 | { 5 | "key": "free" 6 | }, 7 | "ruyi": 8 | { 9 | "appKey": "123456789abcde", 10 | "userID": "mirai-qq-bot" 11 | }, 12 | "tuling": 13 | { 14 | "apiKey": "123456789abcde", 15 | "userID": "mirai" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/ChatBot/utils.py: -------------------------------------------------------------------------------- 1 | from aiohttp import ClientSession 2 | import json 3 | 4 | 5 | def load_config(config_file: str = "./modules/ChatBot/config.json") -> dict: 6 | with open(config_file, 'r', encoding='utf-8') as f: # 从json读配置 7 | config = json.loads(f.read()) 8 | for key in config.keys(): 9 | config[key] = config[key].strip() if isinstance(config[key], str) else config[key] 10 | return config 11 | 12 | 13 | config = load_config() 14 | print(config) 15 | bot = config['bot'] 16 | 17 | 18 | async def get_reply(msg): 19 | if bot == 'qingyunke': 20 | return await get_qingyunke_reply(msg) 21 | elif bot == 'ruyi': 22 | return await get_ruyi_reply(msg) 23 | elif bot == 'tuling': 24 | return await get_tuling_reply(msg) 25 | 26 | 27 | # 青云客机器人,https://api.qingyunke.com 28 | async def get_qingyunke_reply(msg): 29 | reply = '' 30 | img_path = None 31 | voice_path = None 32 | 33 | key = config['qingyunke']['key'] 34 | url= f'http://api.qingyunke.com/api.php?key={ key }&appid=0&msg={ msg }' 35 | async with ClientSession() as session: 36 | async with session.get(url) as response: 37 | content = await response.read() 38 | reply = json.loads(content.decode('utf-8'))['content'].replace('{br}', '\n') 39 | 40 | return ' ' + reply, img_path, voice_path 41 | 42 | 43 | # 如意机器人,https://ruyi.ai/ 44 | async def get_ruyi_reply(msg): 45 | reply = '' 46 | img_path = None 47 | voice_path = None 48 | 49 | appKey = config['ruyi']['appKey'] 50 | userID = config['ruyi']['userID'] 51 | url = f"http://api.ruyi.ai/v1/message?q={ msg }&app_key={ appKey }&user_id={ userID }" 52 | replys = [] 53 | async with ClientSession() as session: 54 | async with session.get(url) as response: 55 | content = await response.json() 56 | if content['code'] == 0: 57 | replys = content['result']['intents'][0]['outputs'] 58 | for r in replys: 59 | if r['type'] == 'dialog': 60 | reply = r['property']['text'] 61 | 62 | return ' ' + reply, img_path, voice_path 63 | 64 | 65 | # 图灵机器人,http://www.tuling123.com/ 66 | async def get_tuling_reply(msg): 67 | reply = ' ' 68 | apiKey = config['tuling']['apiKey'] 69 | userID = config['tuling']['userID'] 70 | url = 'http://openapi.tuling123.com/openapi/api/v2' 71 | data = { 72 | "reqType":0, 73 | "perception": { 74 | "inputText": { 75 | "text": msg 76 | } 77 | }, 78 | "userInfo": { 79 | "apiKey": apiKey, 80 | "userId": userID 81 | } 82 | } 83 | async with ClientSession() as session: 84 | async with session.post(url, json = data) as response: 85 | content = await response.read() 86 | print(content) 87 | results = json.loads(content.decode('utf-8')) 88 | if results['intent']['code'] == 4003: 89 | reply += '我今天不能和你聊天啦~' 90 | else: 91 | for res in results['results']: 92 | reply += res['values']['text'] 93 | return reply, None, None -------------------------------------------------------------------------------- /modules/GarbageClassification/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Saya, Channel 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.application.event.messages import * 4 | from graia.application.event.mirai import * 5 | from graia.application.message.parser.kanata import Kanata 6 | from graia.application.message.parser.signature import RegexMatch 7 | from graia.application import GraiaMiraiApplication 8 | from graia.application.message.elements.internal import Plain, At, Image, Voice 9 | from graia.application import session 10 | from graia.application.message.elements.internal import MessageChain 11 | import re 12 | import requests 13 | from lxml import etree 14 | 15 | 16 | # 插件信息 17 | __name__ = "GarbageClassification" 18 | __description__ = "查询某个城市对某种物品的垃圾分类" 19 | __author__ = "Roc" 20 | __usage__ = "发送 \"垃圾分类 物品 城市\"即可" 21 | 22 | 23 | saya = Saya.current() 24 | channel = Channel.current() 25 | 26 | channel.name(__name__) 27 | channel.description(f"{__description__}\n使用方法:{__usage__}") 28 | channel.author(__author__) 29 | 30 | headers={ 31 | "User-Agent" : "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.6) ", 32 | "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 33 | "Accept-Language" : "en-us", 34 | "Connection" : "keep-alive", 35 | "Accept-Charset" : "GB2312,utf-8;q=0.7,*;q=0.7" 36 | } 37 | CITYS = ["北京", "天津", "上海", "重庆", "石家庄", "邯郸", "太原", "呼和浩特", "沈阳", 38 | "大连", "长春", "哈尔滨", "南京", "苏州", "杭州", "宁波", "合肥", "铜陵", "福州", 39 | "厦门", "南昌", "宜春", "郑州", "济南", "泰安", "青岛", "武汉", "长沙", "宜昌", 40 | "广州", "深圳", "南宁", "海口", "成都", "广元", "德阳", "贵阳", "昆明", "拉萨", 41 | "日喀则", "西安", "咸阳", "兰州", "西宁", "银川", "乌鲁木齐"] 42 | 43 | 44 | def getclassify(thing, city): 45 | response = ' ' 46 | img_path = None 47 | if city in CITYS: 48 | html = requests.get(f"https://lajifenleiapp.com/sk/{ thing }?l={ city }", headers=headers) 49 | selector = etree.HTML(html.text) 50 | try: 51 | kind = selector.xpath("/html/body/div[1]/div[7]/div/div[1]/h1/span[3]")[0] 52 | response += f"{ thing }在{ city }属于{ kind.text }" 53 | img_url = selector.xpath("/html/body/div[1]/div[7]/div/div[3]/img/@src")[0] 54 | try: 55 | img_res = requests.get(img_url, headers=headers) 56 | img_path = './modules/GarbageClassification/img.jpg' 57 | with open(img_path, 'wb') as f: 58 | f.write(img_res.content) 59 | except Exception as e: 60 | print("img error:", e) 61 | except: 62 | response += f"没有找到{ thing }在{ city }的分类信息!" 63 | else: 64 | response += f"暂不支持当前城市,支持的城市列表:{ ', '.join(CITYS) }" 65 | return response, img_path 66 | 67 | 68 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch('垃圾分类 .*')])])) 69 | async def group_message_listener(app:GraiaMiraiApplication, message: MessageChain, member: Member, group: Group): 70 | re_res = re.search(r'垃圾分类\ (.+?)[ ,,/|\*&](.+)', message.asDisplay()) 71 | print(re_res) 72 | if re_res: 73 | thing = re_res.group(1) 74 | city = re_res.group(2) 75 | # print(thing, city) 76 | reply, img_path = getclassify(thing, city) 77 | if img_path is not None: 78 | msg = MessageChain.create([At(member.id), Plain(reply), Image.fromLocalFile(img_path)]) 79 | else: 80 | msg = MessageChain.create([At(member.id), Plain(reply)]) 81 | try: 82 | await app.sendGroupMessage( 83 | group, msg 84 | ) 85 | except AccountMuted: 86 | pass 87 | -------------------------------------------------------------------------------- /modules/GarbageClassification/requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | requests 3 | -------------------------------------------------------------------------------- /modules/GithubHotSearch.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from bs4 import BeautifulSoup 3 | 4 | from graia.application.message.elements.internal import Plain 5 | from graia.application import GraiaMiraiApplication 6 | from graia.application.message.parser.kanata import Kanata 7 | from graia.application.message.parser.signature import FullMatch 8 | from graia.saya import Saya, Channel 9 | from graia.saya.builtins.broadcast.schema import ListenerSchema 10 | from graia.application.event.messages import * 11 | from graia.application.event.mirai import * 12 | from graia.application.exceptions import AccountMuted 13 | 14 | from utils import messagechain_to_img 15 | 16 | # 插件信息 17 | __name__ = "GithubHotSearch" 18 | __description__ = "获取当前github热搜(trend)" 19 | __author__ = "SAGIRI-kawaii" 20 | __usage__ = "在群内发送 github热搜 即可" 21 | 22 | saya = Saya.current() 23 | channel = Channel.current() 24 | 25 | channel.name(__name__) 26 | channel.description(f"{__description__}\n使用方法:{__usage__}") 27 | channel.author(__author__) 28 | 29 | 30 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([FullMatch('github热搜')])])) 31 | async def group_message_listener(app: GraiaMiraiApplication, group: Group): 32 | try: 33 | await app.sendGroupMessage( 34 | group, 35 | await get_github_hot() 36 | ) 37 | except AccountMuted: 38 | pass 39 | 40 | 41 | async def get_github_hot() -> MessageChain: 42 | url = "https://github.com/trending" 43 | headers = { 44 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36" 45 | } 46 | async with aiohttp.ClientSession() as session: 47 | async with session.get(url=url, headers=headers) as resp: 48 | html = await resp.read() 49 | soup = BeautifulSoup(html, "html.parser") 50 | articles = soup.find_all("article", {"class": "Box-row"}) 51 | 52 | text_list = ["github实时热榜:\n"] 53 | index = 0 54 | for i in articles: 55 | try: 56 | index += 1 57 | text_list.append("\n%d. %s\n" % (index, i.find("h1").get_text().replace("\n", "").replace(" ", "").replace("\\", " \\ "))) 58 | text_list.append("\n %s\n" % i.find("p").get_text().strip()) 59 | except: 60 | pass 61 | 62 | text = "".join(text_list).replace("#", "") 63 | return await messagechain_to_img(MessageChain.create([Plain(text=text)]), max_width=2000) 64 | -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/README.md: -------------------------------------------------------------------------------- 1 | # GroupWordCloudGenerator 2 | 3 | 一个群/个人词云生成器 4 | 5 | ## 如何使用 6 | 7 | 在群内发送 `我的月内总结` / `我的年内总结` / `本群月内总结` / `本群年内总结` 即可 8 | 9 | ## 使用注意 10 | 11 | - 请先使用 `pip install -r requirements.txt` 命令安装所需依赖 12 | - 请在使用前修改 `BASE_PATH` (可选) -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/STKAITI.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/GroupWordCloudGenerator/STKAITI.TTF -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/Sqlite3Manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | 4 | 5 | class Sqlite3Manager: 6 | __instance = None 7 | __first_init: bool = False 8 | path: str = None 9 | __conn = None 10 | 11 | def __new__(cls): 12 | if not cls.__instance: 13 | cls.__instance = object.__new__(cls) 14 | return cls.__instance 15 | 16 | def __init__(self): 17 | if not self.__first_init: 18 | self.path = os.getcwd() 19 | self.__conn = sqlite3.connect(self.path + './modules/GroupWordCloudGenerator/chatRecord.db') 20 | cur = self.__conn.cursor() 21 | cur.execute( 22 | """CREATE TABLE IF NOT EXISTS `chatrecord` ( 23 | `time` TEXT NOT NULL, 24 | `groupId` INTEGER NOT NULL, 25 | `memberId` INTEGER NOT NULL, 26 | `content` TEXT NOT NULL, 27 | `seg` text NOT NULL 28 | )""" 29 | ) 30 | self.__conn.commit() 31 | 32 | Sqlite3Manager.__first_init = True 33 | else: 34 | raise ValueError("Sqlite3Manager already initialized!") 35 | 36 | @classmethod 37 | def get_instance(cls): 38 | if cls.__instance: 39 | return cls.__instance 40 | else: 41 | raise ValueError("Sqlite3Manager not initialized!") 42 | 43 | @classmethod 44 | def get_connection(cls): 45 | if cls.__conn: 46 | return cls.__conn 47 | else: 48 | raise ValueError("Sqlite3Manager not initialized!") 49 | 50 | def execute(self, sql: str): 51 | if sql.lower().startswith("select"): 52 | cur = self.__conn.cursor() 53 | cur.execute(sql) 54 | result = cur.fetchall() 55 | cur.close() 56 | return result 57 | else: 58 | cur = self.__conn.cursor() 59 | cur.execute(sql) 60 | self.__conn.commit() 61 | cur.close() 62 | return 63 | 64 | 65 | manager = Sqlite3Manager() 66 | 67 | 68 | async def execute_sql(sql: str): 69 | instance = Sqlite3Manager.get_instance() 70 | return instance.execute(sql) 71 | -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from PIL import Image as IMG 5 | from wordcloud import WordCloud, ImageColorGenerator 6 | from dateutil.relativedelta import relativedelta 7 | import pkuseg 8 | import re 9 | 10 | from graia.saya import Saya, Channel 11 | from graia.saya.builtins.broadcast.schema import ListenerSchema 12 | from graia.application.message.elements.internal import MessageChain 13 | from graia.application.message.elements.internal import Plain 14 | from graia.application.message.elements.internal import Image 15 | from graia.application.event.messages import GroupMessage 16 | from graia.application import GraiaMiraiApplication 17 | from graia.application.group import Group, Member 18 | from graia.application.exceptions import AccountMuted 19 | 20 | from .Sqlite3Manager import execute_sql 21 | 22 | # 插件信息 23 | __name__ = "GroupWordCloudGenerator" 24 | __description__ = "记录聊天记录并生成个人/群组词云" 25 | __author__ = "SAGIRI-kawaii" 26 | __usage__ = "在群内发送 我的月内总结/我的年内总结/本群月内总结/本群年内总结 即可" 27 | 28 | seg = pkuseg.pkuseg() 29 | 30 | BASE_PATH = "./modules/GroupWordCloudGenerator/" 31 | 32 | saya = Saya.current() 33 | channel = Channel.current() 34 | 35 | channel.name(__name__) 36 | channel.description(f"{__description__}\n使用方法:{__usage__}") 37 | channel.author(__author__) 38 | 39 | 40 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 41 | async def group_wordcloud_generator(app: GraiaMiraiApplication, message: MessageChain, group: Group, member: Member): 42 | """ 43 | 群/个人词云生成器 44 | 使用方法: 45 | 群内发送 我的月内总结/我的年内总结/本群月内总结/本群年内总结 即可 46 | 插件来源: 47 | SAGIRI-kawaii 48 | """ 49 | message_text = message.asDisplay() 50 | member_id = member.id 51 | group_id = group.id 52 | await write_chat_record(seg, group_id, member_id, message_text) 53 | try: 54 | if message_text == "我的月内总结": 55 | await app.sendGroupMessage(group, await get_review(group_id, member_id, "month", "member")) 56 | elif message_text == "我的年内总结": 57 | await app.sendGroupMessage(group, await get_review(group_id, member_id, "year", "member")) 58 | elif message_text == "本群月内总结": 59 | await app.sendGroupMessage(group, await get_review(group_id, member_id, "month", "group")) 60 | elif message_text == "本群年内总结": 61 | await app.sendGroupMessage(group, await get_review(group_id, member_id, "year", "group")) 62 | except AccountMuted: 63 | pass 64 | 65 | 66 | async def count_words(sp, n): 67 | w = {} 68 | for i in sp: 69 | if i not in w: 70 | w[i] = 1 71 | else: 72 | w[i] += 1 73 | top = sorted(w.items(), key=lambda item: (-item[1], item[0])) 74 | top_n = top[:n] 75 | return top_n 76 | 77 | 78 | async def filter_label(label_list: list) -> list: 79 | """ 80 | Filter labels 81 | 82 | Args: 83 | label_list: Words to filter 84 | 85 | Examples: 86 | result = await filter_label(label_list) 87 | 88 | Return: 89 | list 90 | """ 91 | not_filter = ["草"] 92 | image_filter = "mirai:" 93 | result = [] 94 | for i in label_list: 95 | if image_filter in i: 96 | continue 97 | elif i in not_filter: 98 | result.append(i) 99 | elif len(i) != 1 and i.find('nbsp') < 0: 100 | result.append(i) 101 | return result 102 | 103 | 104 | async def write_chat_record(seg, group_id: int, member_id: int, content: str) -> None: 105 | content = content.replace("\\", "/") 106 | filter_words = re.findall(r"\[mirai:(.*?)\]", content, re.S) 107 | for i in filter_words: 108 | content = content.replace(f"[mirai:{i}]", "") 109 | content = content.replace("\"", " ") 110 | seg_result = seg.cut(content) 111 | seg_result = await filter_label(seg_result) 112 | # print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 113 | sql = f"""INSERT INTO chatRecord 114 | (`time`, groupId, memberId, content, seg) 115 | VALUES 116 | (\"{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\", {group_id}, {member_id}, \"{content}\", 117 | \"{','.join(seg_result)}\") """ 118 | await execute_sql(sql) 119 | 120 | 121 | async def draw_word_cloud(read_name): 122 | mask = np.array(IMG.open(f'{BASE_PATH}back.jpg')) 123 | print(mask.shape) 124 | wc = WordCloud( 125 | font_path=f'{BASE_PATH}STKAITI.TTF', 126 | background_color='white', 127 | # max_words=500, 128 | max_font_size=100, 129 | width=1920, 130 | height=1080, 131 | mask=mask 132 | ) 133 | name = [] 134 | value = [] 135 | for t in read_name: 136 | name.append(t[0]) 137 | value.append(t[1]) 138 | for i in range(len(name)): 139 | name[i] = str(name[i]) 140 | # name[i] = name[i].encode('gb2312').decode('gb2312') 141 | dic = dict(zip(name, value)) 142 | print(dic) 143 | print(len(dic.keys())) 144 | wc.generate_from_frequencies(dic) 145 | image_colors = ImageColorGenerator(mask, default_color=(255, 255, 255)) 146 | print(image_colors.image.shape) 147 | wc.recolor(color_func=image_colors) 148 | plt.imshow(wc.recolor(color_func=image_colors), interpolation="bilinear") 149 | # plt.imshow(wc) 150 | plt.axis("off") 151 | # plt.show() 152 | wc.to_file(f'{BASE_PATH}tempWordCloud.png') 153 | 154 | 155 | async def get_review(group_id: int, member_id: int, review_type: str, target: str) -> MessageChain: 156 | time = datetime.now() 157 | year, month, day, hour, minute, second = time.strftime("%Y %m %d %H %M %S").split(" ") 158 | if review_type == "year": 159 | yearp, monthp, dayp, hourp, minutep, secondp = (time - relativedelta(years=1)).strftime("%Y %m %d %H %M %S").split(" ") 160 | tag = "年内" 161 | elif review_type == "month": 162 | yearp, monthp, dayp, hourp, minutep, secondp = (time - relativedelta(months=1)).strftime("%Y %m %d %H %M %S").split(" ") 163 | tag = "月内" 164 | else: 165 | return MessageChain.create([ 166 | Plain(text="Error: review_type invalid!") 167 | ]) 168 | 169 | sql = f"""SELECT * FROM chatRecord 170 | WHERE 171 | groupId={group_id} {f'AND memberId={member_id}' if target == 'member' else ''} 172 | AND time<'{year}-{month}-{day} {hour}:{minute}:{second}' 173 | AND time>'{yearp}-{monthp}-{dayp} {hourp}:{minutep}:{secondp}'""" 174 | # print(sql) 175 | res = await execute_sql(sql) 176 | texts = [] 177 | for i in res: 178 | if i[4]: 179 | texts += i[4].split(",") 180 | else: 181 | texts.append(i[3]) 182 | print(texts) 183 | top_n = await count_words(texts, 20000) 184 | await draw_word_cloud(top_n) 185 | sql = f"""SELECT count(*) FROM chatRecord 186 | WHERE 187 | groupId={group_id} {f'AND memberId={member_id}' if target == 'member' else ''} 188 | AND time<'{year}-{month}-{day} {hour}:{minute}:{second}' 189 | AND time>'{yearp}-{monthp}-{dayp} {hourp}:{minutep}:{secondp}'""" 190 | res = await execute_sql(sql) 191 | times = res[0][0] 192 | return MessageChain.create([ 193 | Plain(text="记录时间:\n"), 194 | Plain(text=f"{yearp}-{monthp}-{dayp} {hourp}:{minutep}:{secondp}"), 195 | Plain(text="\n---------至---------\n"), 196 | Plain(text=f"{year}-{month}-{day} {hour}:{minute}:{second}"), 197 | Plain(text=f"\n自有记录以来,{'你' if target == 'member' else '本群'}一共发了{times}条消息\n下面是{'你的' if target == 'member' else '本群的'}{tag}词云:\n"), 198 | Image.fromLocalFile(f"{BASE_PATH}tempWordCloud.png") 199 | ]) 200 | -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/GroupWordCloudGenerator/back.jpg -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pkuseg 3 | matplotlib 4 | wordcloud 5 | Pillow 6 | python_dateutil 7 | -------------------------------------------------------------------------------- /modules/GroupWordCloudGenerator/simsunb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/GroupWordCloudGenerator/simsunb.ttf -------------------------------------------------------------------------------- /modules/HeadSplicer/__init__.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import os 3 | from io import BytesIO 4 | 5 | from graia.saya import Saya, Channel 6 | from graia.saya.builtins.broadcast.schema import ListenerSchema 7 | from graia.application.exceptions import AccountMuted 8 | from graia.application import GraiaMiraiApplication 9 | from graia.application.event.messages import GroupMessage, Group, Member 10 | from graia.application.message.parser.kanata import Kanata 11 | from graia.application.message.parser.signature import RegexMatch 12 | from graia.application.event.messages import MessageChain 13 | from graia.application.message.elements.internal import Plain 14 | from graia.application.message.elements.internal import Image 15 | 16 | from .utils import * 17 | 18 | # 插件信息 19 | __name__ = "HeadSplicer" 20 | __description__ = "一个接头霸王插件,修改自 https://github.com/pcrbot/plugins-for-Hoshino/tree/master/shebot/conhead" 21 | __author__ = "SAGIRI-kawaii" 22 | __usage__ = "群内发送 `接头[图片]` 即可" 23 | 24 | saya = Saya.current() 25 | channel = Channel.current() 26 | 27 | channel.name(__name__) 28 | channel.description(f"{__description__}\n使用方法:{__usage__}") 29 | channel.author(__author__) 30 | 31 | signal: int = 0 32 | 33 | 34 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch("接头.*")])])) 35 | async def head_splicer(app: GraiaMiraiApplication, message: MessageChain, member: Member, group: Group): 36 | print(globals()["signal"]) 37 | if not os.path.exists("./modules/HeadSplicer/temp/"): 38 | os.mkdir("./modules/HeadSplicer/temp/") 39 | if "".join([plain.text for plain in message.get(Plain)]).strip() == "接头": 40 | if globals()["signal"] >= 2: 41 | try: 42 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=f"目前有{signal}个任务正在处理,请稍后再试!")])) 43 | except AccountMuted: 44 | pass 45 | return None 46 | 47 | globals()["signal"] += 1 48 | 49 | if message.get(Image): 50 | image = message[Image][0] 51 | img_url = image.url 52 | async with aiohttp.ClientSession() as session: 53 | async with session.get(url=img_url) as resp: 54 | img_content = await resp.read() 55 | image = IMG.open(BytesIO(img_content)) 56 | image.save(f"./modules/HeadSplicer/temp/temp-{group.id}-{member.id}.png") 57 | try: 58 | try: 59 | splicing_result = await process( 60 | f"./modules/HeadSplicer/temp/temp-{group.id}-{member.id}.png", 61 | f"./modules/HeadSplicer/temp/tempResult-{group.id}-{member.id}.png" 62 | ) 63 | except TooManyFacesDetected: 64 | globals()["signal"] -= 1 65 | await app.sendGroupMessage( 66 | group, 67 | MessageChain.create([Plain(text="脸太tm多了!说!你是不是故意欺负我!爪巴啊啊啊啊啊啊!")]) 68 | ) 69 | return None 70 | except Exception: 71 | globals()["signal"] -= 1 72 | await app.sendGroupMessage( 73 | group, 74 | MessageChain.create([Image.fromLocalFile("./modules/HeadSplicer/statics/接头失败.png")]) 75 | ) 76 | return None 77 | if splicing_result: 78 | globals()["signal"] -= 1 79 | await app.sendGroupMessage( 80 | group, 81 | MessageChain.create([ 82 | Image.fromLocalFile(f"./modules/HeadSplicer/temp/tempResult-{group.id}-{member.id}.png") 83 | ]) 84 | ) 85 | else: 86 | globals()["signal"] -= 1 87 | await app.sendGroupMessage( 88 | group, 89 | MessageChain.create([Image.fromLocalFile("./modules/HeadSplicer/statics/没找到头.png")]) 90 | ) 91 | except AccountMuted: 92 | return None 93 | else: 94 | globals()["signal"] -= 1 95 | try: 96 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="请附带图片!")])) 97 | except AccountMuted: 98 | pass 99 | -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/head/1/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/head/1/1.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/head/1/dat.json: -------------------------------------------------------------------------------- 1 | { 2 | "angle" : -7.4, 3 | "face_width" : 418, 4 | "chin_tip_x" : 398, 5 | "chin_tip_y" : 892 6 | } -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/head/2/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/head/2/2.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/head/2/dat.json: -------------------------------------------------------------------------------- 1 | { 2 | "angle" : 4.5, 3 | "face_width" : 372, 4 | "chin_tip_x" : 498, 5 | "chin_tip_y" : 839 6 | } -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/接头失败.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/接头失败.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/没找到头.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/没找到头.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/猫猫头_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/猫猫头_0.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/猫猫头_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/猫猫头_1.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/猫猫头_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/猫猫头_2.png -------------------------------------------------------------------------------- /modules/HeadSplicer/statics/猫猫头_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/HeadSplicer/statics/猫猫头_3.png -------------------------------------------------------------------------------- /modules/HeadSplicer/utils.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | import cv2 4 | from PIL import Image as IMG 5 | 6 | 7 | cascades = [ 8 | cv2.CascadeClassifier("./modules/HeadSplicer/statics/haarcascade_frontalface_alt.xml"), 9 | cv2.CascadeClassifier("./modules/HeadSplicer/statics/haarcascade_profileface.xml"), 10 | cv2.CascadeClassifier("./modules/HeadSplicer/statics/lbpcascade_animeface.xml"), 11 | cv2.CascadeClassifier("./modules/HeadSplicer/statics/haarcascade_frontalcatface.xml"), 12 | cv2.CascadeClassifier("./modules/HeadSplicer/statics/haarcascade_frontalcatface_extended.xml"), 13 | ] 14 | 15 | 16 | class TooManyFacesDetected(Exception): 17 | """ 脸太tm多了!爬! """ 18 | 19 | 20 | async def process(filename, outfile) -> bool: 21 | cvimg = cv2.imread(filename, cv2.IMREAD_COLOR) # 图片灰度化 22 | gray = cv2.cvtColor(cvimg, cv2.COLOR_BGR2GRAY) # 直方图均衡化 23 | gray = cv2.equalizeHist(gray) # 加载级联分类器 24 | for cascade in cascades: 25 | # print(cascade_file) 26 | # cascade = cv2.CascadeClassifier(cascade_file) # 加载级联分类器 27 | faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(24, 24)) # 多尺度检测 28 | if not len(faces): 29 | continue 30 | if len(faces) >= 10: 31 | raise TooManyFacesDetected 32 | img = IMG.open(filename) 33 | img = img.convert("RGBA") 34 | top_shift_scale = 0.45 35 | x_scale = 0.25 36 | for (x, y, w, h) in faces: 37 | y_shift = int(h * top_shift_scale) 38 | x_shift = int(w * x_scale) 39 | face_w = max(w + 2 * x_shift, h + y_shift) 40 | faceimg = IMG.open("./modules/HeadSplicer/statics/猫猫头_" + str(randint(0, 3)) + ".png") 41 | faceimg = faceimg.resize((face_w, face_w)) 42 | r, g, b, a = faceimg.split() 43 | img.paste(faceimg, (x - x_shift, y - y_shift), mask=a) 44 | img.save(outfile) 45 | return True 46 | return False 47 | -------------------------------------------------------------------------------- /modules/ImageSender/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/ImageSender/README.md -------------------------------------------------------------------------------- /modules/ImageSender/Sqlite3Manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | 4 | 5 | keywords_init_sql = [ 6 | "INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('setu', 'setu')", 7 | "INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('real', 'real')", 8 | "INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('rhq', 'realHighq')", 9 | "INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('bizhi', 'bizhi')", 10 | "INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('sketch', 'sketch')", 11 | ] 12 | 13 | 14 | class Sqlite3Manager: 15 | __instance = None 16 | __first_init: bool = False 17 | path: str = None 18 | __conn = None 19 | 20 | def __new__(cls): 21 | if not cls.__instance: 22 | cls.__instance = object.__new__(cls) 23 | return cls.__instance 24 | 25 | def __init__(self): 26 | if not self.__first_init: 27 | self.path = os.getcwd() 28 | self.__conn = sqlite3.connect(self.path + './modules/ImageSender/imageSenderInfo.db') 29 | cur = self.__conn.cursor() 30 | cur.execute( 31 | """CREATE TABLE IF NOT EXISTS `setting` ( 32 | `groupId` INT NOT NULL, 33 | `setu` INT NOT NULL DEFAULT 0, 34 | `setu18` INT NOT NULL DEFAULT 0, 35 | `real` INT NOT NULL DEFAULT 0, 36 | `realHighq` INT NOT NULL DEFAULT 0, 37 | `bizhi` INT NOT NULL DEFAULT 0, 38 | `sketch` INT NOT NULL DEFAULT 0 39 | )""" 40 | ) 41 | cur.execute( 42 | """CREATE TABLE IF NOT EXISTS `keywords` ( 43 | `keyword` TEXT PRIMARY KEY, 44 | `function` TEXT NOT NULL 45 | )""" 46 | ) 47 | for sql in keywords_init_sql: 48 | cur.execute(sql) 49 | cur.execute( 50 | """CREATE TABLE IF NOT EXISTS `admin` ( 51 | `groupId` INT NOT NULL, 52 | `adminId` INT NOT NULL 53 | )""" 54 | ) 55 | self.__conn.commit() 56 | 57 | Sqlite3Manager.__first_init = True 58 | else: 59 | raise ValueError("Sqlite3Manager already initialized!") 60 | 61 | @classmethod 62 | def get_instance(cls): 63 | if cls.__instance: 64 | return cls.__instance 65 | else: 66 | raise ValueError("Sqlite3Manager not initialized!") 67 | 68 | @classmethod 69 | def get_connection(cls): 70 | if cls.__conn: 71 | return cls.__conn 72 | else: 73 | raise ValueError("Sqlite3Manager not initialized!") 74 | 75 | def get_conn(self): 76 | if self.__conn: 77 | return self.__conn 78 | else: 79 | raise ValueError("Sqlite3Manager not initialized!") 80 | 81 | def execute(self, sql: str): 82 | if sql.lower().startswith("select"): 83 | cur = self.__conn.cursor() 84 | cur.execute(sql) 85 | result = cur.fetchall() 86 | cur.close() 87 | return result 88 | else: 89 | cur = self.__conn.cursor() 90 | cur.execute(sql) 91 | self.__conn.commit() 92 | cur.close() 93 | return 94 | 95 | 96 | manager = Sqlite3Manager() 97 | 98 | 99 | async def execute_sql(sql: str): 100 | instance = Sqlite3Manager.get_instance() 101 | return instance.execute(sql) 102 | -------------------------------------------------------------------------------- /modules/ImageSender/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from graia.saya import Saya, Channel 4 | from graia.saya.builtins.broadcast.schema import ListenerSchema 5 | from graia.application.exceptions import AccountMuted 6 | from graia.application import GraiaMiraiApplication 7 | from graia.application.event.messages import GroupMessage, Group, Member 8 | from graia.application.message.parser.kanata import Kanata 9 | from graia.application.message.parser.signature import RegexMatch 10 | from graia.application.event.lifecycle import ApplicationLaunched 11 | from graia.application.event.mirai import BotJoinGroupEvent 12 | from graia.broadcast.interrupt import InterruptControl 13 | from graia.broadcast.interrupt.waiter import Waiter 14 | 15 | from .Sqlite3Manager import execute_sql 16 | from .utils import * 17 | 18 | # 插件信息 19 | __name__ = "ImageSender" 20 | __description__ = "一个图片(setu)发送插件" 21 | __author__ = "SAGIRI-kawaii" 22 | __usage__ = "获取图片:在群内发送设置好的关键词即可\n" \ 23 | "关键词管理:在群内发送 `(添加/删除)关键词(文字/图片)` 即可(需要管理)\n" \ 24 | "管理员管理:在群内发送 `(添加/删除)管理员(At/QQ号)` 即可(需要hostQQ)" 25 | 26 | saya = Saya.current() 27 | channel = Channel.current() 28 | bcc = saya.broadcast 29 | inc = InterruptControl(bcc) 30 | 31 | channel.name(__name__) 32 | channel.description(f"{__description__}\n使用方法:{__usage__}") 33 | channel.author(__author__) 34 | 35 | 36 | @channel.use(ListenerSchema(listening_events=[ApplicationLaunched])) 37 | async def data_init(app: GraiaMiraiApplication): 38 | group_list = await app.groupList() 39 | await check_group_data_init(group_list) 40 | 41 | 42 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 43 | async def image_sender(app: GraiaMiraiApplication, message: MessageChain, group: Group): 44 | message_serialization = message.asSerializationString() 45 | message_serialization = message_serialization.replace( 46 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 47 | "" 48 | ) 49 | if re.match(r"\[mirai:image:{.*}\..*]", message_serialization): 50 | message_serialization = re.findall(r"\[mirai:image:{(.*?)}\..*]", message_serialization, re.S)[0] 51 | sql = f"SELECT * FROM keywords WHERE keyword='{message_serialization}'" 52 | if result := await execute_sql(sql): 53 | function = result[0][1] 54 | if function == "setu": 55 | sql = f"SELECT setu, setu18 FROM setting WHERE groupId='{group.id}'" 56 | result = (await execute_sql(sql))[0] 57 | if result[0] and result[1]: 58 | await app.sendGroupMessage(group, await get_pic("setu18")) 59 | elif result[0] and not result[1]: 60 | await app.sendGroupMessage(group, await get_pic(function)) 61 | else: 62 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="烧鹅图功能尚未开启~")])) 63 | elif function in ["real", "realHighq"]: 64 | sql = f"SELECT real, realHighq FROM setting WHERE groupId='{group.id}'" 65 | result = (await execute_sql(sql))[0] 66 | if result[0] and result[1]: 67 | await app.sendGroupMessage(group, await get_pic(function)) 68 | elif result[0] and not result[1] and function == "real": 69 | await app.sendGroupMessage(group, await get_pic(function)) 70 | else: 71 | await app.sendGroupMessage( 72 | group, 73 | MessageChain.create([Plain(text=f"{function}图功能尚未开启~")]) 74 | ) 75 | else: 76 | sql = f"SELECT {function} FROM setting WHERE groupId='{group.id}'" 77 | if (await execute_sql(sql))[0][0]: 78 | await app.sendGroupMessage(group, await get_pic(function)) 79 | else: 80 | await app.sendGroupMessage( 81 | group, 82 | MessageChain.create([Plain(text=f"{function}图功能尚未开启~")]) 83 | ) 84 | 85 | 86 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch("(打开|关闭).*功能")])])) 87 | async def switch_control(app: GraiaMiraiApplication, message: MessageChain, group: Group, member: Member): 88 | legal_config = ("setu", "setu18", "real", "realHighq", "bizhi", "sketch") 89 | config = re.findall(r"(.*?)功能", message.asDisplay()[2:], re.S)[0] 90 | if message.asDisplay().startswith("打开"): 91 | new_setting_value = 1 92 | else: 93 | new_setting_value = 0 94 | try: 95 | if config in legal_config: 96 | admins = await get_admin(group.id) 97 | if member.id in admins: 98 | await update_setting(group.id, config, new_setting_value) 99 | await app.sendGroupMessage(group, MessageChain.create([Plain(f"本群{config}已{message.asDisplay()[:2]}")])) 100 | else: 101 | await app.sendGroupMessage(group, MessageChain.create([Plain("不是管理员!给爷爬!")])) 102 | else: 103 | await app.sendGroupMessage(group, MessageChain.create([Plain("错误的选项!")])) 104 | except AccountMuted: 105 | pass 106 | 107 | 108 | @channel.use(ListenerSchema( 109 | listening_events=[GroupMessage], 110 | # inline_dispatchers=[Kanata([RegexMatch("(添加|删除)管理员.*")])] 111 | ) 112 | ) 113 | async def admin_manage(app: GraiaMiraiApplication, message: MessageChain, group: Group, member: Member): 114 | message_serialization = message.asSerializationString() 115 | message_serialization = message_serialization.replace( 116 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 117 | "" 118 | ) 119 | if re.match(r"(添加|删除)管理员.*", message_serialization): 120 | if member.id == configs["hostQQ"]: 121 | print("command get:", message_serialization) 122 | if re.match(r"(添加|删除)管理员(\[mirai:at:[0-9]*,]|[0-9]*)", message_serialization): 123 | if result := re.findall(r"管理员\[mirai:at:(.*?),]", message_serialization, re.S): 124 | target = int(result[0]) 125 | elif message_serialization[5:].isdigit(): 126 | target = int(message_serialization[5:]) 127 | else: 128 | try: 129 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="未获取到成员!检查参数!")])) 130 | return None 131 | except AccountMuted: 132 | return None 133 | print(message_serialization) 134 | if target_member := await app.getMember(group, target): 135 | try: 136 | await app.sendGroupMessage( 137 | group, 138 | MessageChain.create([ 139 | Plain(text="获取到以下信息:\n"), 140 | Plain(text=f"成员ID:{target_member.id}\n"), 141 | Plain(text=f"成员昵称:{target_member.name}\n"), 142 | Plain(text=f"成员本群权限(QQ):{target_member.permission}\n"), 143 | Plain(text=f"亲爱的你确定要{'设置他为管理员' if message_serialization[:2] == '添加' else '撤回他的管理员权限'}嘛?(是/否)") 144 | ]) 145 | ) 146 | except AccountMuted: 147 | return None 148 | 149 | result = "否" 150 | 151 | @Waiter.create_using_function([GroupMessage]) 152 | def waiter( 153 | event: GroupMessage, waiter_group: Group, 154 | waiter_member: Member, waiter_message: MessageChain 155 | ): 156 | nonlocal result 157 | if all([ 158 | waiter_group.id == group.id, 159 | waiter_member.id == member.id 160 | ]): 161 | if re.match(r"[是否]", waiter_message.asDisplay()): 162 | result = waiter_message.asDisplay() 163 | return event 164 | 165 | await inc.wait(waiter) 166 | 167 | if result == "是": 168 | try: 169 | await app.sendGroupMessage( 170 | group, 171 | await admin_management( 172 | group.id, 173 | target, 174 | "add" if message_serialization[:2] == "添加" else "delete" 175 | ) 176 | ) 177 | except AccountMuted: 178 | pass 179 | else: 180 | try: 181 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="进程关闭")])) 182 | except AccountMuted: 183 | pass 184 | else: 185 | try: 186 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=f"未在本群找到成员{target}!进程关闭")])) 187 | except AccountMuted: 188 | pass 189 | else: 190 | try: 191 | await app.sendGroupMessage( 192 | group, 193 | MessageChain.create([ 194 | Plain(text="你不是我的主人!只有主人才可以添删管理员,难道你想篡位?!来人啊!快护驾!") 195 | ]) 196 | ) 197 | except AccountMuted: 198 | pass 199 | 200 | 201 | @channel.use(ListenerSchema( 202 | listening_events=[GroupMessage], 203 | inline_dispatchers=[Kanata([RegexMatch("(添加|删除).*关键词.*")])])) 204 | async def keyword_manage(app: GraiaMiraiApplication, message: MessageChain, group: Group, member: Member): 205 | legal_config = ("setu", "real", "realHighq", "bizhi", "sketch") 206 | message_serialization = message.asSerializationString() 207 | message_serialization = message_serialization.replace( 208 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 209 | "" 210 | ) 211 | function, keyword = message_serialization[2:].split("关键词") 212 | function = function.strip() 213 | keyword = keyword.strip() 214 | if not function: return None 215 | if member.id in await get_admin(group.id): 216 | if function in legal_config: 217 | if re.match(r"\[mirai:image:{.*}\..*]", keyword): 218 | keyword = re.findall(r"\[mirai:image:{(.*?)}\..*]", keyword, re.S)[0] 219 | print(keyword) 220 | sql = f"SELECT function FROM keywords WHERE keyword='{keyword}'" 221 | if result := await execute_sql(sql): 222 | try: 223 | await app.sendGroupMessage( 224 | group, 225 | MessageChain.create([ 226 | Plain(text=f"关键词{keyword}已被占用,具体信息:\n"), 227 | Plain(text=f"{keyword} -> {result[0][0]}") 228 | ]) 229 | ) 230 | except AccountMuted: 231 | return None 232 | else: 233 | sql = f"INSERT OR IGNORE INTO keywords (keyword, function) VALUES ('{keyword}', '{function}')" 234 | await execute_sql(sql) 235 | try: 236 | await app.sendGroupMessage( 237 | group, 238 | MessageChain.create([ 239 | Plain(text=f"关键词{keyword}添加成功!\n"), 240 | Plain(text=f"{keyword} -> {function}") 241 | ]) 242 | ) 243 | except AccountMuted: 244 | pass 245 | else: 246 | try: 247 | await app.sendGroupMessage( 248 | group, 249 | MessageChain.create([ 250 | Plain(text=f"没有{function}功能哦~\n"), 251 | Plain(text="目前的功能:\n"), 252 | Plain(text="\n".join(legal_config)) 253 | ]) 254 | ) 255 | except AccountMuted: 256 | pass 257 | else: 258 | try: 259 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="哼,你又不是管理员,我才不听你的!")])) 260 | except AccountMuted: 261 | pass 262 | 263 | 264 | @channel.use(ListenerSchema(listening_events=[BotJoinGroupEvent])) 265 | async def join_group_init(event: BotJoinGroupEvent): 266 | group_id = event.group.id 267 | await add_group(group_id) 268 | -------------------------------------------------------------------------------- /modules/ImageSender/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostQQ": 1900384123, 3 | "setuPath": "M:\\Pixiv\\pxer_new\\", 4 | "setu18Path": "M:\\Pixiv\\pxer18_new\\", 5 | "realPath": "M:\\Pixiv\\reality\\", 6 | "realHighqPath": "M:\\Pixiv\\reality\\highq\\", 7 | "wallpaperPath": "M:\\Pixiv\\bizhi\\highq\\", 8 | "sketchPath": "M:\\线稿\\" 9 | } -------------------------------------------------------------------------------- /modules/ImageSender/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConfigurationNotFound(Exception): 2 | """ 未配置config.json """ 3 | pass 4 | 5 | 6 | class ImagePathEmpty(Exception): 7 | """ 文件夹下没有图片 """ 8 | pass 9 | -------------------------------------------------------------------------------- /modules/ImageSender/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import os 4 | from itertools import chain 5 | 6 | from graia.application.message.chain import MessageChain 7 | from graia.application.message.elements.internal import Plain 8 | from graia.application.message.elements.internal import Image 9 | 10 | from .Sqlite3Manager import execute_sql 11 | from .exceptions import * 12 | 13 | 14 | def load_config(config_file: str = "./modules/ImageSender/config.json") -> dict: 15 | with open(config_file, 'r', encoding='utf-8') as f: # 从json读配置 16 | config = json.loads(f.read()) 17 | return config 18 | 19 | 20 | configs = load_config() 21 | 22 | 23 | async def get_setting(group_id: int, setting_name: str) -> int: 24 | """ 25 | Return setting from database 26 | 27 | Args: 28 | group_id: group id 29 | setting_name: setting name 30 | 31 | Examples: 32 | setting = get_setting(12345678, "repeat") 33 | 34 | Return: 35 | Operation result 36 | """ 37 | sql = f"SELECT {setting_name} from setting WHERE groupId={group_id}" 38 | data = await execute_sql(sql) 39 | return data[0][0] 40 | 41 | 42 | async def random_pic(base_path: str) -> str: 43 | """ 44 | Return random pic path in base_dir 45 | 46 | Args: 47 | base_path: Target library path 48 | 49 | Examples: 50 | pic_path = random_pic(wallpaper_path) 51 | 52 | Return: 53 | str: Target pic path 54 | """ 55 | path_dir = os.listdir(base_path) 56 | if not path_dir: 57 | raise ImagePathEmpty() 58 | path = random.sample(path_dir, 1)[0] 59 | return base_path + path 60 | 61 | 62 | async def get_pic(image_type: str) -> MessageChain: 63 | """ 64 | Return random pics message 65 | 66 | Args: 67 | image_type: The type of picture to return 68 | 69 | Examples: 70 | assist_process = await get_pic("setu")[0] 71 | message = await get_pic("real")[1] 72 | 73 | Return: 74 | [ 75 | str: Auxiliary treatment to be done(Such as add statement), 76 | MessageChain: Message to be send(MessageChain) 77 | ] 78 | """ 79 | async def color() -> str: 80 | if "setuPath" in configs.keys(): 81 | base_path = configs["setuPath"] 82 | else: 83 | raise ConfigurationNotFound() 84 | pic_path = await random_pic(base_path) 85 | return pic_path 86 | 87 | async def color18() -> str: 88 | if "setu18Path" in configs.keys(): 89 | base_path = configs["setu18Path"] 90 | else: 91 | raise ConfigurationNotFound() 92 | pic_path = await random_pic(base_path) 93 | return pic_path 94 | 95 | async def real() -> str: 96 | if "realPath" in configs.keys(): 97 | base_path = configs["realPath"] 98 | else: 99 | raise ConfigurationNotFound() 100 | pic_path = await random_pic(base_path) 101 | return pic_path 102 | 103 | async def real_highq() -> str: 104 | if "realHighqPath" in configs.keys(): 105 | base_path = configs["realHighqPath"] 106 | else: 107 | raise ConfigurationNotFound() 108 | pic_path = await random_pic(base_path) 109 | return pic_path 110 | 111 | async def wallpaper() -> str: 112 | if "wallpaperPath" in configs.keys(): 113 | base_path = configs["wallpaperPath"] 114 | else: 115 | raise ConfigurationNotFound() 116 | pic_path = await random_pic(base_path) 117 | return pic_path 118 | 119 | async def sketch() -> str: 120 | if "sketchPath" in configs.keys(): 121 | base_path = configs["sketchPath"] 122 | else: 123 | raise ConfigurationNotFound() 124 | pic_path = await random_pic(base_path) 125 | return pic_path 126 | 127 | switch = { 128 | "setu": color, 129 | "setu18": color18, 130 | "real": real, 131 | "realHighq": real_highq, 132 | "bizhi": wallpaper, 133 | "sketch": sketch 134 | } 135 | 136 | try: 137 | target_pic_path = await switch[image_type]() 138 | except ConfigurationNotFound: 139 | return MessageChain.create([Plain(f"{image_type}Path参数未配置!请检查配置文件!")]) 140 | except ImagePathEmpty: 141 | return MessageChain.create([Plain(f"{image_type}文件夹为空!请添加图片!")]) 142 | message = MessageChain.create([ 143 | Image.fromLocalFile(target_pic_path) 144 | ]) 145 | return message 146 | 147 | 148 | async def check_group_data_init(group_list: list) -> None: 149 | sql = "select groupId from setting" 150 | data = await execute_sql(sql) 151 | group_id = list(chain.from_iterable(data)) 152 | for i in group_list: 153 | # print(i.id, ':', i.name) 154 | if i.id not in group_id: 155 | sql = f"INSERT INTO setting (groupId) VALUES ({i.id})" 156 | await execute_sql(sql) 157 | sql = f"INSERT INTO admin (groupId, adminId) VALUES ({i.id}, {configs['hostQQ']})" 158 | await execute_sql(sql) 159 | 160 | 161 | async def get_admin(group_id: int) -> list: 162 | sql = f"SELECT adminId from admin WHERE groupId={group_id}" 163 | data = await execute_sql(sql) 164 | admins = list(chain.from_iterable(data)) 165 | return admins 166 | 167 | 168 | async def update_setting(group_id: int, setting_name: str, new_setting_value) -> None: 169 | """ 170 | Update setting to database 171 | 172 | Args: 173 | group_id: Group id 174 | setting_name: Setting name 175 | new_setting_value: New setting value 176 | 177 | Examples: 178 | await update_setting(12345678, "setu", True) 179 | 180 | Return: 181 | None 182 | """ 183 | sql = f"UPDATE setting SET {setting_name}={new_setting_value} WHERE groupId={group_id}" 184 | await execute_sql(sql) 185 | 186 | 187 | async def admin_management(group_id: int, member_id: int, operation: str) -> MessageChain: 188 | """ 189 | Update setting to database 190 | 191 | Args: 192 | group_id: Group id 193 | member_id: Member id 194 | operation: add/delete 195 | 196 | Examples: 197 | await admin_manage(12345678, 12345678, "delete") 198 | 199 | Return: 200 | None 201 | """ 202 | sql = f"SELECT * FROM admin WHERE groupId={group_id} and adminId={member_id}" 203 | exist = True if await execute_sql(sql) else False 204 | if operation == "add": 205 | if exist: 206 | return MessageChain.create([Plain(text=f"{member_id}已经是群{group_id}的管理员啦!")]) 207 | else: 208 | sql = f"INSERT INTO admin (groupId, adminId) VALUES ({group_id}, {member_id})" 209 | await execute_sql(sql) 210 | return MessageChain.create([Plain(text=f"{member_id}被设置为群{group_id}的管理员啦!")]) 211 | elif operation == "delete": 212 | if exist: 213 | sql = f"DELETE FROM admin WHERE groupId={group_id} AND adminId={member_id}" 214 | await execute_sql(sql) 215 | return MessageChain.create([Plain(text=f"{member_id}现在不是群{group_id}的管理员啦!")]) 216 | else: 217 | return MessageChain.create([Plain(text=f"{member_id}本来就不是群{group_id}的管理员哦!")]) 218 | else: 219 | return MessageChain.create([Plain(text=f"operation error: {operation}")]) 220 | 221 | 222 | async def add_group(group_id: int): 223 | sql = f"SELECT * FROM setting WHERE groupId={group_id}" 224 | if await execute_sql(sql): 225 | return None 226 | else: 227 | sql = f"INSERT INTO setting (groupId) VALUES ({group_id})" 228 | await execute_sql(sql) 229 | sql = f"INSERT INTO admin (groupId, adminId) VALUES ({group_id}, {configs['hostQQ']})" 230 | await execute_sql(sql) 231 | -------------------------------------------------------------------------------- /modules/KeywordDetection/DFA.py: -------------------------------------------------------------------------------- 1 | from .Sqlite3Manager import Sqlite3Manager 2 | 3 | 4 | class DFAUtils: 5 | __filter_words_dict = {} 6 | __skip_char = [' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》'] 7 | 8 | def __init__(self): 9 | keywords = self.__get_words() 10 | for keyword in keywords: 11 | self.add_keyword(keyword[0]) 12 | # print(self.__filter_words_dict) 13 | 14 | def filter_judge(self, text: str) -> list: 15 | words = [] 16 | for i in range(len(text)): 17 | length = self.check(text, i) 18 | if length > 0: 19 | words.append(text[i:i + length]) 20 | return words 21 | 22 | def check(self, text: str, begin_index: int) -> int: 23 | flag = False 24 | match_flag_length = 0 25 | node = self.__filter_words_dict 26 | tmp_flag = 0 27 | for i in range(begin_index, len(text)): 28 | char = text[i] 29 | if char in self.__skip_char: 30 | tmp_flag += 1 31 | continue 32 | node = node.get(char) 33 | if node: 34 | match_flag_length += 1 35 | tmp_flag += 1 36 | if node.get("is_end"): 37 | flag = True 38 | else: 39 | break 40 | if tmp_flag < 2 or not flag: 41 | tmp_flag = 0 42 | return tmp_flag 43 | 44 | def add_keyword(self, keyword: str): 45 | node = self.__filter_words_dict 46 | for i in range(len(keyword)): 47 | char = keyword[i] 48 | if char in node.keys(): 49 | node = node.get(char) 50 | node["is_end"] = False 51 | else: 52 | node[char] = {"is_end": True if i == len(keyword) - 1 else False} 53 | node = node[char] 54 | 55 | def replace_filter_word(self, text: str) -> str: 56 | filter_words = self.filter_judge(text) 57 | for word in filter_words: 58 | text = text.replace(word, "*" * len(word)) 59 | return text 60 | 61 | def __get_words(self) -> tuple: 62 | conn = Sqlite3Manager.get_instance().get_conn() 63 | cursor = conn.cursor() 64 | sql = f"SELECT keyword FROM keywords" 65 | cursor.execute(sql) 66 | result = cursor.fetchall() 67 | cursor.close() 68 | return result 69 | -------------------------------------------------------------------------------- /modules/KeywordDetection/Sqlite3Manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | 4 | 5 | class Sqlite3Manager: 6 | __instance = None 7 | __first_init: bool = False 8 | path: str = None 9 | __conn = None 10 | 11 | def __new__(cls): 12 | if not cls.__instance: 13 | cls.__instance = object.__new__(cls) 14 | return cls.__instance 15 | 16 | def __init__(self): 17 | if not self.__first_init: 18 | self.path = os.getcwd() 19 | self.__conn = sqlite3.connect(self.path + './modules/KeywordDetection/keywordDetection.db') 20 | cur = self.__conn.cursor() 21 | cur.execute( 22 | """CREATE TABLE IF NOT EXISTS `keywords` ( 23 | `keyword` TEXT PRIMARY KEY 24 | )""" 25 | ) 26 | cur.execute( 27 | """CREATE TABLE IF NOT EXISTS `setting` ( 28 | `groupId` INT PRIMARY KEY, 29 | `switch` INT NOT NULL DEFAULT 0 30 | )""" 31 | ) 32 | self.__conn.commit() 33 | Sqlite3Manager.__first_init = True 34 | else: 35 | raise ValueError("Sqlite3Manager already initialized!") 36 | 37 | @classmethod 38 | def get_instance(cls): 39 | if cls.__instance: 40 | return cls.__instance 41 | else: 42 | raise ValueError("Sqlite3Manager not initialized!") 43 | 44 | @classmethod 45 | def get_connection(cls): 46 | if cls.__conn: 47 | return cls.__conn 48 | else: 49 | raise ValueError("Sqlite3Manager not initialized!") 50 | 51 | def get_conn(self): 52 | if self.__conn: 53 | return self.__conn 54 | else: 55 | raise ValueError("Sqlite3Manager not initialized!") 56 | 57 | 58 | manager = Sqlite3Manager() -------------------------------------------------------------------------------- /modules/KeywordDetection/__init__.py: -------------------------------------------------------------------------------- 1 | from aiohttp.client_exceptions import ClientResponseError 2 | 3 | from graia.application.message.elements.internal import Plain 4 | from graia.application import GraiaMiraiApplication 5 | from graia.saya import Saya, Channel 6 | from graia.saya.builtins.broadcast.schema import ListenerSchema 7 | from graia.application.event.messages import * 8 | from graia.application.exceptions import AccountMuted 9 | from graia.application.message.parser.kanata import Kanata 10 | from graia.application.message.parser.signature import RegexMatch 11 | 12 | from .DFA import DFAUtils 13 | from .utils import * 14 | 15 | # 插件信息 16 | __name__ = "KeywordDetection" 17 | __description__ = "一个敏感词检测插件" 18 | __author__ = "SAGIRI-kawaii" 19 | __usage__ = "控制插件开关:打开/关闭敏感词过滤" 20 | 21 | saya = Saya.current() 22 | channel = Channel.current() 23 | 24 | channel.name(__name__) 25 | channel.description(f"{__description__}\n使用方法:{__usage__}") 26 | channel.author(__author__) 27 | 28 | DFA = DFAUtils() 29 | HostQQ = 1900384123 30 | 31 | 32 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 33 | async def keyword_detection( 34 | app: GraiaMiraiApplication, 35 | message: MessageChain, 36 | group: Group 37 | ): 38 | message_text = message.asDisplay() 39 | if await get_group_switch(group.id): 40 | if DFA.filter_judge(message_text): 41 | try: 42 | try: 43 | await app.revokeMessage(message[Source][0]) 44 | await app.sendGroupMessage( 45 | group, 46 | MessageChain.create([ 47 | Plain(text="检测到敏感词,自动撤回\n"), 48 | Plain(text="过滤后:\n"), 49 | Plain(text=DFA.replace_filter_word(message_text)) 50 | ])) 51 | except (ClientResponseError, PermissionError): 52 | await app.sendGroupMessage( 53 | group, 54 | MessageChain.create([ 55 | Plain(text="检测到敏感词,发生错误: 无权限"), 56 | Plain(text="\n过滤后:\n"), 57 | Plain(text=DFA.replace_filter_word(message_text)) 58 | ]), 59 | quote=message[Source][0] 60 | ) 61 | except AccountMuted: 62 | pass 63 | 64 | 65 | @channel.use(ListenerSchema( 66 | listening_events=[GroupMessage], 67 | inline_dispatchers=[Kanata([RegexMatch("(打开|关闭)敏感词过滤")])] 68 | )) 69 | async def switch_modify( 70 | app: GraiaMiraiApplication, 71 | message: MessageChain, 72 | group: Group, 73 | member: Member 74 | ): 75 | if member.id == HostQQ: 76 | switch = 1 if message.asDisplay()[:2] == "开启" else 0 77 | await set_group_switch(group.id, switch) 78 | try: 79 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=f"敏感词过滤已{message.asDisplay()[:2]}")])) 80 | except AccountMuted: 81 | pass 82 | else: 83 | try: 84 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="你没有权限,爬!")])) 85 | except AccountMuted: 86 | pass 87 | -------------------------------------------------------------------------------- /modules/KeywordDetection/keywordDetection.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KeywordDetection/keywordDetection.db -------------------------------------------------------------------------------- /modules/KeywordDetection/utils.py: -------------------------------------------------------------------------------- 1 | from .Sqlite3Manager import Sqlite3Manager 2 | 3 | 4 | async def get_group_switch(group_id: int) -> bool: 5 | conn = Sqlite3Manager.get_instance().get_conn() 6 | sql = f"SELECT switch FROM setting WHERE groupId={group_id}" 7 | print(sql) 8 | cursor = conn.cursor() 9 | cursor.execute(sql) 10 | if result := cursor.fetchall(): 11 | cursor.close() 12 | print(result[0][0]) 13 | return True if result[0][0] else False 14 | sql = f"INSERT INTO setting (groupId, switch) VALUES (?, ?)" 15 | cursor.execute(sql, (group_id, 1)) 16 | conn.commit() 17 | cursor.close() 18 | return True 19 | 20 | 21 | async def set_group_switch(group_id: int, new_status: int) -> None: 22 | conn = Sqlite3Manager.get_instance().get_conn() 23 | cursor = conn.cursor() 24 | sql = f"UPDATE setting SET switch={new_status} WHERE groupId={group_id}" 25 | cursor.execute(sql) 26 | conn.commit() 27 | cursor.close() 28 | -------------------------------------------------------------------------------- /modules/KeywordReply/DFA.py: -------------------------------------------------------------------------------- 1 | from .Sqlite3Manager import Sqlite3Manager 2 | 3 | 4 | class DFAUtils: 5 | __filter_words_dict = {} 6 | __skip_char = [' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》', '-', '_'] 7 | 8 | def __init__(self): 9 | keywords = self.__get_words() 10 | for keyword in keywords: 11 | self.add_keyword(keyword[0]) 12 | # print(self.__filter_words_dict) 13 | 14 | def filter_judge(self, text: str) -> list: 15 | words = [] 16 | for i in range(len(text)): 17 | length = self.check(text, i) 18 | if length > 0: 19 | words.append(text[i:i + length]) 20 | return words 21 | 22 | def check(self, text: str, begin_index: int) -> int: 23 | flag = False 24 | match_flag_length = 0 25 | node = self.__filter_words_dict 26 | tmp_flag = 0 27 | for i in range(begin_index, len(text)): 28 | char = text[i] 29 | if char in self.__skip_char: 30 | tmp_flag += 1 31 | continue 32 | node = node.get(char) 33 | if node: 34 | match_flag_length += 1 35 | tmp_flag += 1 36 | if node.get("is_end"): 37 | flag = True 38 | else: 39 | break 40 | if tmp_flag < 2 or not flag: 41 | tmp_flag = 0 42 | return tmp_flag 43 | 44 | def add_keyword(self, keyword: str): 45 | node = self.__filter_words_dict 46 | for i in range(len(keyword)): 47 | char = keyword[i] 48 | if char in node.keys(): 49 | node = node.get(char) 50 | node["is_end"] = False 51 | else: 52 | node[char] = {"is_end": True if i == len(keyword) - 1 else False} 53 | node = node[char] 54 | 55 | def replace_filter_word(self, text: str) -> str: 56 | filter_words = self.filter_judge(text) 57 | for word in filter_words: 58 | text = text.replace(word, "*" * len(word)) 59 | return text 60 | 61 | def __get_words(self) -> tuple: 62 | conn = Sqlite3Manager.get_instance().get_conn() 63 | cursor = conn.cursor() 64 | sql = f"SELECT keyword FROM keywords" 65 | cursor.execute(sql) 66 | result = cursor.fetchall() 67 | cursor.close() 68 | return result 69 | -------------------------------------------------------------------------------- /modules/KeywordReply/Sqlite3Manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | 4 | 5 | class Sqlite3Manager: 6 | __instance = None 7 | __first_init: bool = False 8 | path: str = None 9 | __conn = None 10 | 11 | def __new__(cls): 12 | if not cls.__instance: 13 | cls.__instance = object.__new__(cls) 14 | return cls.__instance 15 | 16 | def __init__(self): 17 | if not self.__first_init: 18 | self.path = os.getcwd() 19 | self.__conn = sqlite3.connect(self.path + './modules/KeywordReply/keywordReply.db') 20 | cur = self.__conn.cursor() 21 | cur.execute( 22 | """CREATE TABLE IF NOT EXISTS `keywordReply` ( 23 | `keyword` TEXT NOT NULL, 24 | `type` TEXT NOT NULL, 25 | `content` LONGBLOB NOT NULL 26 | )""" 27 | ) 28 | cur.execute( 29 | """CREATE TABLE IF NOT EXISTS `filterKeywords` ( 30 | `keyword` TEXT PRIMARY KEY 31 | )""" 32 | ) 33 | self.__conn.commit() 34 | 35 | Sqlite3Manager.__first_init = True 36 | else: 37 | raise ValueError("Sqlite3Manager already initialized!") 38 | 39 | @classmethod 40 | def get_instance(cls): 41 | if cls.__instance: 42 | return cls.__instance 43 | else: 44 | raise ValueError("Sqlite3Manager not initialized!") 45 | 46 | @classmethod 47 | def get_connection(cls): 48 | if cls.__conn: 49 | return cls.__conn 50 | else: 51 | raise ValueError("Sqlite3Manager not initialized!") 52 | 53 | def get_conn(self): 54 | if self.__conn: 55 | return self.__conn 56 | else: 57 | raise ValueError("Sqlite3Manager not initialized!") 58 | 59 | def execute(self, sql: str): 60 | if sql.lower().startswith("select"): 61 | cur = self.__conn.cursor() 62 | cur.execute(sql) 63 | result = cur.fetchall() 64 | cur.close() 65 | return result 66 | else: 67 | cur = self.__conn.cursor() 68 | cur.execute(sql) 69 | self.__conn.commit() 70 | cur.close() 71 | return 72 | 73 | 74 | manager = Sqlite3Manager() 75 | 76 | 77 | async def execute_sql(sql: str): 78 | instance = Sqlite3Manager.get_instance() 79 | return instance.execute(sql) 80 | -------------------------------------------------------------------------------- /modules/KeywordReply/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | import base64 3 | import aiohttp 4 | import re 5 | 6 | from graia.application.message.elements.internal import Plain 7 | from graia.application.message.elements.internal import Image 8 | from graia.application import GraiaMiraiApplication 9 | from graia.saya import Saya, Channel 10 | from graia.saya.builtins.broadcast.schema import ListenerSchema 11 | from graia.application.event.messages import * 12 | from graia.application.exceptions import AccountMuted 13 | from graia.broadcast.interrupt import InterruptControl 14 | from graia.broadcast.interrupt.waiter import Waiter 15 | 16 | from .Sqlite3Manager import execute_sql 17 | 18 | # 插件信息 19 | __name__ = "KeywordReply" 20 | __description__ = "一个关键词回复插件" 21 | __author__ = "SAGIRI-kawaii" 22 | __usage__ = "发送关键词即可,若要设置关键词则发送 添加关键词#关键词/图片#回复文本/图片 即可" 23 | 24 | saya = Saya.current() 25 | channel = Channel.current() 26 | bcc = saya.broadcast 27 | inc = InterruptControl(bcc) 28 | 29 | channel.name(__name__) 30 | channel.description(f"{__description__}\n使用方法:{__usage__}") 31 | channel.author(__author__) 32 | 33 | 34 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 35 | async def keyword_reply( 36 | app: GraiaMiraiApplication, 37 | message: MessageChain, 38 | group: Group 39 | ): 40 | message_serialization = message.asSerializationString() 41 | message_serialization = message_serialization.replace( 42 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 43 | "" 44 | ) 45 | if re.match(r"\[mirai:image:{.*}\..*]", message_serialization): 46 | message_serialization = re.findall(r"\[mirai:image:{(.*?)}\..*]", message_serialization, re.S)[0] 47 | sql = f"SELECT * FROM keywordReply WHERE keyword='{message_serialization}'" 48 | if result := await execute_sql(sql): 49 | replies = [] 50 | for i in range(len(result)): 51 | content_type = result[i][1] 52 | content = result[i][2] 53 | replies.append([content_type, content]) 54 | # print(replies) 55 | final_reply = random.choice(replies) 56 | 57 | content_type = final_reply[0] 58 | content = final_reply[1] 59 | try: 60 | if content_type == "img": 61 | await app.sendGroupMessage(group, MessageChain.create([Image.fromUnsafeBytes(base64.b64decode(content))])) 62 | elif content_type == "text": 63 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=content)])) 64 | else: 65 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=f"unknown content_type:{content_type}")])) 66 | except AccountMuted: 67 | pass 68 | 69 | 70 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 71 | async def add_keyword( 72 | app: GraiaMiraiApplication, 73 | message: MessageChain, 74 | member: Member, 75 | group: Group 76 | ): 77 | message_serialization = message.asSerializationString() 78 | message_serialization = message_serialization.replace( 79 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 80 | "" 81 | ) 82 | if re.match(r"添加关键词#[\s\S]*#[\s\S]*", message_serialization): 83 | try: 84 | _, keyword, content = message_serialization.split("#") 85 | except ValueError: 86 | await app.sendGroupMessage( 87 | group, 88 | MessageChain.create([ 89 | Plain(text="设置格式:\n添加关键词#关键词/图片#回复文本/图片\n"), 90 | Plain(text="注:目前不支持文本中含有#!") 91 | ]) 92 | ) 93 | return None 94 | keyword = keyword.strip() 95 | content = content.strip() 96 | if keyword == "" or content == "": 97 | try: 98 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="怎么是空的啊!爬!")])) 99 | except AccountMuted: 100 | pass 101 | return None 102 | content_type = "text" 103 | keyword_type = "text" 104 | if re.match(r"\[mirai:image:{.*}\..*]", keyword): 105 | keyword = re.findall(r"\[mirai:image:{(.*?)}\..*]", keyword, re.S)[0] 106 | keyword_type = "img" 107 | if re.match(r"\[mirai:image:{.*}\..*]", content): 108 | content_type = "img" 109 | image: Image = message[Image][0] if keyword_type == "text" else message[Image][1] 110 | async with aiohttp.ClientSession() as session: 111 | async with session.get(url=image.url) as resp: 112 | content = await resp.read() 113 | content = base64.b64encode(content) 114 | 115 | conn = Sqlite3Manager.Sqlite3Manager.get_instance().get_conn() 116 | cursor = conn.cursor() 117 | sql = f"SELECT * FROM keywordReply WHERE keyword=? AND `type`=? AND `content`=?" 118 | cursor.execute(sql, (keyword, content_type, content)) 119 | if cursor.fetchall(): 120 | try: 121 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="存在相同数据!进程退出")])) 122 | cursor.close() 123 | return None 124 | except AccountMuted: 125 | cursor.close() 126 | return None 127 | sql = f"INSERT INTO keywordReply (`keyword`, `type`, `content`) VALUES (?,?,?)" 128 | cursor.execute(sql, (keyword, content_type, content)) 129 | 130 | conn.commit() 131 | cursor.close() 132 | try: 133 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="添加成功!")])) 134 | except AccountMuted: 135 | pass 136 | elif re.match(r"删除关键词#[\s\S]*", message_serialization): 137 | try: 138 | _, keyword = message_serialization.split("#") 139 | except ValueError: 140 | await app.sendGroupMessage( 141 | group, 142 | MessageChain.create([ 143 | Plain(text="设置格式:\n添加关键词#关键词/图片#回复文本/图片\n"), 144 | Plain(text="注:目前不支持文本中含有#!") 145 | ]) 146 | ) 147 | return None 148 | keyword = keyword.strip() 149 | 150 | if re.match(r"\[mirai:image:{.*}\..*]", keyword): 151 | keyword = re.findall(r"\[mirai:image:{(.*?)}\..*]", keyword, re.S)[0] 152 | 153 | sql = f"SELECT * FROM keywordReply WHERE keyword='{keyword}'" 154 | if result := await execute_sql(sql): 155 | replies = [] 156 | for i in range(len(result)): 157 | content_type = result[i][1] 158 | content = result[i][2] 159 | replies.append([content_type, content]) 160 | msg = [Plain(text=f"关键词{keyword}目前有以下数据:\n")] 161 | for i in range(len(replies)): 162 | msg.append(Plain(text=f"{i + 1}. ")) 163 | msg.append(Plain(text=replies[i][1]) if replies[i][0] == "text" else Image.fromUnsafeBytes(base64.b64decode(replies[i][1]))) 164 | msg.append(Plain(text="\n")) 165 | msg.append(Plain(text="请发送你要删除的回复编号")) 166 | 167 | try: 168 | await app.sendGroupMessage(group, MessageChain.create(msg)) 169 | except AccountMuted: 170 | return None 171 | 172 | number = 0 173 | 174 | @Waiter.create_using_function([GroupMessage]) 175 | def number_waiter( 176 | event: GroupMessage, waiter_group: Group, 177 | waiter_member: Member, waiter_message: MessageChain 178 | ): 179 | nonlocal number 180 | if all([ 181 | waiter_group.id == group.id, 182 | waiter_member.id == member.id, 183 | waiter_message.asDisplay().isnumeric() and 0 < int(waiter_message.asDisplay()) <= len(replies) 184 | ]): 185 | number = int(waiter_message.asDisplay()) 186 | return event 187 | elif all([ 188 | waiter_group.id == group.id, 189 | waiter_member.id == member.id 190 | ]): 191 | number = None 192 | return event 193 | 194 | await inc.wait(number_waiter) 195 | 196 | if number is None: 197 | try: 198 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="非预期回复,进程退出")])) 199 | except AccountMuted: 200 | pass 201 | elif 1 <= number <= len(replies): 202 | try: 203 | await app.sendGroupMessage( 204 | group, 205 | MessageChain.create([ 206 | Plain(text="你确定要删除下列回复吗(是/否):\n"), 207 | Plain(text=keyword), 208 | Plain(text="\n->\n"), 209 | Plain(text=replies[number - 1][1]) if replies[number - 1][0] == "text" else Image.fromUnsafeBytes(base64.b64decode(replies[number - 1][1])) 210 | ]) 211 | ) 212 | except AccountMuted: 213 | return None 214 | 215 | result = "否" 216 | 217 | @Waiter.create_using_function([GroupMessage]) 218 | def confirm_waiter( 219 | event: GroupMessage, waiter_group: Group, 220 | waiter_member: Member, waiter_message: MessageChain 221 | ): 222 | nonlocal result 223 | if all([ 224 | waiter_group.id == group.id, 225 | waiter_member.id == member.id 226 | ]): 227 | if re.match(r"[是否]", waiter_message.asDisplay()): 228 | result = waiter_message.asDisplay() 229 | return event 230 | 231 | await inc.wait(confirm_waiter) 232 | 233 | if result == "是": 234 | sql = f"DELETE FROM keywordReply WHERE keyword=? AND `type`=? AND `content`=?" 235 | conn = Sqlite3Manager.Sqlite3Manager.get_instance().get_conn() 236 | cursor = conn.cursor() 237 | cursor.execute(sql, (keyword, replies[number - 1][0], replies[number - 1][1])) 238 | conn.commit() 239 | cursor.close() 240 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="删除成功!")])) 241 | 242 | else: 243 | try: 244 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="进程退出")])) 245 | except AccountMuted: 246 | pass 247 | else: 248 | try: 249 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="进程退出")])) 250 | except AccountMuted: 251 | pass 252 | 253 | else: 254 | try: 255 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="未检测到此关键词数据!")])) 256 | except AccountMuted: 257 | pass 258 | -------------------------------------------------------------------------------- /modules/KeywordReply/keywordAppender.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from .Sqlite3Manager import execute_sql 3 | 4 | FILE_PATH = "" 5 | 6 | 7 | class KeywordAppender: 8 | """ 9 | 注意文件格式应为用 \n 分隔的关键词 10 | 如: 11 | 测试1 12 | 测试2 13 | 测试3 14 | """ 15 | @staticmethod 16 | async def append(file_path: str): 17 | with open(file_path, "r") as r: 18 | keywords = r.read().split("\n") 19 | for keyword in keywords: 20 | sql = f"INSERT INTO filterKeywords (`keyword`) VALUES ('{keyword}')" 21 | await execute_sql(sql) 22 | 23 | 24 | if __name__ == "__main__": 25 | loop = asyncio.get_event_loop() 26 | loop.run_until_complete(KeywordAppender.append(FILE_PATH)) 27 | -------------------------------------------------------------------------------- /modules/KeywordReply/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding=utf-8 -*-z` 2 | from cnocr import CnOcr 3 | from cnstd import CnStd 4 | from PIL import Image as IMG 5 | from io import BytesIO 6 | import aiohttp 7 | import numpy 8 | 9 | from graia.application.message.elements.internal import Image 10 | 11 | from .DFA import DFAUtils 12 | 13 | DFA = DFAUtils() 14 | 15 | 16 | async def word_valid(word: str) -> list: 17 | return DFA.filter_judge(word) 18 | 19 | 20 | async def flat(lst: list) -> list: 21 | result = [] 22 | for i in lst: 23 | if isinstance(i, list): 24 | result += await flat(i) 25 | else: 26 | result.append(i) 27 | return result 28 | 29 | 30 | async def Img2Text(img: Image, ocr_model: CnOcr, std: CnStd) -> str: 31 | url = img.url 32 | async with aiohttp.ClientSession() as session: 33 | async with session.get(url=url) as resp: 34 | img_content = await resp.read() 35 | img = IMG.open(BytesIO(img_content)).convert("RGB") 36 | img = numpy.array(img) 37 | box_info_list = std.detect(img) 38 | res = [] 39 | for box_info in box_info_list: 40 | cropped_img = box_info['cropped_img'] # 检测出的文本框 41 | ocr_res = ocr_model.ocr_for_single_line(cropped_img) 42 | res.append([ocr_res]) 43 | print(res) 44 | return "".join(await flat(res)) 45 | -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/1.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/10.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/11.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/12.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/13.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/2.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/3.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/4.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/5.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/6.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/7.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/8.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/9.png -------------------------------------------------------------------------------- /modules/KissKiss/KissFrames/Kiss.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/KissFrames/Kiss.gif -------------------------------------------------------------------------------- /modules/KissKiss/README.md: -------------------------------------------------------------------------------- 1 | ## KissKiss 2 | 3 | 一个互亲gif生成器 4 | 5 | ## 使用注意 6 | 7 | 请先使用 `pip install -r requirements.txt` 命令安装所需依赖 8 | -------------------------------------------------------------------------------- /modules/KissKiss/__init__.py: -------------------------------------------------------------------------------- 1 | from PIL import Image as IMG 2 | from PIL import ImageOps, ImageDraw 3 | from moviepy.editor import ImageSequenceClip as imageclip 4 | import numpy 5 | import aiohttp 6 | from io import BytesIO 7 | import os 8 | 9 | from graia.application import GraiaMiraiApplication 10 | from graia.saya import Saya, Channel 11 | from graia.saya.builtins.broadcast.schema import ListenerSchema 12 | from graia.application.event.messages import * 13 | from graia.application.message.chain import MessageChain 14 | from graia.application.message.elements.internal import At, Image, Plain 15 | from graia.application.event.messages import Group, Member 16 | from graia.application.exceptions import AccountMuted 17 | 18 | # 插件信息 19 | __name__ = "KissKiss" 20 | __description__ = "生成亲吻gif" 21 | __author__ = "Super_Water_God" 22 | __usage__ = "在群内发送 亲@目标 即可" 23 | 24 | saya = Saya.current() 25 | channel = Channel.current() 26 | 27 | channel.name(__name__) 28 | channel.description(f"{__description__}\n使用方法:{__usage__}") 29 | channel.author(__author__) 30 | 31 | 32 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 33 | async def petpet_generator(app: GraiaMiraiApplication, message: MessageChain, group: Group, member: Member): 34 | if message.has(At) and message.asDisplay().startswith("亲") and message.get(At)[ 35 | 0].target != app.connect_info.account: 36 | if not os.path.exists("./modules/KissKiss/temp"): 37 | os.mkdir("./modules/KissKiss/temp") 38 | AtQQ = message.get(At)[0].target 39 | if member.id == AtQQ: 40 | await app.sendGroupMessage(group, MessageChain.create([Plain("请不要自交~😋")]), quote=message[Source][0]) 41 | else: 42 | SavePic = f"./modules/KissKiss/temp/tempKiss-{member.id}-{AtQQ}.gif" 43 | await kiss(member.id, AtQQ) 44 | await app.sendGroupMessage(group, MessageChain.create([Image.fromLocalFile(SavePic)])) 45 | 46 | 47 | async def save_gif(gif_frames, dest, fps=10): 48 | clip = imageclip(gif_frames, fps=fps) 49 | clip.write_gif(dest) 50 | clip.close() 51 | 52 | 53 | async def kiss_make_frame(operator, target, i): 54 | operator_x = [92, 135, 84, 80, 155, 60, 50, 98, 35, 38, 70, 84, 75] 55 | operator_y = [64, 40, 105, 110, 82, 96, 80, 55, 65, 100, 80, 65, 65] 56 | target_x = [58, 62, 42, 50, 56, 18, 28, 54, 46, 60, 35, 20, 40] 57 | target_y = [90, 95, 100, 100, 100, 120, 110, 100, 100, 100, 115, 120, 96] 58 | bg = IMG.open(f"./modules/KissKiss/KissFrames/{i}.png") 59 | gif_frame = IMG.new('RGB', (200, 200), (255, 255, 255)) 60 | gif_frame.paste(bg, (0, 0)) 61 | gif_frame.paste(target, (target_x[i - 1], target_y[i - 1]), target) 62 | gif_frame.paste(operator, (operator_x[i - 1], operator_y[i - 1]), operator) 63 | return numpy.array(gif_frame) 64 | 65 | 66 | async def kiss(operator_id, target_id) -> None: 67 | operator_url = f'http://q1.qlogo.cn/g?b=qq&nk={str(operator_id)}&s=640' 68 | target_url = f'http://q1.qlogo.cn/g?b=qq&nk={str(target_id)}&s=640' 69 | gif_frames = [] 70 | if str(operator_id) != "": # admin自定义 71 | async with aiohttp.ClientSession() as session: 72 | async with session.get(url=operator_url) as resp: 73 | operator_img = await resp.read() 74 | operator = IMG.open(BytesIO(operator_img)) 75 | else: 76 | operator = IMG.open("./modules/KissKiss/avatar.png") 77 | 78 | if str(target_id) != "": # admin自定义 79 | async with aiohttp.ClientSession() as session: 80 | async with session.get(url=target_url) as resp: 81 | target_img = await resp.read() 82 | target = IMG.open(BytesIO(target_img)) 83 | else: 84 | target = IMG.open("./modules/KissKiss/avatar.png") 85 | 86 | operator = operator.resize((40, 40), IMG.ANTIALIAS) 87 | size = operator.size 88 | r2 = min(size[0], size[1]) 89 | circle = IMG.new('L', (r2, r2), 0) 90 | draw = ImageDraw.Draw(circle) 91 | draw.ellipse((0, 0, r2, r2), fill=255) 92 | alpha = IMG.new('L', (r2, r2), 255) 93 | alpha.paste(circle, (0, 0)) 94 | operator.putalpha(alpha) 95 | 96 | target = target.resize((50, 50), IMG.ANTIALIAS) 97 | size = target.size 98 | r2 = min(size[0], size[1]) 99 | circle = IMG.new('L', (r2, r2), 0) 100 | draw = ImageDraw.Draw(circle) 101 | draw.ellipse((0, 0, r2, r2), fill=255) 102 | alpha = IMG.new('L', (r2, r2), 255) 103 | alpha.paste(circle, (0, 0)) 104 | target.putalpha(alpha) 105 | 106 | for i in range(1, 14): 107 | gif_frames.append(await kiss_make_frame(operator, target, i)) 108 | await save_gif(gif_frames, f'./modules/KissKiss/temp/tempKiss-{operator_id}-{target_id}.gif', fps=25) 109 | -------------------------------------------------------------------------------- /modules/KissKiss/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/KissKiss/avatar.png -------------------------------------------------------------------------------- /modules/KissKiss/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | moviepy 3 | aiohttp 4 | Pillow 5 | -------------------------------------------------------------------------------- /modules/LeetcodeInfoCrawer/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.application.message.chain import MessageChain 2 | from graia.saya import Saya, Channel 3 | from graia.saya.builtins.broadcast.schema import ListenerSchema 4 | from graia.application.exceptions import AccountMuted 5 | from graia.application.message.elements.internal import Plain 6 | from graia.application.message.elements.internal import Image 7 | from graia.application import GraiaMiraiApplication 8 | from graia.application.event.messages import GroupMessage, Group 9 | from graia.application.message.parser.kanata import Kanata 10 | from graia.application.message.parser.signature import RegexMatch 11 | from graia.application.message.parser.signature import FullMatch 12 | 13 | from .leetcode_user_info_crawer import get_leetcode_user_statics 14 | from .leetcode_daily_question_crawer import get_leetcode_daily_question 15 | 16 | # 插件信息 17 | __name__ = "LeetcodeInfoCrawer" 18 | __description__ = "一个可以获取leetcode信息的插件" 19 | __author__ = "SAGIRI-kawaii" 20 | __usage__ = "查询用户信息:发送 leetcode userSlug (userSlug为个人唯一标识 个人主页地址 -> https://leetcode-cn.com/u/userSlug/)" \ 21 | "\n查询每日一题:发送 leetcode每日一题 即可" 22 | 23 | saya = Saya.current() 24 | channel = Channel.current() 25 | 26 | channel.name(__name__) 27 | channel.description(f"{__description__}\n使用方法:{__usage__}") 28 | channel.author(__author__) 29 | 30 | 31 | @channel.use(ListenerSchema( 32 | listening_events=[GroupMessage], 33 | inline_dispatchers=[Kanata([RegexMatch("leetcode .*")])] 34 | )) 35 | async def leetcode_user_info_crawer(app: GraiaMiraiApplication, message: MessageChain, group: Group): 36 | try: 37 | if userSlug := message.asDisplay()[9:]: 38 | await app.sendGroupMessage(group, await get_leetcode_user_statics(userSlug)) 39 | else: 40 | await app.sendGroupMessage( 41 | group, 42 | MessageChain.create([ 43 | Plain(text="请输入userSlug!\nuserSlug为个人主页地址的标识(https://leetcode-cn.com/u/userSlug/)") 44 | ]) 45 | ) 46 | except AccountMuted: 47 | pass 48 | 49 | 50 | @channel.use(ListenerSchema( 51 | listening_events=[GroupMessage], 52 | inline_dispatchers=[Kanata([RegexMatch("leetcode每日一题.*")])] 53 | )) 54 | async def leetcode_daily_question(app: GraiaMiraiApplication, message: MessageChain, group: Group): 55 | try: 56 | await app.sendGroupMessage(group, await get_leetcode_daily_question()) 57 | except AccountMuted: 58 | pass 59 | -------------------------------------------------------------------------------- /modules/LeetcodeInfoCrawer/leetcode_daily_question_crawer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | from html import unescape 5 | from PIL import Image as IMG 6 | from io import BytesIO 7 | 8 | from graia.application.message.elements.internal import MessageChain 9 | from graia.application.message.elements.internal import Plain 10 | from graia.application.message.elements.internal import Image 11 | 12 | from utils import messagechain_to_img 13 | 14 | 15 | async def get_leetcode_daily_question(language: str = "Zh") -> MessageChain: 16 | questionSlugData = await get_daily_question_json() 17 | questionSlug = questionSlugData["data"]["todayRecord"][0]["question"]["questionTitleSlug"] 18 | content = await get_question_content(questionSlug, language) 19 | content = await image_in_html2text(content) 20 | msg_list = [] 21 | count = 0 22 | for i in content: 23 | if i.startswith("img["): 24 | # print(i.replace("img[" + re.findall(r'img\[(.*?)]', i, re.S)[0] + "]:", "")) 25 | async with aiohttp.ClientSession() as session: 26 | async with session.get( 27 | url=i.replace("img[" + re.findall(r'img\[(.*?)]', i, re.S)[0] + "]:", ""), 28 | headers={"accept-encoding": "gzip, deflate, br"} 29 | ) as resp: 30 | img_content = await resp.read() 31 | image = IMG.open(BytesIO(img_content)) 32 | print(f"./modules/LeetcodeInfoCrawer/temp/tempQuestion{count}.jpg") 33 | image.save(f"./modules/LeetcodeInfoCrawer/temp/tempQuestion{count}.jpg") 34 | msg_list.append(Image.fromLocalFile(f"./modules/LeetcodeInfoCrawer/temp/tempQuestion{count}.jpg")) 35 | count += 1 36 | else: 37 | msg_list.append(Plain(text=i)) 38 | print(msg_list) 39 | return await messagechain_to_img(MessageChain.create(msg_list)) 40 | 41 | 42 | async def get_daily_question_json(): 43 | url = "https://leetcode-cn.com/graphql/" 44 | headers = { 45 | "content-type": "application/json", 46 | "origin": "https://leetcode-cn.com", 47 | "referer": "https://leetcode-cn.com/problemset/all/", 48 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) " 49 | "Chrome/84.0.4147.135 Safari/537.36 " 50 | } 51 | payload = { 52 | "operationName": "questionOfToday", 53 | "variables": {}, 54 | "query": "query questionOfToday {\n todayRecord {\n question {\n questionFrontendId," 55 | "\n questionTitleSlug,\n __typename\n }\n lastSubmission {\n id," 56 | "\n __typename,\n }\n date,\n userStatus,\n __typename\n }\n}\n " 57 | } 58 | async with aiohttp.ClientSession() as session: 59 | async with session.post(url=url, headers=headers, data=json.dumps(payload)) as resp: 60 | result = await resp.json() 61 | return result 62 | 63 | 64 | async def get_question_content(questionTitleSlug, language="Zh"): 65 | url = "https://leetcode-cn.com/graphql/" 66 | headers = { 67 | "accept": "*/*", 68 | "accept-encoding": "gzip, deflate, br", 69 | "accept-language": "zh-CN,zh;q=0.9", 70 | "content-type": "application/json", 71 | "origin": "https://leetcode-cn.com", 72 | "referer": "https://leetcode-cn.com/problems/%s/" % questionTitleSlug, 73 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) " 74 | "Chrome/84.0.4147.135 Safari/537.36", 75 | "x-definition-name": "question", 76 | "x-operation-name": "questionData", 77 | "x-timezone": "Asia/Shanghai" 78 | } 79 | payload = { 80 | "operationName": "questionData", 81 | "variables": {"titleSlug": "%s" % questionTitleSlug}, 82 | "query": "query questionData($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n questionId\n " 83 | "questionFrontendId\n boundTopicId\n title\n titleSlug\n content\n translatedTitle\n " 84 | " translatedContent\n isPaidOnly\n difficulty\n likes\n dislikes\n isLiked\n " 85 | "similarQuestions\n contributors {\n username\n profileUrl\n avatarUrl\n " 86 | "__typename\n }\n langToValidPlayground\n topicTags {\n name\n slug\n " 87 | "translatedName\n __typename\n }\n companyTagStats\n codeSnippets {\n lang\n " 88 | "langSlug\n code\n __typename\n }\n stats\n hints\n solution {\n id\n " 89 | " canSeeDetail\n __typename\n }\n status\n sampleTestCase\n metaData\n " 90 | "judgerAvailable\n judgeType\n mysqlSchemas\n enableRunCode\n envInfo\n book {\n " 91 | "id\n bookName\n pressName\n source\n shortDescription\n fullDescription\n " 92 | " bookImgUrl\n pressImgUrl\n productUrl\n __typename\n }\n isSubscribed\n " 93 | "isDailyQuestion\n dailyRecordStatus\n editorType\n ugcQuestionId\n style\n " 94 | "__typename\n }\n}\n " 95 | } 96 | async with aiohttp.ClientSession() as session: 97 | async with session.post(url=url, headers=headers, data=json.dumps(payload)) as resp: 98 | result = await resp.json() 99 | if language == "En": 100 | return result["data"]["question"]["content"] 101 | elif language == "Zh": 102 | return result["data"]["question"]["translatedContent"] 103 | else: 104 | return None 105 | 106 | 107 | async def html2plain_text(html): 108 | text = re.sub('