├── .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('.*?', '', html, flags=re.M | re.S | re.I) 109 | text = re.sub('', ' HYPERLINK ', text, flags=re.M | re.S | re.I) 110 | text = re.sub('<.*?>', '', text, flags=re.M | re.S) 111 | text = re.sub(r'(\s*\n)+', '\n', text, flags=re.M | re.S) 112 | return unescape(text) 113 | 114 | 115 | async def image_in_html2text(content) -> list: 116 | images = re.findall(r'', content, re.S) 117 | for i in range(len(images)): 118 | content = content.replace(images[i], "/>ImAgEiMaGe%dImAgE MessageChain: 9 | url = "https://leetcode-cn.com/graphql/" 10 | headers = { 11 | "origin": "https://leetcode-cn.com", 12 | "referer": "https://leetcode-cn.com/u/%s/" % account_name, 13 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 14 | "Chrome/80.0.3987.100 Safari/537.36", 15 | "x-definition-name": "userProfilePublicProfile", 16 | "x-operation-name": "userPublicProfile", 17 | "content-type": "application/json" 18 | } 19 | payload = { 20 | 'operationName': "userPublicProfile", 21 | "query": "query userPublicProfile($userSlug: String!) {\n userProfilePublicProfile(userSlug: $userSlug) {\n " 22 | " username,\n haveFollowed,\n siteRanking,\n profile {\n userSlug,\n realName," 23 | "\n aboutMe,\n userAvatar,\n location,\n gender,\n websites," 24 | "\n skillTags,\n contestCount,\n asciiCode,\n medals {\n name," 25 | "\n year,\n month,\n category,\n __typename,\n }\n ranking {\n " 26 | " rating,\n ranking,\n currentLocalRanking,\n currentGlobalRanking," 27 | "\n currentRating,\n ratingProgress,\n totalLocalUsers,\n " 28 | "totalGlobalUsers,\n __typename,\n }\n skillSet {\n langLevels {\n " 29 | "langName,\n langVerboseName,\n level,\n __typename,\n }\n " 30 | "topics {\n slug,\n name,\n translatedName,\n __typename," 31 | "\n }\n topicAreaScores {\n score,\n topicArea {\n name," 32 | "\n slug,\n __typename,\n }\n __typename,\n }\n " 33 | " __typename,\n }\n socialAccounts {\n provider,\n profileUrl," 34 | "\n __typename,\n }\n __typename,\n }\n educationRecordList {\n " 35 | "unverifiedOrganizationName,\n __typename,\n }\n occupationRecordList {\n " 36 | "unverifiedOrganizationName,\n jobTitle,\n __typename,\n }\n submissionProgress {\n " 37 | " totalSubmissions,\n waSubmissions,\n acSubmissions,\n reSubmissions," 38 | "\n otherSubmissions,\n acTotal,\n questionTotal,\n __typename\n }\n " 39 | "__typename\n }\n}", 40 | 'variables': '{"userSlug": "%s"}' % account_name 41 | } 42 | 43 | async with aiohttp.ClientSession() as session: 44 | async with session.post(url=url, headers=headers, data=json.dumps(payload)) as resp: 45 | data_json = await resp.json() 46 | 47 | if 'userProfilePublicProfile' in data_json["data"].keys() and data_json["data"]['userProfilePublicProfile'] is None: 48 | return MessageChain.create([Plain(text="未找到 userSlug: %s!" % account_name)]) 49 | data_json = data_json['data']['userProfilePublicProfile'] 50 | profile = data_json['profile'] 51 | 52 | user_slug = profile['userSlug'] 53 | 54 | user_name = profile['realName'] 55 | 56 | ranking = data_json['siteRanking'] 57 | if ranking == 100000: 58 | ranking = "%s+" % ranking 59 | 60 | websites_list = profile['websites'] 61 | websites = [] 62 | for i in websites_list: 63 | websites.append("\n %s" % i) 64 | 65 | skills_list = profile['skillTags'] 66 | skills = [] 67 | for i in skills_list: 68 | skills.append("\n %s" % i) 69 | 70 | architecture = profile['skillSet']['topicAreaScores'][0]['score'] 71 | data_structures = profile['skillSet']['topicAreaScores'][1]['score'] 72 | algorithms = profile['skillSet']['topicAreaScores'][2]['score'] 73 | design = profile['skillSet']['topicAreaScores'][3]['score'] 74 | solved_problems = data_json['submissionProgress']['acTotal'] 75 | ac_submissions = data_json['submissionProgress']['acSubmissions'] 76 | total_question = data_json['submissionProgress']['questionTotal'] 77 | total_submissions = data_json['submissionProgress']['totalSubmissions'] 78 | submission_pass_rate = float(100*ac_submissions/total_submissions) 79 | 80 | text = """userSlug: %s 81 | userName: %s 82 | ranking: %s 83 | websites: %s 84 | skills: %s 85 | score: 86 | architecture: %s%% 87 | data-structures: %s%% 88 | algorithms: %s%% 89 | design: %s%% 90 | solvedProblems: %s/%s 91 | acSubmissions: %s 92 | submissionPassRate:%.2f%% 93 | """ % (user_slug, 94 | user_name, 95 | ranking, 96 | "".join(websites), 97 | "".join(skills), 98 | architecture, 99 | data_structures, 100 | algorithms, 101 | design, 102 | solved_problems, 103 | total_question, 104 | ac_submissions, 105 | submission_pass_rate 106 | ) 107 | return MessageChain.create([Plain(text=text)]) 108 | -------------------------------------------------------------------------------- /modules/MessagePrinter.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 | 6 | 7 | # 插件信息 8 | __name__ = "MessagePrinter" 9 | __description__ = "打印收到的消息" 10 | __author__ = "SAGIRI-kawaii" 11 | __usage__ = "发送消息即可触发" 12 | 13 | 14 | saya = Saya.current() 15 | channel = Channel.current() 16 | 17 | channel.name(__name__) 18 | channel.description(f"{__description__}\n使用方法:{__usage__}") 19 | channel.author(__author__) 20 | 21 | 22 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 23 | async def group_message_listener( 24 | message: MessageChain, 25 | sender: Member, 26 | group: Group 27 | ): 28 | print(f"接收到来自群组 <{group.name} ({group.id})> 中成员 <{sender.name} ({sender.id})> 的消息:{message.asDisplay()}") 29 | 30 | 31 | @channel.use(ListenerSchema(listening_events=[FriendMessage])) 32 | async def friend_message_listener( 33 | message: MessageChain, 34 | sender: Friend 35 | ): 36 | print(f"接收到来自好友 <{sender.nickname} ({sender.id})> 的消息:{message.asDisplay()}") 37 | 38 | 39 | @channel.use(ListenerSchema(listening_events=[TempMessage])) 40 | async def temp_message_listener( 41 | message: MessageChain, 42 | sender: Member 43 | ): 44 | print(f"接收到来自 <{sender.name} ({sender.id})> 的临时消息:{message.asDisplay()}") 45 | -------------------------------------------------------------------------------- /modules/NetworkCompiler.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import re 3 | 4 | from graia.application.message.elements.internal import Plain 5 | from graia.application import GraiaMiraiApplication 6 | from graia.saya import Saya, Channel 7 | from graia.saya.builtins.broadcast.schema import ListenerSchema 8 | from graia.application.event.messages import * 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__ = "NetworkCompiler" 15 | __description__ = "网络编译器(菜鸟教程)" 16 | __author__ = "SAGIRI-kawaii" 17 | __usage__ = "在群内发送 super 语言:\\n代码 即可" 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( 28 | listening_events=[GroupMessage], 29 | inline_dispatchers=[Kanata([RegexMatch('super .*:[\n\r][\s\S]*')])] 30 | )) 31 | async def network_compiler( 32 | app: GraiaMiraiApplication, 33 | message: MessageChain, 34 | group: Group 35 | ): 36 | message_text = message.asDisplay() 37 | language = re.findall(r"super (.*?):", message_text, re.S)[0] 38 | code = message_text[8 + len(language):] 39 | result = await network_compile(language, code) 40 | print(result) 41 | try: 42 | if isinstance(result, str): 43 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=result)])) 44 | else: 45 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=result["output"] if result["output"] else result["errors"])])) 46 | except AccountMuted: 47 | pass 48 | 49 | 50 | legal_language = { 51 | "R": 80, 52 | "vb": 84, 53 | "ts": 1001, 54 | "kt": 19, 55 | "pas": 18, 56 | "lua": 17, 57 | "node.js": 4, 58 | "go": 6, 59 | "swift": 16, 60 | "rs": 9, 61 | "sh": 11, 62 | "pl": 14, 63 | "erl": 12, 64 | "scala": 5, 65 | "cs": 10, 66 | "rb": 1, 67 | "cpp": 7, 68 | "c": 7, 69 | "java": 8, 70 | "py3": 15, 71 | "py": 0, 72 | "php": 3 73 | } 74 | 75 | 76 | async def network_compile(language: str, code: str): 77 | if language not in legal_language: 78 | return f"支持的语言:{', '.join(list(legal_language.keys()))}" 79 | url = "https://tool.runoob.com/compile2.php" 80 | payload = { 81 | "code": code, 82 | "token": "4381fe197827ec87cbac9552f14ec62a", 83 | "stdin": "", 84 | "language": legal_language[language], 85 | "fileext": language 86 | } 87 | headers = { 88 | "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" 89 | } 90 | async with aiohttp.ClientSession() as session: 91 | async with session.post(url=url, headers=headers, data=payload) as resp: 92 | res = await resp.json() 93 | return { 94 | "output": res["output"], 95 | "errors": res["errors"] 96 | } 97 | -------------------------------------------------------------------------------- /modules/NiBuNengXXMa/ArialEnUnicodeBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/NiBuNengXXMa/ArialEnUnicodeBold.ttf -------------------------------------------------------------------------------- /modules/NiBuNengXXMa/BasicImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/NiBuNengXXMa/BasicImage.jpg -------------------------------------------------------------------------------- /modules/NiBuNengXXMa/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from graia.application import GraiaMiraiApplication 5 | from graia.application.exceptions import AccountMuted 6 | from graia.application.message.chain import MessageChain 7 | from graia.application.message.elements.internal import At, Image, Source 8 | from graia.application.event.messages import GroupMessage 9 | from graia.application.message.parser.kanata import Kanata 10 | from graia.application.message.parser.signature import RegexMatch 11 | from graia.broadcast.interrupt import InterruptControl 12 | from graia.saya import Saya, Channel 13 | from graia.saya.builtins.broadcast.schema import ListenerSchema 14 | from graia.application.event.mirai import * 15 | 16 | from PIL import ImageDraw, ImageFont 17 | 18 | from PIL import Image as IMG 19 | 20 | __name__ = "NiBuNengXXMa" 21 | __description__ = "生成示例的这种图片" 22 | __author__ = "eeehhheee" 23 | __usage__ = "发送nbnxxm 文本1 文本2 即可" 24 | 25 | saya = Saya.current() 26 | channel = Channel.current() 27 | bcc = saya.broadcast 28 | inc = InterruptControl(bcc) 29 | 30 | channel.name(__name__) 31 | channel.description(f"{__description__}\n使用方法:{__usage__}") 32 | channel.author(__author__) 33 | 34 | saya.module_context() 35 | 36 | 37 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([RegexMatch('nbnxxm .* .*')])])) 38 | async def send_img(app: GraiaMiraiApplication, group: Group, member: Member, message: MessageChain, source: Source): 39 | _, text1, text2 = message.asDisplay().split(" ") 40 | if os.path.exists(f'./modules/NiBuNengXXMa/temp/{text1} {text2}.jpg'): 41 | try: 42 | await app.sendGroupMessage(group, MessageChain.create( 43 | [At(member.id), Image.fromLocalFile(f'./modules/NiBuNengXXMa/temp/{text1} {text2}.jpg')]), 44 | quote=source.id) 45 | except AccountMuted: 46 | logging.warning('账户被禁言!') 47 | else: 48 | try: 49 | res = await create_img(text1=text1, text2=text2) 50 | await app.sendGroupMessage(group, MessageChain.create( 51 | [At(member.id), Image.fromLocalFile(res)]), quote=source.id) 52 | except AccountMuted: 53 | logging.warning('账户被禁言!') 54 | 55 | 56 | font_size = 15 57 | 58 | 59 | async def create_img(text1: str, text2: str): 60 | font = ImageFont.truetype('./modules/NiBuNengXXMa/ArialEnUnicodeBold.ttf', font_size) 61 | font_width_text1, font_height_text1 = font.getsize(text1) 62 | font_width_text2, font_height_text2 = font.getsize(text2) 63 | font_width_text1 = font_width_text1 / 2 64 | font_height_text1 = font_height_text1 / 2 65 | font_height_text2 = font_height_text2 / 2 66 | font_width_text2 = font_width_text2 / 2 67 | img = IMG.open('./modules/NiBuNengXXMa/BasicImage.jpg') 68 | draw = ImageDraw.Draw(img) 69 | draw.text((225 - font_width_text1, 75 - font_height_text1), text=text1, fill=(0, 0, 0), font=font) 70 | draw.text((350 - font_width_text2, 260 - font_height_text2), text=text2, fill=(0, 0, 0), font=font) 71 | img_name = f'{text1} {text2}.jpg' 72 | img.save(f'./modules/NiBuNengXXMa/temp/{img_name}') 73 | return f'./modules/NiBuNengXXMa/temp/{img_name}' 74 | -------------------------------------------------------------------------------- /modules/NiBuNengXXMa/requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | -------------------------------------------------------------------------------- /modules/NiBuNengXXMa/示例图片.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/NiBuNengXXMa/示例图片.jpg -------------------------------------------------------------------------------- /modules/PdfSearcher.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from bs4 import BeautifulSoup 3 | import re 4 | import qrcode 5 | from PIL import Image as IMG 6 | from PIL import ImageDraw, ImageFont 7 | 8 | from graia.application.message.elements.internal import MessageChain 9 | from graia.application.message.elements.internal import Image 10 | from graia.application.message.elements.internal import Plain 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.event.messages import GroupMessage 15 | from graia.application.event.messages import Group 16 | from graia.application.message.parser.kanata import Kanata 17 | from graia.application.message.parser.signature import RegexMatch 18 | from graia.application.exceptions import AccountMuted 19 | 20 | # 插件信息 21 | __name__ = "PdfSearcher" 22 | __description__ = "搜索PDF" 23 | __author__ = "SAGIRI-kawaii" 24 | __usage__ = "在群内发送 `pdf 关键词` 即可" 25 | 26 | saya = Saya.current() 27 | channel = Channel.current() 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], inline_dispatchers=[Kanata([RegexMatch('pdf .*')])])) 35 | async def pdf_searcher( 36 | app: GraiaMiraiApplication, 37 | message: MessageChain, 38 | group: Group 39 | ): 40 | keyword = message.asDisplay()[4:] 41 | try: 42 | await app.sendGroupMessage(group, await search_pdf(keyword)) 43 | except AccountMuted: 44 | pass 45 | 46 | 47 | async def search_pdf(keyword: str) -> MessageChain: 48 | url = f"https://2lib.org/s/?q={keyword}" 49 | base_url = "https://2lib.org" 50 | async with aiohttp.ClientSession() as session: 51 | async with session.get(url=url) as resp: 52 | html = await resp.read() 53 | soup = BeautifulSoup(html, "html.parser") 54 | divs = soup.find("div", {"id": "searchResultBox"}).find_all("div", {"class": "resItemBox resItemBoxBooks exactMatch"}) 55 | count = 0 56 | books = [] 57 | text = "搜索到以下结果:\n\n" 58 | for div in divs: 59 | count += 1 60 | if count > 5: 61 | break 62 | name = div.find("h3").get_text().strip() 63 | href = div.find("h3").find("a", href=True)["href"] 64 | first_div = div.find("table").find("table").find("div") 65 | publisher = first_div.get_text().strip() if re.search('.*?title="Publisher".*?', str(first_div)) else None 66 | authors = div.find("div", {"class": "authors"}).get_text().strip() 67 | 68 | text += f"{count}.\n" 69 | text += f"名字:{name}\n" 70 | text += f"作者:{authors}\n" if authors else "" 71 | text += f"出版社:{publisher}\n" if publisher else "" 72 | text += f"页面链接:{base_url + href}\n\n" 73 | 74 | books.append({ 75 | "name": name, 76 | "href": base_url + href, 77 | "publisher": publisher, 78 | "authors": authors, 79 | # "download_href": base_url + download_href 80 | }) 81 | 82 | print(name, href, publisher, authors, sep="\n", end="\n\n") 83 | 84 | if not books: 85 | text = "未搜索到结果呢 >A<\n要不要换个关键词试试呢~" 86 | return MessageChain.create([Plain(text=text)]) 87 | 88 | text = text.replace("搜索到以下结果:\n\n", "") 89 | pics_path = await text2piiic_with_link(text=text) 90 | msg = [Plain(text="搜索到以下结果:\n\n")] 91 | for path in pics_path: 92 | msg.append(Image.fromLocalFile(path)) 93 | return MessageChain.create(msg) 94 | 95 | 96 | def is_chinese(ch): 97 | if '\u4e00' <= ch <= '\u9fff': 98 | return True 99 | return False 100 | 101 | 102 | def count_len(string: str) -> int: 103 | length = 0 104 | for i in string: 105 | length += 2 if is_chinese(i) else 1 106 | return length 107 | 108 | 109 | async def text2piiic_with_link(text: str, fontsize=40, x=20, y=40, spacing=15): 110 | pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' 111 | match_res = re.findall(pattern, text, re.S) 112 | for mres in match_res: 113 | text = text.replace(mres, "|||\n\n\n|||") 114 | for i in range(len(match_res)): 115 | qrcode_img = qrcode.make(match_res[i]) 116 | qrcode_img.save(f"./temp/tempQrcodeWithLink{i + 1}.jpg") 117 | blocks = text.split("|||\n\n\n|||") 118 | block_count = 0 119 | font = ImageFont.truetype('./simhei.ttf', fontsize, encoding="utf-8") 120 | for block in blocks: 121 | if not block or not block.strip(): 122 | break 123 | block_count += 1 124 | lines = block.strip().split("\n") 125 | length = max(count_len(line) for line in lines) 126 | width = x * 4 + int(fontsize * (length + 10) / 2) 127 | height = y * 4 + (fontsize + spacing) * len(lines) + width 128 | qr_img = IMG.open(f"./temp/tempQrcodeWithLink{block_count}.jpg") 129 | qr_img = qr_img.resize((width, width)) 130 | picture = IMG.new('RGB', (width, height), (255, 255, 255)) 131 | draw = ImageDraw.Draw(picture) 132 | for i in range(len(lines)): 133 | y_pos = y + i * (fontsize + spacing) 134 | draw.text((x, y_pos), lines[i], font=font, fill=(0, 0, 0)) 135 | y_pos = y + len(lines) * (fontsize + spacing) 136 | picture.paste(qr_img, (0, y_pos)) 137 | picture.save(f"./temp/tempText2piiicWithLink{block_count}.jpg") 138 | 139 | return [ 140 | f"./temp/tempText2piiicWithLink{i + 1}.jpg" for i in range(block_count) 141 | ] 142 | -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/frame0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/frame0.png -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/frame1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/frame1.png -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/frame2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/frame2.png -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/frame3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/frame3.png -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/frame4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/frame4.png -------------------------------------------------------------------------------- /modules/PetPet/PetPetFrames/template.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PetPet/PetPetFrames/template.gif -------------------------------------------------------------------------------- /modules/PetPet/README.md: -------------------------------------------------------------------------------- 1 | ## PetPet 2 | 3 | 一个摸头gif生成器 4 | 5 | ## 使用注意 6 | 7 | 请先使用 `pip install -r requirements.txt` 命令安装所需依赖 -------------------------------------------------------------------------------- /modules/PetPet/__init__.py: -------------------------------------------------------------------------------- 1 | from PIL import Image as IMG 2 | from PIL import ImageOps 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 15 | from graia.application.message.elements.internal import Image 16 | from graia.application.event.messages import Group 17 | from graia.application.exceptions import AccountMuted 18 | 19 | # 插件信息 20 | __name__ = "PetPet" 21 | __description__ = "生成摸头gif" 22 | __author__ = "SAGIRI-kawaii" 23 | __usage__ = "在群内发送 摸@目标 即可" 24 | 25 | saya = Saya.current() 26 | channel = Channel.current() 27 | 28 | channel.name(__name__) 29 | channel.description(f"{__description__}\n使用方法:{__usage__}") 30 | channel.author(__author__) 31 | 32 | 33 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 34 | async def petpet_generator(app: GraiaMiraiApplication, message: MessageChain, group: Group): 35 | message_text = message.asDisplay() 36 | if message.has(At) and message_text.startswith("摸") or message_text.startswith("摸 "): 37 | if not os.path.exists("./modules/PetPet/temp"): 38 | os.mkdir("./modules/PetPet/temp") 39 | await petpet(message.get(At)[0].target) 40 | try: 41 | await app.sendGroupMessage( 42 | group, 43 | MessageChain.create([ 44 | Image.fromLocalFile(f"./modules/PetPet/temp/tempPetPet-{message.get(At)[0].target}.gif") 45 | ]) 46 | ) 47 | except AccountMuted: 48 | pass 49 | 50 | frame_spec = [ 51 | (27, 31, 86, 90), 52 | (22, 36, 91, 90), 53 | (18, 41, 95, 90), 54 | (22, 41, 91, 91), 55 | (27, 28, 86, 91) 56 | ] 57 | 58 | squish_factor = [ 59 | (0, 0, 0, 0), 60 | (-7, 22, 8, 0), 61 | (-8, 30, 9, 6), 62 | (-3, 21, 5, 9), 63 | (0, 0, 0, 0) 64 | ] 65 | 66 | squish_translation_factor = [0, 20, 34, 21, 0] 67 | 68 | frames = tuple([f'./modules/PetPet/PetPetFrames/frame{i}.png' for i in range(5)]) 69 | 70 | 71 | async def save_gif(gif_frames, dest, fps=10): 72 | """生成 gif 73 | 74 | 将输入的帧数据合并成视频并输出为 gif 75 | 76 | 参数 77 | gif_frames: list 78 | 为每一帧的数据 79 | dest: str 80 | 为输出路径 81 | fps: int, float 82 | 为输出 gif 每秒显示的帧数 83 | 84 | 返回 85 | None 86 | 但是会输出一个符合参数的 gif 87 | """ 88 | clip = imageclip(gif_frames, fps=fps) 89 | clip.write_gif(dest) # 使用 imageio 90 | clip.close() 91 | 92 | 93 | # 生成函数(非数学意味) 94 | async def make_frame(avatar, i, squish=0, flip=False): 95 | """生成帧 96 | 97 | 将输入的头像转变为参数指定的帧,以供 make_gif() 处理 98 | 99 | 参数 100 | avatar: PIL.Image.Image 101 | 为头像 102 | i: int 103 | 为指定帧数 104 | squish: float 105 | 为一个 [0, 1] 之间的数,为挤压量 106 | flip: bool 107 | 为是否横向反转头像 108 | 109 | 返回 110 | numpy.ndarray 111 | 为处理完的帧的数据 112 | """ 113 | # 读入位置 114 | spec = list(frame_spec[i]) 115 | # 将位置添加偏移量 116 | for j, s in enumerate(spec): 117 | spec[j] = int(s + squish_factor[i][j] * squish) 118 | # 读取手 119 | hand = IMG.open(frames[i]) 120 | # 反转 121 | if flip: 122 | avatar = ImageOps.mirror(avatar) 123 | # 将头像放缩成所需大小 124 | avatar = avatar.resize((int((spec[2] - spec[0]) * 1.2), int((spec[3] - spec[1]) * 1.2)), IMG.ANTIALIAS) 125 | # 并贴到空图像上 126 | gif_frame = IMG.new('RGB', (112, 112), (255, 255, 255)) 127 | gif_frame.paste(avatar, (spec[0], spec[1])) 128 | # 将手覆盖(包括偏移量) 129 | gif_frame.paste(hand, (0, int(squish * squish_translation_factor[i])), hand) 130 | # 返回 131 | return numpy.array(gif_frame) 132 | 133 | 134 | async def petpet(member_id, flip=False, squish=0, fps=20) -> None: 135 | """生成PetPet 136 | 137 | 将输入的头像生成为所需的 PetPet 并输出 138 | 139 | 参数 140 | path: str 141 | 为头像路径 142 | flip: bool 143 | 为是否横向反转头像 144 | squish: float 145 | 为一个 [0, 1] 之间的数,为挤压量 146 | fps: int 147 | 为输出 gif 每秒显示的帧数 148 | 149 | 返回 150 | bool 151 | 但是会输出一个符合参数的 gif 152 | """ 153 | 154 | url = f'http://q1.qlogo.cn/g?b=qq&nk={str(member_id)}&s=640' 155 | gif_frames = [] 156 | # 打开头像 157 | # avatar = Image.open(path) 158 | async with aiohttp.ClientSession() as session: 159 | async with session.get(url=url) as resp: 160 | img_content = await resp.read() 161 | 162 | avatar = IMG.open(BytesIO(img_content)) 163 | 164 | # 生成每一帧 165 | for i in range(5): 166 | gif_frames.append(await make_frame(avatar, i, squish=squish, flip=flip)) 167 | # 输出 168 | await save_gif(gif_frames, f'./modules/PetPet/temp/tempPetPet-{member_id}.gif', fps=fps) 169 | 170 | -------------------------------------------------------------------------------- /modules/PetPet/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | moviepy 3 | aiohttp 4 | Pillow 5 | -------------------------------------------------------------------------------- /modules/PhantomTank/__init__.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import os 3 | from io import BytesIO 4 | from PIL import Image as IMG 5 | 6 | from graia.saya import Saya, Channel 7 | from graia.saya.builtins.broadcast.schema import ListenerSchema 8 | from graia.application.exceptions import AccountMuted 9 | from graia.application import GraiaMiraiApplication 10 | from graia.application.event.messages import GroupMessage, Group, Member 11 | from graia.application.message.parser.kanata import Kanata 12 | from graia.application.message.parser.signature import RegexMatch 13 | from graia.application.event.messages import MessageChain 14 | from graia.application.message.elements.internal import Plain 15 | from graia.application.message.elements.internal import Image 16 | 17 | from .utils import make_tank, colorful_tank 18 | 19 | # 插件信息 20 | __name__ = "PhantomTank" 21 | __description__ = "一个幻影坦克生成器" 22 | __author__ = "SAGIRI-kawaii" 23 | __usage__ = "群内发送 `(幻影|彩色幻影)[图片][图片]` 即可" 24 | 25 | saya = Saya.current() 26 | channel = Channel.current() 27 | 28 | channel.name(__name__) 29 | channel.description(f"{__description__}\n使用方法:{__usage__}") 30 | channel.author(__author__) 31 | 32 | signal: int = 0 33 | 34 | 35 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 36 | async def phantom_tank(app: GraiaMiraiApplication, message: MessageChain, group: Group): 37 | 38 | message_text = "".join([plain.text for plain in message.get(Plain)]).strip() 39 | if message_text in ["幻影", "彩色幻影"]: 40 | if len(message[Image]) != 2: 41 | try: 42 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="非预期图片数量!请按 `显示图 隐藏图` 顺序发送,共两张")])) 43 | except AccountMuted: 44 | pass 45 | return None 46 | if globals()["signal"] >= 2: 47 | try: 48 | await app.sendGroupMessage(group, MessageChain.create([Plain(text=f"目前有{signal}个任务正在处理,请稍后再试!")])) 49 | except AccountMuted: 50 | pass 51 | return None 52 | 53 | globals()["signal"] += 1 54 | 55 | try: 56 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="converting")])) 57 | except AccountMuted: 58 | pass 59 | 60 | im_1 = message[Image][0] 61 | async with aiohttp.ClientSession() as session: 62 | async with session.get(url=im_1.url) as resp: 63 | img_content = await resp.read() 64 | display_img = IMG.open(BytesIO(img_content)) 65 | 66 | im_2 = message[Image][1] 67 | async with aiohttp.ClientSession() as session: 68 | async with session.get(url=im_2.url) as resp: 69 | img_content = await resp.read() 70 | hide_img = IMG.open(BytesIO(img_content)) 71 | 72 | try: 73 | await app.sendGroupMessage( 74 | group, 75 | MessageChain.create([ 76 | Image.fromUnsafeBytes(await make_tank(display_img, hide_img) if message_text == "幻影" else await colorful_tank(display_img, hide_img)) 77 | # Image.fromUnsafeBytes(await colorful_tank(display_img, hide_img)) 78 | ]) 79 | ) 80 | except AccountMuted: 81 | pass 82 | 83 | globals()["signal"] -= 1 84 | -------------------------------------------------------------------------------- /modules/PhantomTank/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image as IMG 3 | from io import BytesIO 4 | from PIL import ImageEnhance 5 | 6 | 7 | async def get_max_size(a, b): 8 | return a if a[0] * a[1] >= b[0] * b[1] else b 9 | 10 | 11 | async def make_tank(im_1: IMG, im_2: IMG) -> bytes: 12 | im_1 = im_1.convert("L") 13 | im_2 = im_2.convert("L") 14 | max_size = await get_max_size(im_1.size, im_2.size) 15 | if max_size == im_1.size: 16 | im_2 = im_2.resize(max_size) 17 | else: 18 | im_1 = im_1.resize(max_size) 19 | arr_1 = np.array(im_1, dtype=np.uint8) 20 | arr_2 = np.array(im_2, dtype=np.uint8) 21 | arr_1 = 225 - 70 * ((np.max(arr_1) - arr_1) / (np.max(arr_1) - np.min(arr_1))) 22 | arr_2 = 30 + 70 * ((arr_2 - np.min(arr_2)) / (np.max(arr_2) - np.min(arr_2))) 23 | arr_alpha = 255 - (arr_1 - arr_2) 24 | arr_offset = arr_2 * (255 / arr_alpha) 25 | arr_new = np.dstack([arr_offset, arr_alpha]).astype(np.uint8) 26 | if arr_new.shape[0] == 3: 27 | arr_new = (np.transpose(arr_new, (1, 2, 0)) + 1) / 2.0 * 255.0 28 | bytesIO = BytesIO() 29 | IMG.fromarray(arr_new).save(bytesIO, format='PNG') 30 | return bytesIO.getvalue() 31 | 32 | 33 | async def colorful_tank( 34 | wimg: IMG.Image, 35 | bimg: IMG.Image, 36 | wlight: float = 1.0, 37 | blight: float = 0.18, 38 | wcolor: float = 0.5, 39 | bcolor: float = 0.7, 40 | chess: bool = False 41 | ): 42 | wimg = ImageEnhance.Brightness(wimg).enhance(wlight).convert("RGB") 43 | bimg = ImageEnhance.Brightness(bimg).enhance(blight).convert("RGB") 44 | 45 | max_size = await get_max_size(wimg.size, bimg.size) 46 | if max_size == wimg.size: 47 | bimg = bimg.resize(max_size) 48 | else: 49 | wimg = wimg.resize(max_size) 50 | 51 | wpix = np.array(wimg).astype("float64") 52 | bpix = np.array(bimg).astype("float64") 53 | 54 | if chess: 55 | wpix[::2, ::2] = [255., 255., 255.] 56 | bpix[1::2, 1::2] = [0., 0., 0.] 57 | 58 | wpix /= 255. 59 | bpix /= 255. 60 | 61 | wgray = wpix[:, :, 0] * 0.334 + wpix[:, :, 1] * 0.333 + wpix[:, :, 2] * 0.333 62 | wpix *= wcolor 63 | wpix[:, :, 0] += wgray * (1. - wcolor) 64 | wpix[:, :, 1] += wgray * (1. - wcolor) 65 | wpix[:, :, 2] += wgray * (1. - wcolor) 66 | 67 | bgray = bpix[:, :, 0] * 0.334 + bpix[:, :, 1] * 0.333 + bpix[:, :, 2] * 0.333 68 | bpix *= bcolor 69 | bpix[:, :, 0] += bgray * (1. - bcolor) 70 | bpix[:, :, 1] += bgray * (1. - bcolor) 71 | bpix[:, :, 2] += bgray * (1. - bcolor) 72 | 73 | d = 1. - wpix + bpix 74 | 75 | d[:, :, 0] = d[:, :, 1] = d[:, :, 2] = d[:, :, 0] * 0.222 + d[:, :, 1] * 0.707 + d[:, :, 2] * 0.071 76 | 77 | p = np.where(d != 0, bpix / d * 255., 255.) 78 | a = d[:, :, 0] * 255. 79 | 80 | colors = np.zeros((p.shape[0], p.shape[1], 4)) 81 | colors[:, :, :3] = p 82 | colors[:, :, -1] = a 83 | 84 | colors[colors > 255] = 255 85 | 86 | bytesIO = BytesIO() 87 | # IMG.fromarray(colors.astype("uint8")).convert("RGBA").save("output.png") 88 | IMG.fromarray(colors.astype("uint8")).convert("RGBA").save(bytesIO, format='PNG') 89 | 90 | return bytesIO.getvalue() 91 | 92 | 93 | # colorful_tank(IMG.open("M:\\pixiv\\sagiri\\63005167_p0.jpg"), IMG.open("M:\\pixiv\\sagiri\\7903.png")) 94 | -------------------------------------------------------------------------------- /modules/PixivImageSearcher/__init__.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from io import BytesIO 3 | from PIL import Image as IMG 4 | import time 5 | 6 | from graia.application import GraiaMiraiApplication 7 | from graia.application.message.chain import MessageChain 8 | from graia.application.message.elements.internal import Source 9 | from graia.application.message.elements.internal import Plain 10 | from graia.application.message.elements.internal import At 11 | from graia.application.message.elements.internal import Image 12 | from graia.application.event.messages import GroupMessage 13 | from graia.broadcast.interrupt import InterruptControl 14 | from graia.broadcast.interrupt.waiter import Waiter 15 | from graia.application.message.parser.kanata import Kanata 16 | from graia.application.message.parser.signature import FullMatch 17 | from graia.application.exceptions import AccountMuted 18 | from graia.saya import Saya, Channel 19 | from graia.saya.builtins.broadcast.schema import ListenerSchema 20 | from graia.application.event.mirai import * 21 | 22 | # 插件信息 23 | __name__ = "PixivImageSearcher" 24 | __description__ = "SauceNao以图搜图" 25 | __author__ = "SAGIRI-kawaii" 26 | __usage__ = "发送搜图后发送图片即可" 27 | 28 | saya = Saya.current() 29 | channel = Channel.current() 30 | bcc = saya.broadcast 31 | inc = InterruptControl(bcc) 32 | 33 | channel.name(__name__) 34 | channel.description(f"{__description__}\n使用方法:{__usage__}") 35 | channel.author(__author__) 36 | 37 | # 填入你的saucenao_cookie 38 | saucenao_cookie = "" 39 | 40 | 41 | @channel.use(ListenerSchema(listening_events=[GroupMessage], inline_dispatchers=[Kanata([FullMatch('搜图')])])) 42 | async def pixiv_image_searcher(app: GraiaMiraiApplication, member: Member, group: Group): 43 | image_get: bool = False 44 | message_received = None 45 | 46 | try: 47 | await app.sendGroupMessage(group, MessageChain.create([ 48 | At(member.id), Plain("请在30秒内发送要搜索的图片呐~(仅支持pixiv图片搜索呐!)") 49 | ])) 50 | except AccountMuted: 51 | return None 52 | 53 | @Waiter.create_using_function([GroupMessage]) 54 | def waiter( 55 | event: GroupMessage, waiter_group: Group, 56 | waiter_member: Member, waiter_message: MessageChain 57 | ): 58 | nonlocal image_get 59 | nonlocal message_received 60 | if time.time() - start_time < 30: 61 | if all([ 62 | waiter_group.id == group.id, 63 | waiter_member.id == member.id, 64 | len(waiter_message[Image]) == len(waiter_message.__root__) - 1 65 | ]): 66 | image_get = True 67 | message_received = waiter_message 68 | return event 69 | else: 70 | print("等待用户发送图片超时!") 71 | return event 72 | 73 | start_time = time.time() 74 | await inc.wait(waiter) 75 | if image_get: 76 | try: 77 | await app.sendGroupMessage( 78 | group, 79 | await search_image(message_received[Image][0]), 80 | quote=message_received[Source][0] 81 | ) 82 | except AccountMuted: 83 | pass 84 | 85 | 86 | async def search_image(img: Image) -> MessageChain: 87 | path = "./modules/PixivImageSearcher/tempSavedImage.png" 88 | thumbnail_path = "./modules/PixivImageSearcher/tempThumbnail.png" 89 | async with aiohttp.ClientSession() as session: 90 | async with session.get(url=img.url) as resp: 91 | img_content = await resp.read() 92 | image = IMG.open(BytesIO(img_content)) 93 | image.save(path) 94 | 95 | # url for headers 96 | url = "https://saucenao.com/search.php" 97 | 98 | # picture url 99 | pic_url = img.url 100 | 101 | # json requesting url 102 | url2 = f"https://saucenao.com/search.php?db=999&output_type=2&testmode=1&numres=1&url={pic_url}" 103 | 104 | # data for posting. 105 | payload = { 106 | "url": pic_url, 107 | "numres": 1, 108 | "testmode": 1, 109 | "db": 999, 110 | "output_type": 2, 111 | } 112 | 113 | # header to fool the website. 114 | headers = { 115 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", 116 | "Sec-Fetch-Dest": "document", 117 | "Sec-Fetch-Mode": "navigate", 118 | "Sec-Fetch-Site": "none", 119 | "Sec-Fetch-User": "?1", 120 | "Referer": url, 121 | "Origin": "https://saucenao.com", 122 | "Host": "saucenao.com", 123 | "cookie": saucenao_cookie 124 | } 125 | 126 | async with aiohttp.ClientSession() as session: 127 | async with session.post(url=url, headers=headers, data=payload) as resp: 128 | json_data = await resp.json() 129 | 130 | if json_data["header"]["status"] == -1: 131 | return MessageChain.create([ 132 | Plain(text=f"错误:{json_data['header']['message']}") 133 | ]) 134 | print(json_data) 135 | 136 | if not json_data["results"]: 137 | return MessageChain.create([ 138 | Plain(text="没有搜索到结果呐~") 139 | ]) 140 | 141 | result = json_data["results"][0] 142 | header = result["header"] 143 | data = result["data"] 144 | 145 | async with aiohttp.ClientSession() as session: 146 | async with session.get(url=header["thumbnail"]) as resp: 147 | img_content = await resp.read() 148 | 149 | image = IMG.open(BytesIO(img_content)) 150 | image.save(thumbnail_path) 151 | similarity = header["similarity"] 152 | data_str = f"搜索到如下结果:\n\n相似度:{similarity}%\n" 153 | for key in data.keys(): 154 | if isinstance(data[key], list): 155 | data_str += (f"\n{key}:\n " + "\n".join(data[key]) + "\n") 156 | else: 157 | data_str += f"\n{key}:\n {data[key]}\n" 158 | return MessageChain.create([ 159 | Image.fromLocalFile(thumbnail_path), 160 | Plain(text=f"\n{data_str}") 161 | ]) 162 | -------------------------------------------------------------------------------- /modules/PornhubStyleLogoGenerator/__init__.py: -------------------------------------------------------------------------------- 1 | from PIL import Image as IMG, ImageDraw, ImageFont 2 | import os 3 | 4 | from graia.application.message.chain import MessageChain 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.saya import Saya, Channel 9 | from graia.saya.builtins.broadcast.schema import ListenerSchema 10 | from graia.application.exceptions import AccountMuted 11 | from graia.application.event.messages import GroupMessage, Group, Member 12 | from graia.application.message.parser.kanata import Kanata 13 | from graia.application.message.parser.signature import RegexMatch 14 | 15 | # 插件信息 16 | __name__ = "PornhubStyleLogoGenerator" 17 | __description__ = "一个可以生成 pornhub style logo 的插件" 18 | __author__ = "SAGIRI-kawaii" 19 | __usage__ = "发送 `ph text1 text2` 即可" 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 | 29 | @channel.use(ListenerSchema( 30 | listening_events=[GroupMessage], 31 | inline_dispatchers=[Kanata([RegexMatch('ph .* .*')])] 32 | )) 33 | async def pornhub_style_logo_generator( 34 | app: GraiaMiraiApplication, 35 | message: MessageChain, 36 | group: Group 37 | ): 38 | try: 39 | _, left_text, right_text = message.asDisplay().split(" ") 40 | try: 41 | await app.sendGroupMessage(group, await make_ph_style_logo(left_text, right_text)) 42 | except AccountMuted: 43 | pass 44 | except ValueError: 45 | try: 46 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="参数非法!使用格式:ph text1 text2")])) 47 | except AccountMuted: 48 | pass 49 | 50 | 51 | LEFT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT = 2 52 | LEFT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH = 1 / 4 53 | RIGHT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT = 1 54 | RIGHT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH = 1 / 4 55 | RIGHT_PART_RADII = 10 56 | BG_COLOR = '#000000' 57 | BOX_COLOR = '#F7971D' 58 | LEFT_TEXT_COLOR = '#FFFFFF' 59 | RIGHT_TEXT_COLOR = '#000000' 60 | FONT_SIZE = 50 61 | 62 | 63 | async def create_left_part_img(text: str, font_size: int): 64 | font = ImageFont.truetype('./modules/PornhubStyleLogoGenerator/ttf/ArialEnUnicodeBold.ttf', font_size) 65 | font_width, font_height = font.getsize(text) 66 | offset_y = font.font.getsize(text)[1][1] 67 | blank_height = font_height * LEFT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT 68 | right_blank = int(font_width / len(text) * LEFT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH) 69 | img_height = font_height + offset_y + blank_height * 2 70 | image_width = font_width + right_blank 71 | image_size = image_width, img_height 72 | image = IMG.new('RGBA', image_size, BG_COLOR) 73 | draw = ImageDraw.Draw(image) 74 | draw.text((0, blank_height), text, fill=LEFT_TEXT_COLOR, font=font) 75 | return image 76 | 77 | 78 | async def create_right_part_img(text: str, font_size: int): 79 | radii = RIGHT_PART_RADII 80 | font = ImageFont.truetype('./modules/PornhubStyleLogoGenerator/ttf/ArialEnUnicodeBold.ttf', font_size) 81 | font_width, font_height = font.getsize(text) 82 | offset_y = font.font.getsize(text)[1][1] 83 | blank_height = font_height * RIGHT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT 84 | left_blank = int(font_width / len(text) * RIGHT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH) 85 | image_width = font_width + 2 * left_blank 86 | image_height = font_height + offset_y + blank_height * 2 87 | image = IMG.new('RGBA', (image_width, image_height), BOX_COLOR) 88 | draw = ImageDraw.Draw(image) 89 | draw.text((left_blank, blank_height), text, fill=RIGHT_TEXT_COLOR, font=font) 90 | 91 | # 圆 92 | magnify_time = 10 93 | magnified_radii = radii * magnify_time 94 | circle = IMG.new('L', (magnified_radii * 2, magnified_radii * 2), 0) # 创建一个黑色背景的画布 95 | draw = ImageDraw.Draw(circle) 96 | draw.ellipse((0, 0, magnified_radii * 2, magnified_radii * 2), fill=255) # 画白色圆形 97 | 98 | # 画4个角(将整圆分离为4个部分) 99 | magnified_alpha_width = image_width * magnify_time 100 | magnified_alpha_height = image_height * magnify_time 101 | alpha = IMG.new('L', (magnified_alpha_width, magnified_alpha_height), 255) 102 | alpha.paste(circle.crop((0, 0, magnified_radii, magnified_radii)), (0, 0)) # 左上角 103 | alpha.paste(circle.crop((magnified_radii, 0, magnified_radii * 2, magnified_radii)), 104 | (magnified_alpha_width - magnified_radii, 0)) # 右上角 105 | alpha.paste(circle.crop((magnified_radii, magnified_radii, magnified_radii * 2, magnified_radii * 2)), 106 | (magnified_alpha_width - magnified_radii, magnified_alpha_height - magnified_radii)) # 右下角 107 | alpha.paste(circle.crop((0, magnified_radii, magnified_radii, magnified_radii * 2)), 108 | (0, magnified_alpha_height - magnified_radii)) # 左下角 109 | alpha = alpha.resize((image_width, image_height), IMG.ANTIALIAS) 110 | image.putalpha(alpha) 111 | return image 112 | 113 | 114 | async def combine_img(left_text: str, right_text, font_size: int, out_put_path: str): 115 | left_img = await create_left_part_img(left_text, font_size) 116 | right_img = await create_right_part_img(right_text, font_size) 117 | blank = 30 118 | bg_img_width = left_img.width + right_img.width + blank * 2 119 | bg_img_height = left_img.height 120 | bg_img = IMG.new('RGBA', (bg_img_width, bg_img_height), BG_COLOR) 121 | bg_img.paste(left_img, (blank, 0)) 122 | bg_img.paste(right_img, (blank + left_img.width, int((bg_img_height - right_img.height) / 2)), mask=right_img) 123 | bg_img.save(out_put_path) 124 | 125 | 126 | async def make_ph_style_logo(left_text: str, right_text: str) -> MessageChain: 127 | img_name = f'ph_{left_text}_{right_text}.png' 128 | out_put_path = f"./modules/PornhubStyleLogoGenerator/temp/{img_name}" 129 | if not os.path.exists("./modules/PornhubStyleLogoGenerator/temp"): 130 | os.mkdir("./modules/PornhubStyleLogoGenerator/temp") 131 | await combine_img(left_text, right_text, FONT_SIZE, out_put_path) 132 | return MessageChain.create([Image.fromLocalFile(out_put_path)]) 133 | -------------------------------------------------------------------------------- /modules/PornhubStyleLogoGenerator/ttf/ArialEnUnicodeBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/PornhubStyleLogoGenerator/ttf/ArialEnUnicodeBold.ttf -------------------------------------------------------------------------------- /modules/Repeater.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import re 3 | 4 | from graia.application import GraiaMiraiApplication 5 | from graia.application.exceptions import AccountMuted 6 | from graia.saya import Saya, Channel 7 | from graia.saya.builtins.broadcast.schema import ListenerSchema 8 | from graia.application.event.messages import * 9 | from graia.application.event.mirai import * 10 | 11 | 12 | # 插件信息 13 | __name__ = "Repeater" 14 | __description__ = "复读🐓(x" 15 | __author__ = "SAGIRI-kawaii" 16 | __usage__ = "两个相同message即可触发复读" 17 | 18 | saya = Saya.current() 19 | channel = Channel.current() 20 | group_repeat = {} 21 | lock = threading.Lock() 22 | 23 | channel.name(__name__) 24 | channel.description(f"{__description__}\n使用方法:{__usage__}") 25 | channel.author(__author__) 26 | 27 | 28 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 29 | async def repeater(app: GraiaMiraiApplication, message: MessageChain, group: Group): 30 | group_id = group.id 31 | message_serialization = message.asSerializationString() 32 | message_serialization = message_serialization.replace( 33 | "[mirai:source:" + re.findall(r'\[mirai:source:(.*?)]', message_serialization, re.S)[0] + "]", 34 | "" 35 | ) 36 | 37 | # lock.acquire() 38 | if group_id in group_repeat.keys(): 39 | group_repeat[group.id]["lastMsg"] = group_repeat[group.id]["thisMsg"] 40 | group_repeat[group.id]["thisMsg"] = message_serialization 41 | if group_repeat[group.id]["lastMsg"] != group_repeat[group.id]["thisMsg"]: 42 | group_repeat[group.id]["stopMsg"] = "" 43 | else: 44 | if group_repeat[group.id]["thisMsg"] != group_repeat[group.id]["stopMsg"]: 45 | group_repeat[group.id]["stopMsg"] = group_repeat[group.id]["thisMsg"] 46 | try: 47 | await app.sendGroupMessage(group, message.asSendable()) 48 | except AccountMuted: 49 | pass 50 | else: 51 | group_repeat[group_id] = {"lastMsg": "", "thisMsg": "", "stopMsg": ""} 52 | # lock.release() 53 | -------------------------------------------------------------------------------- /modules/SteamGameSearcher/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import aiohttp 3 | from PIL import Image as IMG 4 | from io import BytesIO 5 | import re 6 | 7 | from graia.application.message.chain import MessageChain 8 | from graia.saya import Saya, Channel 9 | from graia.saya.builtins.broadcast.schema import ListenerSchema 10 | from graia.application.exceptions import AccountMuted 11 | from graia.application.message.elements.internal import Plain 12 | from graia.application.message.elements.internal import Image 13 | from graia.application import GraiaMiraiApplication 14 | from graia.application.event.messages import GroupMessage, Group 15 | from graia.application.message.parser.kanata import Kanata 16 | from graia.application.message.parser.signature import RegexMatch 17 | 18 | from utils import messagechain_to_img 19 | 20 | # 插件信息 21 | __name__ = "SteamGameSearcher" 22 | __description__ = "一个通过关键词搜索steam游戏的插件" 23 | __author__ = "SAGIRI-kawaii" 24 | __usage__ = "在群内发送 `steam 游戏名` 即可" 25 | 26 | saya = Saya.current() 27 | channel = Channel.current() 28 | 29 | channel.name(__name__) 30 | channel.description(f"{__description__}\n使用方法:{__usage__}") 31 | channel.author(__author__) 32 | 33 | 34 | @channel.use(ListenerSchema( 35 | listening_events=[GroupMessage], 36 | inline_dispatchers=[Kanata([RegexMatch('steam .*')])] 37 | )) 38 | async def steam_game_searcher( 39 | app: GraiaMiraiApplication, 40 | message: MessageChain, 41 | group: Group 42 | ): 43 | keyword = message.asDisplay()[6:] 44 | try: 45 | if keyword: 46 | await app.sendGroupMessage(group, await get_steam_game_search(keyword)) 47 | else: 48 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="请输入你要搜索的关键词(英文更管用哦~)")])) 49 | except AccountMuted: 50 | pass 51 | 52 | 53 | async def get_steam_game_description(game_id: int) -> str: 54 | """ 55 | Return game description on steam 56 | 57 | Args: 58 | game_id: Steam shop id of target game 59 | 60 | Examples: 61 | get_steam_game_description(502010) 62 | 63 | Return: 64 | str 65 | """ 66 | url = "https://store.steampowered.com/app/%s/" % game_id 67 | async with aiohttp.ClientSession() as session: 68 | async with session.get(url=url) as resp: 69 | html = await resp.text() 70 | description = re.findall(r'
(.*?)
', html, re.S) 71 | if len(description) == 0: 72 | return "none" 73 | return description[0].lstrip().rstrip() 74 | 75 | 76 | async def get_steam_game_search(keyword: str, msg_type: str = "text") -> MessageChain: 77 | """ 78 | Return search result 79 | 80 | Args: 81 | keyword: Keyword to search(game name) 82 | msg_type: Type of MessageChain 83 | 84 | Examples: 85 | await get_steam_game_search("Monster Hunter") 86 | 87 | Return: 88 | MessageChain 89 | """ 90 | 91 | base_path = "./modules/SteamGameSearcher/game_cover_cache/" 92 | if not os.path.exists(base_path): 93 | os.mkdir(base_path) 94 | 95 | url = "https://steamstats.cn/api/steam/search?q=%s&page=1&format=json&lang=zh-hans" % keyword 96 | headers = { 97 | "referer": "https://steamstats.cn/", 98 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) " 99 | "Chrome/85.0.4183.121 Safari/537.36 " 100 | } 101 | 102 | async with aiohttp.ClientSession() as session: 103 | async with session.get(url=url, headers=headers) as resp: 104 | result = await resp.json() 105 | 106 | if len(result["data"]["results"]) == 0: 107 | return MessageChain.create([Plain(text=f"搜索不到{keyword}呢~检查下有没有吧~偷偷告诉你,搜英文名的效果可能会更好哟~")]) 108 | else: 109 | result = result["data"]["results"][0] 110 | path = f"{base_path}{result['app_id']}.png" 111 | print(f"cache: {os.path.exists(path)}") 112 | if not os.path.exists(path): 113 | async with aiohttp.ClientSession() as session: 114 | async with session.get(url=result["avatar"]) as resp: 115 | img_content = await resp.read() 116 | image = IMG.open(BytesIO(img_content)) 117 | image.save(path) 118 | description = await get_steam_game_description(result["app_id"]) 119 | msg = MessageChain.create([ 120 | Plain(text="\n搜索到以下信息:\n"), 121 | Plain(text="游戏:%s (%s)\n" % (result["name"], result["name_cn"])), 122 | Plain(text="游戏id:%s\n" % result["app_id"]), 123 | Image.fromLocalFile(path), 124 | Plain(text="游戏描述:%s\n" % description), 125 | Plain(text="\nsteamUrl:https://store.steampowered.com/app/%s/" % result["app_id"]) 126 | ]) 127 | return await messagechain_to_img(msg) if msg_type == "img" else msg 128 | -------------------------------------------------------------------------------- /modules/Text2QrcodeGenerator.py: -------------------------------------------------------------------------------- 1 | import qrcode 2 | 3 | from graia.application import GraiaMiraiApplication 4 | from graia.application.message.elements.internal import Plain 5 | from graia.application.message.elements.internal import Image 6 | from graia.saya import Saya, Channel 7 | from graia.saya.builtins.broadcast.schema import ListenerSchema 8 | from graia.application.event.messages import * 9 | from graia.application.exceptions import AccountMuted 10 | 11 | # 插件信息 12 | __name__ = "Text2QrcodeGenerator" 13 | __description__ = "一个简易的文字转二维码的插件" 14 | __author__ = "SAGIRI-kawaii" 15 | __usage__ = "在群内发送 `qrcode 内容` 即可" 16 | 17 | saya = Saya.current() 18 | channel = Channel.current() 19 | 20 | channel.name(__name__) 21 | channel.description(f"{__description__}\n使用方法:{__usage__}") 22 | channel.author(__author__) 23 | 24 | 25 | @channel.use(ListenerSchema(listening_events=[GroupMessage])) 26 | async def make_qrcode(app: GraiaMiraiApplication, message: MessageChain, group: Group): 27 | if message.asDisplay().startswith("qrcode "): 28 | content = "".join([plain.text for plain in message[Plain]])[7:] 29 | try: 30 | if content: 31 | img = qrcode.make(content) 32 | img.save("./temp/tempQrcodeMaked.jpg") 33 | await app.sendGroupMessage( 34 | group, 35 | MessageChain.create([Image.fromLocalFile("./temp/tempQrcodeMaked.jpg")]), 36 | quote=message[Source][0] 37 | ) 38 | else: 39 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="无效内容!")])) 40 | except AccountMuted: 41 | pass 42 | -------------------------------------------------------------------------------- /modules/Weather/README.md: -------------------------------------------------------------------------------- 1 | # 利用和风天气api制作的一个天气预报机器人 2 | 3 | 使用前需要自行修改配置,复制或重命名 `config_demo.py` 为 `config.py`,添加 `KEY` ,注册地址: https://dev.qweather.com/ 4 | 5 | 使用方法: 6 | 7 | 地名+时间+“天气预报” 8 | 9 | 或 10 | 11 | 地名+时间+“详细天气预报” 12 | 13 | 如: 14 | 15 | 北京明天天气预报 16 | 17 | 北京明天详细天气预报 18 | 19 | 注:`config.py` 的模板文字部分可自行修改,变量名不可修改(前面加$的为变量名,详细参考 [python str Template](https://docs.python.org/3/library/string.html#template-strings) -------------------------------------------------------------------------------- /modules/Weather/__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 | from .utils import text2params, get_weather 13 | from .config import TIME 14 | 15 | 16 | # 插件信息 17 | __name__ = "Weather" 18 | __description__ = "和风天气插件" 19 | __author__ = "Roc" 20 | __usage__ = ( 21 | "发送 地区+时间+\"(详细)天气预报\"即可,如“北京近三天天气预报”或“北京近三天详细天气预报”\n" 22 | f"目前已支持大部分城市,支持的时间包括:{ [*TIME.keys(), *set(TIME.values())] }" 23 | ) 24 | 25 | 26 | saya = Saya.current() 27 | channel = Channel.current() 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], inline_dispatchers=[Kanata([RegexMatch('(.*?)天气预报')])])) 35 | async def group_message_listener(app:GraiaMiraiApplication, message: MessageChain, member: Member, group: Group): 36 | if message.asDisplay() == "天气预报": 37 | reply = f"{__description__}\n使用方法:{__usage__}" 38 | else: 39 | city, time, flag = text2params(message.asDisplay()) 40 | reply = get_weather(city, time, flag) 41 | msg = MessageChain.create([At(member.id), Plain(' ' + reply)]) 42 | try: 43 | await app.sendGroupMessage( 44 | group, msg 45 | ) 46 | except AccountMuted: 47 | pass -------------------------------------------------------------------------------- /modules/Weather/config_demo.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | 3 | # 和风天气配置 4 | # https://dev.qweather.com/ 5 | 6 | 7 | KEY = "1324567890abcdefg" 8 | CITY_URL = "https://geoapi.qweather.com/v2/city/lookup" 9 | WEATHER_URL = "https://devapi.qweather.com/v7/weather/" 10 | TIME = { 11 | "当前": "now", 12 | "现在": "now", 13 | "24小时": "24h", 14 | "今天": "24h", 15 | "明天": "3d", 16 | "后天": "3d", 17 | "近三天": "3d", 18 | "近七天": "7d" 19 | } 20 | MSG_TEMPLATE = { 21 | "now": { 22 | "text": Template( 23 | "当前$city的天气为:\n" 24 | "天气状况:$text\n" 25 | "温度:$temp°C, 相对湿度:$humidity%, 体感温度:$feelsLike°C\n" 26 | "风向:$windDir, 风力:$windScale级, 风速:$windSpeed km/h\n" 27 | "当前小时累计降水量:$precip mm\n" 28 | "气压:$pressure 百帕\n" 29 | "能见度:$vis km\n" 30 | "云量:$cloud%\n" 31 | "观测时间:$obsTime\n" 32 | "数据来源:$sources\n" 33 | "版权:$license" 34 | ), 35 | "textcard": Template( 36 | "暂不支持卡片消息" 37 | ) 38 | }, 39 | "24h": { 40 | "text": Template( 41 | "未来24小时内$city的天气情况为(时间倒序):\n" 42 | "$hourly_data\n" 43 | "数据来源:$sources\n" 44 | "版权:$license" 45 | ), 46 | "textcard": Template( 47 | "暂不支持卡片消息" 48 | ) 49 | }, 50 | "3d": { 51 | "text": Template( 52 | "$time$city的天气情况为:\n" 53 | "$daily_data\n" 54 | "数据来源:$sources\n" 55 | "版权:$license" 56 | ), 57 | "textcard": Template( 58 | "暂不支持卡片消息" 59 | ) 60 | }, 61 | "7d": { 62 | "text": Template( 63 | "$time$city的天气情况为(时间倒序):\n" 64 | "$daily_data\n" 65 | "数据来源:$sources\n" 66 | "版权:$license" 67 | ), 68 | "textcard": Template( 69 | "暂不支持卡片消息" 70 | ) 71 | } 72 | } 73 | HOURLY_DATA_TEMPLATE = Template( 74 | "\n" 75 | "时间: $fxTime\n" 76 | "天气状况:$text\n" 77 | "温度:$temp°C, 相对湿度:$humidity%\n" 78 | "风向:$windDir, 风力:$windScale级, 风速:$windSpeed km/h\n" 79 | "降水概率:$pop, 累计降水量:$precip mm\n" 80 | "气压:$pressure 百帕\n" 81 | "云量:$cloud%\n" 82 | ) 83 | DAILY_DATA_TEMPLATE = Template( 84 | "\n" 85 | "时间: $fxDate\n" 86 | "温度:$tempMax°C/$tempMin°C, 相对湿度:$humidity%\n" 87 | "天气状况:白天$textDay, 夜间$textNight\n" 88 | "风向:白天$windDirDay, 夜间$windDirNight\n" 89 | "风力:白天$windScaleDay级, 夜间$windScaleNight级\n" 90 | "风速:白天$windSpeedDay km/h, 夜间$windSpeedNight km/h\n" 91 | "累计降水量:$precip mm\n" 92 | "气压:$pressure 百帕\n" 93 | "紫外线强度指数:$uvIndex\n" 94 | "能见度:$vis\n" 95 | "云量:$cloud%\n" 96 | "日出:$sunrise, 日落:$sunset\n" 97 | "月升:$moonrise, 月落:$moonset, 月相:$moonPhase\n" 98 | ) 99 | SIMPLE_MSG_TEMPLATE = { 100 | "now": { 101 | "text": Template( 102 | "$city: $text, 温度:$temp°C, 相对湿度:$humidity%, 体感温度:$feelsLike°C\n" 103 | "数据来源:$sources, 版权:$license\n" 104 | "发送地区+时间+\"详细天气\"查看详细天气,如:北京24小时详细天气预报" 105 | ), 106 | "textcard": Template( 107 | "暂不支持卡片消息" 108 | ) 109 | }, 110 | "24h": { 111 | "text": Template( 112 | "未来24小时内$city的天气情况为:\n" 113 | "$hourly_data" 114 | "数据来源:$sources, 版权:$license\n" 115 | "发送地区+时间+\"详细天气\"查看详细天气,如:北京24小时详细天气预报" 116 | ), 117 | "textcard": Template("暂不支持卡片消息") 118 | }, 119 | "3d": { 120 | "text": Template( 121 | "$time$city的天气情况为:\n" 122 | "$daily_data" 123 | "数据来源:$sources, 版权:$license\n" 124 | "发送地区+时间+\"详细天气\"查看详细天气,如:北京24小时详细天气预报" 125 | ), 126 | "textcard": Template("暂不支持卡片消息") 127 | }, 128 | "7d": { 129 | "text": Template( 130 | "$time$city的天气情况为:\n" 131 | "$daily_data" 132 | "数据来源:$sources, 版权:$license\n" 133 | "发送地区+时间+\"详细天气\"查看详细天气,如:北京24小时详细天气预报" 134 | ), 135 | "textcard": Template("暂不支持卡片消息") 136 | } 137 | } 138 | SIMPLE_HOURLY_DATA_TEMPLATE = Template( 139 | "$fxTime: $text, 温度:$temp°C, 相对湿度:$humidity%\n" 140 | ) 141 | SIMPLE_DAILY_DATA_TEMPLATE = Template( 142 | "$fxDate: 白天$textDay, 夜间$textNight, 温度:$tempMax°C/$tempMin°C, 相对湿度:$humidity%\n" 143 | ) 144 | -------------------------------------------------------------------------------- /modules/Weather/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /modules/Weather/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import re 4 | from .config import * 5 | 6 | # 和风天气 7 | 8 | def get_city_id(city: str): 9 | id = '0' 10 | params = { 11 | "key": KEY, 12 | "location": city 13 | } 14 | response = requests.get(CITY_URL, params=params) 15 | if response.status_code == 200: 16 | results = json.loads(response.text) 17 | if results['code'] == '200': 18 | # print(results['location']) 19 | id = results['location'][0]['id'] 20 | city = results['location'][0]['country'] + \ 21 | results['location'][0]['adm1'] 22 | if not results['location'][0]['adm1'].startswith(results['location'][0]['adm2']): 23 | city += results['location'][0]['adm2'] + '市' 24 | if not results['location'][0]['adm2'].startswith(results['location'][0]['name']): 25 | city += results['location'][0]['name'] + '区' 26 | # print(city, id) 27 | return city, id 28 | 29 | 30 | def get_weather(city, time, simple_flag=1, msg_type='text'): 31 | if simple_flag: 32 | msg_template = SIMPLE_MSG_TEMPLATE 33 | hourly_data_template = SIMPLE_HOURLY_DATA_TEMPLATE 34 | daily_data_template = SIMPLE_DAILY_DATA_TEMPLATE 35 | else: 36 | msg_template = MSG_TEMPLATE 37 | hourly_data_template = HOURLY_DATA_TEMPLATE 38 | daily_data_template = DAILY_DATA_TEMPLATE 39 | # 检查并标准化参数 40 | city, city_id = get_city_id(city) 41 | time_s = time 42 | if time in TIME.keys(): 43 | time = TIME[time] 44 | elif time in TIME.values(): 45 | pass 46 | else: 47 | return f"支持的时间:{ ','.join([*TIME.keys(), *set(TIME.values())]) }, 试试换一种说法~" 48 | if city_id == '0': 49 | return f"找不到城市{ city },换一种说法试试呢~" 50 | params = { 51 | "key": KEY, 52 | "location": city_id 53 | } 54 | 55 | response = requests.get(WEATHER_URL + time, params=params) 56 | if response.status_code == 200: 57 | results = json.loads(response.text) 58 | # print(results) 59 | if results['code'] == '200': 60 | # 根据查询天气的不同构建不同的数据结构 61 | if time == 'now': # 当前天气 62 | results['now']['obsTime'] = results['now']['obsTime'][11:16] 63 | data = { 64 | "city": city, 65 | **results['now'] 66 | } 67 | # print(data) 68 | elif time in ["24h"]: # 逐小时天气, 72h, 168h 需要商业版 69 | data = { 70 | "city": city, 71 | "hourly_data": "" 72 | } 73 | for d in results['hourly']: 74 | d['fxTime'] = d['fxTime'][11:16] 75 | d['pop'] = d['pop'] + "%" if d['pop'] else "不详" 76 | if simple_flag: 77 | data['hourly_data'] += hourly_data_template.substitute(d) 78 | else: 79 | data['hourly_data'] = hourly_data_template.substitute(d) + data['hourly_data'] 80 | # print(data) 81 | elif time in ["3d", "7d"]: 82 | data = { 83 | "city": city, 84 | "time": time_s, 85 | "daily_data": "" 86 | } 87 | time2index = { 88 | "今天": 0, 89 | "明天": 1, 90 | "后天": 2 91 | } 92 | if time_s in time2index.keys(): 93 | d = results['daily'][time2index[time_s]] 94 | d['fxDate'] = d['fxDate'][5:10] 95 | if simple_flag: 96 | data['daily_data'] += daily_data_template.substitute(d) 97 | else: 98 | data['daily_data'] = daily_data_template.substitute(d) + data['daily_data'] 99 | else: 100 | for d in results['daily']: 101 | d['fxDate'] = d['fxDate'][5:10] 102 | if simple_flag: 103 | data['daily_data'] += daily_data_template.substitute(d) 104 | else: 105 | data['daily_data'] = daily_data_template.substitute(d) + data['daily_data'] 106 | # print(data) 107 | else: # 不支持的查询 108 | return 'time error' 109 | 110 | # 添加来源和版权 111 | if len(results['refer']['sources']) > 0: 112 | data["sources"] = ','.join(results['refer']['sources']) 113 | else: 114 | data['sources'] = '未知来源' 115 | if len(results['refer']['license']) > 0: 116 | data["license"] = ','.join(results['refer']['license']) 117 | else: 118 | data['license'] = '未知版权' 119 | 120 | # 生成msg文本 121 | if time in msg_template.keys(): 122 | if msg_type in msg_template[time].keys(): 123 | msg = msg_template[time][msg_type].substitute(data) 124 | else: 125 | msg = 'msg_type error' 126 | else: 127 | msg = 'time error' 128 | return msg 129 | else: 130 | return "请求错误" + results['code'] 131 | else: 132 | return "网络错误" 133 | 134 | 135 | def text2params(text:str): 136 | city, time, flag = ('', '', 1) 137 | if '详细' in text: 138 | flag = 0 139 | res = re.match(f"(.*?)({ '|'.join([*TIME.keys(), *set(TIME.values())]) })(详细|)天气预报", text, re.I) 140 | if res: 141 | city = res.group(1) 142 | time = res.group(2) 143 | print(city, time, flag) 144 | return city, time, flag 145 | 146 | 147 | if __name__ == "__main__": 148 | city, time, flag = text2params("北京近七天天气预报") 149 | print(get_weather(city, time, flag)) -------------------------------------------------------------------------------- /modules/WeiboHotSearch.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from graia.application.message.elements.internal import Plain 4 | from utils import messagechain_to_img 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 | # 插件信息 15 | __name__ = "WeiboHotSearch" 16 | __description__ = "获取当前微博热搜" 17 | __author__ = "SAGIRI-kawaii" 18 | __usage__ = "在群内发送 微博 即可" 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(listening_events=[GroupMessage], inline_dispatchers=[Kanata([FullMatch('微博')])])) 29 | async def group_message_listener(app: GraiaMiraiApplication, group: Group): 30 | try: 31 | await app.sendGroupMessage( 32 | group, 33 | await get_weibo_hot() 34 | ) 35 | except AccountMuted: 36 | pass 37 | 38 | 39 | async def get_weibo_hot(display: str = "img") -> MessageChain: 40 | url = "http://api.weibo.cn/2/guest/search/hot/word" 41 | async with aiohttp.ClientSession() as session: 42 | async with session.get(url=url) as resp: 43 | data = await resp.json() 44 | data = data["data"] 45 | text_list = ["微博实时热榜:"] 46 | index = 0 47 | for i in data: 48 | index += 1 49 | text_list.append("\n%d. %s" % (index, i["word"].strip())) 50 | text = "".join(text_list).replace("#", "") 51 | msg = MessageChain.create([ 52 | Plain(text=text) 53 | ]) 54 | if display == "img": 55 | return await messagechain_to_img(msg) 56 | elif display == "text": 57 | return msg 58 | else: 59 | raise ValueError("Invalid display value!") 60 | -------------------------------------------------------------------------------- /modules/WyySongOrderer/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.application.message.parser.kanata import Kanata 2 | from graia.application.message.parser.signature import RegexMatch 3 | from graia.application.event.messages import GroupMessage, Group 4 | from graia.saya.builtins.broadcast.schema import ListenerSchema 5 | from graia.saya import Saya, Channel 6 | from graia.application.exceptions import AccountMuted 7 | 8 | from .utils import * 9 | 10 | # 插件信息 11 | __name__ = "WyySongOrderer" 12 | __description__ = "一个(全损音质x)网易云源的点歌插件" 13 | __author__ = "SAGIRI-kawaii" 14 | __usage__ = "在群中发送 `点歌 歌名` 即可" 15 | 16 | saya = Saya.current() 17 | channel = Channel.current() 18 | 19 | 20 | @channel.use(ListenerSchema( 21 | listening_events=[GroupMessage], 22 | inline_dispatchers=[Kanata([RegexMatch("点歌 .*")])] 23 | )) 24 | async def wyy_song_order(app: GraiaMiraiApplication, message: MessageChain, group: Group): 25 | if keyword := message.asDisplay()[3:].strip(): 26 | try: 27 | await app.sendGroupMessage(group, await get_song_ordered(keyword, app)) 28 | except AccountMuted: 29 | pass 30 | else: 31 | try: 32 | await app.sendGroupMessage(group, MessageChain.create([Plain(text="你要告诉我你要搜索什么歌呐~")])) 33 | except AccountMuted: 34 | pass 35 | -------------------------------------------------------------------------------- /modules/WyySongOrderer/silk_v3_encoder.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAGIRI-kawaii/saya_plugins_collection/715c45f3ccc66ae96e87900198e2d45fb6ffdec1/modules/WyySongOrderer/silk_v3_encoder.exe -------------------------------------------------------------------------------- /modules/WyySongOrderer/utils.py: -------------------------------------------------------------------------------- 1 | import aiofiles 2 | import asyncio 3 | import traceback 4 | import aiohttp 5 | import json 6 | from pydub import AudioSegment 7 | import jpype 8 | from jpype import * 9 | 10 | from graia.application import GraiaMiraiApplication 11 | from graia.application.message.chain import MessageChain 12 | from graia.application.message.elements.internal import Plain 13 | 14 | 15 | async def get_song_ordered(keyword: str, app: GraiaMiraiApplication) -> MessageChain: 16 | """ 17 | Search song from CloudMusic 18 | 19 | Args: 20 | keyword: Keyword to search 21 | 22 | Examples: 23 | message = await get_song_ordered("lemon") 24 | 25 | Return: 26 | MessageChain: Message to be send 27 | """ 28 | song_search_url = "http://music.163.com/api/search/get/web?csrf_token=hlpretag=&hlposttag=&s={" \ 29 | "%s}&type=1&offset=0&total=true&limit=1" % keyword 30 | 31 | async with aiohttp.ClientSession() as session: 32 | async with session.get(url=song_search_url) as resp: 33 | data_json = await resp.read() 34 | data_json = json.loads(data_json) 35 | 36 | if data_json["code"] != 200: 37 | return MessageChain.create([Plain(text=f"服务器返回错误:{data_json['message']}")]) 38 | 39 | if data_json["result"]["songCount"] == 0: 40 | return MessageChain.create([Plain(text="没有搜索到呐~换一首歌试试吧!")]) 41 | 42 | song_id = data_json["result"]["songs"][0]["id"] 43 | 44 | music_url = f"http://music.163.com/song/media/outer/url?id={song_id}" 45 | 46 | async with aiohttp.ClientSession() as session: 47 | async with session.get(url=music_url) as resp: 48 | music_bytes = await resp.read() 49 | 50 | music_bytes = await silk(music_bytes, 'b', '-ss 0 -t 120') 51 | 52 | upload_resp = await app.uploadVoice(music_bytes) 53 | 54 | return MessageChain.create([upload_resp]) 55 | 56 | 57 | def silk4j_java(): 58 | jarPath = "./silk4j-1.0.jar" 59 | jvmPath = jpype.getDefaultJVMPath() 60 | jpype.startJVM(jvmPath, "-ea", f"-Djava.class.path={jarPath}") 61 | audio_utils_class = JClass("io.github.mzdluo123.silk4j.AudioUtils") 62 | util = audio_utils_class() 63 | util.init() 64 | file = java.io.File("./cache.mp3") 65 | silk_file = util.mp3ToSilk(file) 66 | output_file = java.io.File("./cache.slk") 67 | file_input_stream = java.io.FileInputStream(silk_file) 68 | # buffer = jpype.JArray(tp=jpype.JByte) 69 | # while bytes_read := file_input_stream.read(buffer, 0, 1024): 70 | # print(bytes_read) 71 | # output_file.write(buffer, 0, bytes_read) 72 | 73 | util.streamToTempFile(file_input_stream, output_file) 74 | 75 | output_file.close() 76 | file_input_stream.close() 77 | 78 | jpype.shutdownJVM() 79 | 80 | 81 | async def silk(data, mtype='b', options=''): 82 | try: 83 | cache_files = ['./modules/WyySongOrderer/cache.wav'] 84 | 85 | if mtype == 'f': 86 | file = data 87 | elif mtype == 'b': 88 | async with aiofiles.open('./modules/WyySongOrderer/music_cache', 'wb') as f: 89 | await f.write(data) 90 | file = './modules/WyySongOrderer/music_cache' 91 | cache_files.append(file) 92 | else: 93 | raise ValueError("Not fit music_type. only 'f' and 'b'") 94 | 95 | cmd = [ 96 | f'ffmpeg -i "{file}" {options} -af aresample=resampler=soxr -ar 24000 -ac 1 -y -loglevel error "./modules/WyySongOrderer/cache.wav"', 97 | f'"./modules/WyySongOrderer/silk_v3_encoder.exe" "./modules/WyySongOrderer/cache.wav" "./modules/WyySongOrderer/cache.slk" -quiet -tencent' 98 | ] 99 | 100 | for p in cmd: 101 | shell = await asyncio.create_subprocess_shell(p) 102 | await shell.wait() 103 | 104 | async with aiofiles.open(f'./modules/WyySongOrderer/cache.slk', 'rb') as f: 105 | b = await f.read() 106 | return b 107 | except Exception: 108 | traceback.print_exc() 109 | 110 | silk4j_java() -------------------------------------------------------------------------------- /modules/ZhihuHotSearch.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from graia.application.message.elements.internal import Plain 4 | from utils import messagechain_to_img 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 | # 插件信息 15 | __name__ = "ZhihuHotSearch" 16 | __description__ = "获取当前知乎热搜" 17 | __author__ = "SAGIRI-kawaii" 18 | __usage__ = "在群内发送 知乎 即可" 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(listening_events=[GroupMessage], inline_dispatchers=[Kanata([FullMatch('知乎')])])) 29 | async def group_message_listener(app: GraiaMiraiApplication, group: Group): 30 | try: 31 | await app.sendGroupMessage( 32 | group, 33 | await get_zhihu_hot() 34 | ) 35 | except AccountMuted: 36 | pass 37 | 38 | 39 | async def get_zhihu_hot() -> MessageChain: 40 | zhihu_hot_url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50&desktop=true" 41 | async with aiohttp.ClientSession() as session: 42 | async with session.get(url=zhihu_hot_url) as resp: 43 | data = await resp.json() 44 | print(data) 45 | data = data["data"] 46 | text_list = ["知乎实时热榜:"] 47 | index = 0 48 | for i in data: 49 | index += 1 50 | text_list.append("\n%d. %s" % (index, i["target"]["title"])) 51 | text = "".join(text_list).replace("#", "") 52 | return await messagechain_to_img(MessageChain.create([Plain(text=text)])) 53 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Saya, Channel 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.saya.event import SayaModuleInstalled 4 | 5 | saya = Saya.current() 6 | channel = Channel.current() 7 | 8 | 9 | @channel.use(ListenerSchema( 10 | listening_events=[SayaModuleInstalled] 11 | )) 12 | async def module_listener(event: SayaModuleInstalled): 13 | print(f"{event.module}::模块加载成功!!!") 14 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import os 4 | from io import BytesIO 5 | from PIL import Image as IMG 6 | from PIL import ImageDraw, ImageFont 7 | 8 | from graia.application.message.elements.internal import MessageChain 9 | from graia.application.message.elements.internal import Image_LocalFile 10 | from graia.application.message.elements.internal import Image_UnsafeBytes 11 | from graia.application.message.elements.internal import Plain 12 | from graia.application.message.elements.internal import Image 13 | from graia.application.message.elements import Element 14 | 15 | 16 | def load_config(config_file: str = "config.json") -> dict: 17 | necessary_parameters = ["miraiHost", "authKey", "BotQQ"] 18 | with open(config_file, 'r', encoding='utf-8') as f: # 从json读配置 19 | config = json.loads(f.read()) 20 | for key in config.keys(): 21 | config[key] = config[key].strip() if isinstance(config[key], str) else config[key] 22 | if any(parameter not in config for parameter in necessary_parameters): 23 | raise ValueError(f"{config_file} Missing necessary parameters! (miraiHost, authKey, BotQQ)") 24 | else: 25 | return config 26 | 27 | 28 | async def get_final_text_lines(text: str, text_width: int, font: ImageFont.FreeTypeFont) -> int: 29 | lines = text.split("\n") 30 | line_count = 0 31 | for line in lines: 32 | if not line: 33 | line_count += 1 34 | continue 35 | line_count += int(math.ceil(float(font.getsize(line)[0]) / float(text_width))) 36 | # print("lines: ", line_count + 1) 37 | return line_count + 1 38 | 39 | 40 | async def messagechain_to_img( 41 | message: MessageChain, 42 | max_width: int = 1080, 43 | font_size: int = 40, 44 | spacing: int = 15, 45 | padding_x: int = 20, 46 | padding_y: int = 15, 47 | img_fixed: bool = False, 48 | font_path: str = "./simhei.ttf", 49 | save_path: str = "./temp/tempMessageChainToImg.png" 50 | ) -> MessageChain: 51 | """ 52 | 将 MessageChain 转换为图片,仅支持只含有本地图片/文本的 MessageChain 53 | 54 | Args: 55 | message: 要转换的MessageChain 56 | max_width: 最大长度 57 | font_size: 字体尺寸 58 | spacing: 行间距 59 | padding_x: x轴距离边框大小 60 | padding_y: y轴距离边框大小 61 | img_fixed: 图片是否适应大小(仅适用于图片小于最大长度时) 62 | font_path: 字体文件路径 63 | save_path: 图片存储路径 64 | 65 | Examples: 66 | msg = await messagechain_to_img(message=message) 67 | 68 | Returns: 69 | MessageChain (内含图片Image类) 70 | """ 71 | if not os.path.exists("temp"): 72 | os.mkdir("temp") 73 | font = ImageFont.truetype(font_path, font_size, encoding="utf-8") 74 | message = message.asMerged() 75 | elements = message.__root__ 76 | 77 | plains = message.get(Plain) 78 | text_gather = "\n".join([plain.text for plain in plains]) 79 | # print(max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x) 80 | final_width = min(max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x, max_width) 81 | text_width = final_width - 2 * padding_x 82 | text_height = (font_size + spacing) * await get_final_text_lines(text_gather, text_width, font) 83 | 84 | img_height_sum = 0 85 | temp_img_list = [] 86 | images = message.get(Image_LocalFile) 87 | for image in images: 88 | if isinstance(image, Image_LocalFile): 89 | temp_img = IMG.open(image.filepath) 90 | img_width, img_height = temp_img.size 91 | temp_img_list.append( 92 | temp_img := temp_img.resize( 93 | ( 94 | int(final_width - 2 * spacing), 95 | int(float(img_height * (final_width - 2 * spacing)) / float(img_width)) 96 | ) 97 | ) if img_width > final_width - 2 * spacing or (img_fixed and img_width < final_width - 2 * spacing) 98 | else temp_img 99 | ) 100 | img_height_sum = img_height_sum + temp_img.size[1] 101 | # elif isinstance(image, Image_UnsafeBytes): 102 | # temp_img = IMG.open(BytesIO(image.image_bytes)) 103 | else: 104 | raise Exception("messagechain_to_img:仅支持本地图片即Image_LocalFile类的处理!") 105 | final_height = 2 * padding_y + text_height + img_height_sum 106 | picture = IMG.new('RGB', (final_width, final_height), (255, 255, 255)) 107 | draw = ImageDraw.Draw(picture) 108 | present_x = padding_x 109 | present_y = padding_y 110 | image_index = 0 111 | for element in elements: 112 | if isinstance(element, Image_LocalFile): 113 | # print(f"adding img {image_index}") 114 | picture.paste(temp_img_list[image_index], (present_x, present_y)) 115 | present_y += (spacing + temp_img_list[image_index].size[1]) 116 | image_index += 1 117 | elif isinstance(element, Plain): 118 | # print(f"adding text '{element.text}'") 119 | for char in element.text: 120 | if char == "\n": 121 | present_y += (font_size + spacing) 122 | present_x = padding_x 123 | continue 124 | if char == "\r": 125 | continue 126 | if present_x + font.getsize(char)[0] > text_width: 127 | present_y += (font_size + spacing) 128 | present_x = padding_x 129 | draw.text((present_x, present_y), char, font=font, fill=(0, 0, 0)) 130 | present_x += font.getsize(char)[0] 131 | present_y += (font_size + spacing) 132 | present_x = padding_x 133 | 134 | picture.save(save_path) 135 | print(f"process finished! Image saved at {save_path}") 136 | return MessageChain.create([ 137 | Image.fromLocalFile(save_path) 138 | ]) 139 | 140 | 141 | class MessageChainTools: 142 | @staticmethod 143 | def element_only(message: MessageChain, element_class: Element) -> bool: 144 | return all(type(element) is element_class for element in message.__root__) --------------------------------------------------------------------------------