├── .gitignore ├── LICENSE ├── README.md ├── nonebot-plugin-describeinstances ├── __init__.py ├── data.py └── util.py ├── nonebot-plugin-epic_reminder ├── __init__.py ├── draw_image.py └── util.py ├── nonebot-plugin-ocr └── __init__.py ├── nonebot-plugin-outdated_image ├── __init__.py └── data.py ├── nonebot-plugin-setu ├── README.md ├── __init__.py ├── config.py ├── data.py ├── lolicon_api.py ├── route.py ├── util.py └── validation.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dihe Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | nonebot 3 |

4 | 5 |
6 | 7 | # nonebot2-plugins 8 | 9 | _✨ ~~女生自用~~ nonebot2 插件。 ✨_ 10 | 11 |
12 | 13 |

14 | 15 | license 16 | 17 | 18 | pypi 19 | 20 | python 21 |

22 | 23 | 24 | ## 目前功能 25 | 26 | - nonebot-plugin-describeinstances 实体信息查询 27 | 28 | > 接口来自 [腾讯云 自然语言处理 实体信息查询](https://cloud.tencent.com/document/api/271/39420) , 需自行前往 [腾讯云 API 密钥管理](https://console.cloud.tencent.com/capi) 新建密钥 29 | > 30 | > 并在 nonebot 的配置文件 `.env.{environment}` 填入 `secret_id` 和 `secret_key` 。 31 | 32 | - nonebot-plugin-ocr ocr文字识别 33 | 34 | - nonebot-plugin-outdated_image 火星图统计 35 | 36 | > 注意事项请见 [注释](https://github.com/Chendihe4975/nonebot2-plugins/blob/master/nonebot-plugin-outdated_image/__init__.py#L44) 。 37 | 38 | - nonebot-plugin-epic_reminder Epic 白嫖提醒小助手 39 | 40 | > Linux 玩家需自行将微软雅黑的字体文件 `msyh.ttc` 复制到插件目录下 41 | > 42 | > 也可使用其它字体, 见 [draw_image.py](https://github.com/Chendihe4975/nonebot2-plugins/blob/master/nonebot-plugin-epic_reminder/draw_image.py#L27)。 43 | > 44 | > Pillow >= 8.2.0 45 | 46 | - nonebot-plugin-setu 群内活跃气氛小助手 47 | 48 | > 接口来自 [Lolicon API](https://api.lolicon.app/) , 主要功能: 发涩图, 附带功能: 反向代理 i.pximg.net 。 49 | > 50 | > 配置项较为复杂, 见 [README](https://github.com/Chendihe4975/nonebot2-plugins/tree/master/nonebot-plugin-setu) 。 51 | > 52 | > 附带功能需开放公网访问, 可能会有憨批拿爬虫搁那 CC, 服务器上行被占满之后 websocket 连接可能会断。 53 | > 54 | > 可使用 反向代理 / CDN 增强安全性, 公用 Bot 可以考虑放弃该附带功能。 55 | 56 | ## 如何使用 57 | 58 | 下载你中意的插件, 按常规方法添加到你的 nonebot2 机器人上。 59 | 60 | ## 特别感谢 61 | 62 | - [nonebot / nonebot2](https://github.com/nonebot/nonebot2) 63 | - [Mrs4s / go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 64 | 65 | ## 优化建议 66 | 67 | 如有优化建议请积极提交 Issues 或 Pull requests。 68 | 69 | 如果你要提交 Pull requests, 请确保你的代码风格和项目已有的代码保持一致。 70 | 71 | ![](https://i.loli.net/2021/08/23/5Je1CzgoGmqAI3V.jpg) -------------------------------------------------------------------------------- /nonebot-plugin-describeinstances/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-23 15:06:44 4 | - LastEditTime: 2021-09-27 00:27:47 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from re import match 10 | 11 | from loguru import logger 12 | from nonebot.plugin import on_regex 13 | from nonebot.adapters.cqhttp.bot import Bot 14 | from nonebot.adapters.cqhttp.event import Event, MessageEvent 15 | from nonebot.typing import T_State 16 | from nonebot.exception import ActionFailed 17 | 18 | from .data import DescribeInstances 19 | from .util import fetch_data, processing_data 20 | 21 | nlp = on_regex(r'^(你?知道)?(.{1,32}?)是(什么|谁|啥)吗?[??]?$', priority=5, block=False) 22 | 23 | 24 | @nlp.handle() 25 | async def _(bot: Bot, event: Event, state: T_State): 26 | if isinstance(event, MessageEvent): 27 | if match(r'[这那谁你我他她它]个?是[(什么)谁啥]', event.raw_message): 28 | return 29 | instances = state["_matched_groups"][1] 30 | result = DescribeInstances.get_or_none(instances=instances) 31 | if result: 32 | msg = str(result.describe).replace("|@|", "\n|@|") 33 | else: 34 | msg = processing_data(await fetch_data(instances=instances)).replace("|@|", "\n|@|") 35 | try: 36 | await nlp.finish("\n".join([f"> {event.sender.card or event.sender.nickname}", 37 | msg])) 38 | except ActionFailed as e: 39 | logger.exception( 40 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}' 41 | ) 42 | return 43 | else: 44 | logger.warning("Not supported: DescribeInstances.") 45 | return 46 | -------------------------------------------------------------------------------- /nonebot-plugin-describeinstances/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-23 09:59:12 4 | - LastEditTime: 2021-08-26 05:31:48 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | import peewee as pw 10 | from os import path 11 | 12 | db_path = path.abspath( 13 | path.join(path.dirname(__file__), "describeinstances.db")) 14 | db = pw.SqliteDatabase(db_path) 15 | 16 | 17 | class BaseModel(pw.Model): 18 | class Meta: 19 | database = db 20 | 21 | 22 | class DescribeInstances(BaseModel): 23 | """ 24 | - `instances` 实例 25 | - `instances_en` 实例英文名 26 | - `describe` 实例描述 27 | """ 28 | instances = pw.TextField() 29 | instances_en = pw.TextField() 30 | describe = pw.TextField() 31 | 32 | class Meta: 33 | primary_key = pw.CompositeKey("instances", "instances_en") 34 | 35 | 36 | if not path.exists(db_path): 37 | db.connect() 38 | db.create_tables([DescribeInstances]) 39 | db.close() 40 | -------------------------------------------------------------------------------- /nonebot-plugin-describeinstances/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-23 09:59:04 4 | - LastEditTime: 2021-08-26 05:45:50 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from base64 import b64encode 10 | from hashlib import sha1 11 | from hmac import new 12 | from random import randint 13 | from time import time 14 | from typing import Any, Dict, Union 15 | 16 | from aiohttp import ClientSession 17 | from loguru import logger 18 | from nonebot import get_driver 19 | 20 | try: 21 | import ujson as json 22 | except: 23 | import json 24 | 25 | from .data import DescribeInstances 26 | 27 | config = get_driver().config 28 | 29 | 30 | def get_signature(params: dict) -> str: 31 | sign_str = "GET" + "nlp.tencentcloudapi.com/" + "?" + \ 32 | "&".join("%s=%s" % (k, params[k]) for k in sorted(params)) 33 | hashed = new(bytes(config.secret_key, "utf-8"), 34 | bytes(sign_str, "utf-8"), sha1).digest() 35 | signature = b64encode(hashed) 36 | return signature.decode() 37 | 38 | 39 | async def fetch_data(instances: str) -> Union[str, Dict]: 40 | params = { 41 | "Action": "DescribeEntity", 42 | "Version": "2019-04-08", 43 | "Region": "ap-guangzhou", 44 | "EntityName": instances, 45 | "Timestamp": int(time()), 46 | "Nonce": randint(1, int(1e9)), 47 | "SecretId": config.secret_id, 48 | } 49 | params["Signature"] = get_signature(params) 50 | try: 51 | async with ClientSession() as session: 52 | async with session.get("https://nlp.tencentcloudapi.com/", params=params, verify_ssl=False) as resp: 53 | if "Error" in (data := await resp.json())["Response"]: 54 | return data["Response"]["Error"]["Message"] 55 | return json.loads(data["Response"]["Content"]) 56 | except Exception as e: 57 | logger.exception(e) 58 | return str(e) 59 | 60 | 61 | def processing_data(data: Any) -> str: 62 | if isinstance(data, dict): 63 | DescribeInstances.replace( 64 | instances=data["名称"][0]["Name"], 65 | instances_en=data["英文名"][0]["Name"] if "英文名" in data else data["Name"][0]["Name"], 66 | describe=data["简介"][0]["Name"] if "简介" in data else data["Foundin"][0]["Name"] 67 | ).execute() 68 | data = data["简介"][0]["Name"] if "简介" in data else data["Foundin"][0]["Name"] 69 | return str(data) 70 | -------------------------------------------------------------------------------- /nonebot-plugin-epic_reminder/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-27 08:55:35 4 | - LastEditTime: 2021-08-30 11:01:29 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from time import time 10 | 11 | from loguru import logger 12 | from nonebot import require, get_driver 13 | from nonebot.adapters.cqhttp.bot import Bot 14 | from nonebot.adapters.cqhttp.event import Event, MessageEvent 15 | from nonebot.adapters.cqhttp.message import MessageSegment 16 | from nonebot.exception import ActionFailed 17 | from nonebot.permission import SUPERUSER 18 | from nonebot.plugin import MatcherGroup 19 | from nonebot.typing import T_State 20 | 21 | from .draw_image import GenerateImage 22 | from .util import date2time_stamp, fetch_data, get_data, startup_hook 23 | 24 | driver = get_driver() 25 | scheduler = require("nonebot_plugin_apscheduler").scheduler 26 | driver.on_startup(startup_hook) 27 | scheduler.add_job(fetch_data, "cron", day_of_week="thu", hour=23, minute=1) 28 | 29 | matchers = MatcherGroup() 30 | query_free_game = matchers.on_regex( 31 | r"^(epic)?(.{2})?免费游戏$", priority=5, block=True) 32 | force_update = matchers.on_command( 33 | "epic数据更新", permission=SUPERUSER, priority=1, block=True) 34 | 35 | 36 | @query_free_game.handle() 37 | async def _(bot: Bot, event: Event, state: T_State): 38 | if isinstance(event, MessageEvent): 39 | if state["_matched_groups"][1] in ["本期", "本周"]: 40 | data = [i for i in get_data() if time() > 41 | date2time_stamp(i["start_date"])] 42 | elif state["_matched_groups"][1] in ["下期", "下周"]: 43 | data = [i for i in get_data() if time() < 44 | date2time_stamp(i["start_date"])] 45 | else: 46 | data = get_data() 47 | async with GenerateImage(data) as gi: 48 | reply = MessageSegment.image(await gi.generate_image()) 49 | await query_free_game.finish(MessageSegment.reply(event.message_id) + reply) 50 | else: 51 | logger.warning("Not supported: epic reminder.") 52 | return 53 | 54 | 55 | @force_update.handle() 56 | async def _(bot: Bot, event: Event): 57 | if isinstance(event, MessageEvent): 58 | await fetch_data() 59 | try: 60 | await force_update.finish("\n".join([ 61 | f"> {event.sender.card or event.sender.nickname}", 62 | "已执行更新。" 63 | ])) 64 | except ActionFailed as e: 65 | logger.exception( 66 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}' 67 | ) 68 | return 69 | else: 70 | logger.warning("Not supported: epic reminder.") 71 | return 72 | -------------------------------------------------------------------------------- /nonebot-plugin-epic_reminder/draw_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-27 08:55:31 4 | - LastEditTime : 2021-11-15 18:47:15 5 | - LastEditors : DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/DiheChen 8 | """ 9 | from base64 import b64encode 10 | from io import BytesIO 11 | from os import path 12 | from time import time 13 | from typing import Generator, List 14 | 15 | from aiohttp import ClientSession 16 | from PIL import Image, ImageDraw, ImageFont 17 | 18 | from .util import date2time_stamp 19 | 20 | 21 | class GenerateImage: 22 | def __init__(self, data: list) -> None: 23 | self._data = data 24 | self._urls = [i["url"] for i in data] 25 | self._session = ClientSession() 26 | self._font_path = path.abspath( 27 | path.join(path.dirname(__file__), "msyh.ttc")) 28 | 29 | async def _get(self) -> Generator[Image.Image, None, None]: 30 | for url in self._urls: 31 | async with self._session.get(url, verify_ssl=False) as resp: 32 | yield Image.open(BytesIO(await resp.read())).convert("RGBA").resize((360, 480)) 33 | 34 | async def __aenter__(self) -> "GenerateImage": 35 | self.images: List[Image.Image] = list() 36 | async for image in self._get(): 37 | self.images.append(image) 38 | return self 39 | 40 | @staticmethod 41 | def date2str(date: str) -> str: 42 | return date[0:4] + "年" + date[5:7] + "月" + date[8:10] + "日" 43 | 44 | @staticmethod 45 | def image2b64(image: Image.Image) -> str: 46 | buf = BytesIO() 47 | image.save(buf, format="PNG") 48 | base64_str = b64encode(buf.getvalue()).decode() 49 | return "base64://" + base64_str 50 | 51 | def draw_text(self, image: Image.Image, text: str, font_size: int, xy=(0, 0), 52 | color=(255, 255, 255, 255)) -> Image.Image: 53 | font = ImageFont.truetype(self._font_path, font_size) 54 | new_image = Image.new("RGBA", image.size, (255, 255, 255, 0)) 55 | ImageDraw.Draw(new_image).text( 56 | xy, text=text, font=font, fill=color, anchor="lt") 57 | return Image.alpha_composite(image, new_image) 58 | 59 | async def generate_image(self) -> str: 60 | image = Image.new( 61 | "RGBA", (len(self.images) * 480 - abs(len(self.images) - 1 * 40) if len(self._data) 62 | > 1 else 460, 720), 63 | (18, 18, 18, 255)) 64 | image = self.draw_text( 65 | image, "免费游戏", 30, (60, 30), (255, 255, 255, 255)) 66 | banner_draw = ImageDraw.ImageDraw( 67 | blue_banner := Image.new("RGBA", (360, 40))) 68 | for count, game_cover in enumerate(self.images): 69 | banner_draw.rounded_rectangle((0, 0, 360, 40), 10, fill=( 70 | 63, 72, 204) if time() > date2time_stamp(self._data[count]["start_date"]) else (0, 0, 0)) 71 | game_cover.paste(blue_banner.crop((0, 5, 360, 40)), (0, 445)) 72 | image.alpha_composite(game_cover, (count * 440 + 60, 80)) 73 | image = self.draw_text(image, "当前免费" if time() > date2time_stamp( 74 | self._data[count]["start_date"]) else "即将推出", 20, (count * 440 + 205, 535)) 75 | image = self.draw_text( 76 | image, self._data[count]["title"], 20, (count * 440 + 60, 585)) 77 | image = self.draw_text( 78 | image, "原价" + " " * 9 + self._data[count]["original_price"], 20, (count * 440 + 60, 620)) 79 | image = self.draw_text( 80 | image, "开始时间: " + 81 | GenerateImage.date2str(self._data[count]["start_date"]), 82 | 20, (count * 440 + 60, 655)) 83 | image = self.draw_text( 84 | image, "截止时间: " + 85 | GenerateImage.date2str(self._data[count]["end_date"]), 86 | 20, (count * 440 + 60, 680)) 87 | return GenerateImage.image2b64(image) 88 | 89 | async def __aexit__(self, *args): 90 | await self._session.close() 91 | -------------------------------------------------------------------------------- /nonebot-plugin-epic_reminder/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-27 08:56:04 4 | - LastEditTime: 2021-09-24 01:40:53 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from os import path 10 | from time import mktime, strptime 11 | from typing import Any, Dict, List, Optional 12 | 13 | from aiohttp import ClientSession 14 | from loguru import logger 15 | 16 | try: 17 | import ujson as json 18 | except ImportError: 19 | import json 20 | 21 | epic_free_game_data = None 22 | cache_data = path.abspath(path.join(path.dirname(__file__), "cache.json")) 23 | 24 | 25 | async def startup_hook(): 26 | if not path.exists(cache_data): 27 | await fetch_data() 28 | with open(cache_data, "r") as f: 29 | global epic_free_game_data 30 | epic_free_game_data = json.loads(f.read()) 31 | return None 32 | 33 | 34 | def date2time_stamp(date: str): 35 | return mktime(strptime(date.replace("T", " ").replace("Z", ""), "%Y-%m-%d %H:%M:%S.000")) 36 | 37 | 38 | async def fetch_data() -> Optional[str]: 39 | params = { 40 | "locale": "zh-CN", 41 | "country": "CN", 42 | "allowCountries": "CN" 43 | } 44 | try: 45 | async with ClientSession() as session: 46 | async with session.get("https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions", 47 | params=params) as resp: 48 | with open(cache_data, "w") as file: 49 | global epic_free_game_data 50 | file.write(json.dumps(epic_free_game_data := await resp.json(), indent=4)) 51 | return None 52 | except Exception as e: 53 | logger.exception(e) 54 | return str(e) 55 | 56 | 57 | def get_data() -> Optional[List[Dict[str, Any]]]: 58 | if epic_free_game_data: 59 | free_games = [i for i in epic_free_game_data["data"]["Catalog"][ 60 | "searchStore"]["elements"] if i["promotions"]] 61 | result = list() 62 | for game in free_games: 63 | if game["promotions"]["promotionalOffers"]: 64 | extra = { 65 | "start_date": game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0]["startDate"], 66 | "end_date": game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0]["endDate"] 67 | } 68 | else: 69 | extra = { 70 | "start_date": game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0]["startDate"], 71 | "end_date": game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0]["endDate"] 72 | } 73 | result.append( 74 | dict({ 75 | "title": game["title"], 76 | "description": game["description"], 77 | "url": game["keyImages"][0]["url"], 78 | "original_price": game["price"]["totalPrice"]["fmtPrice"]["originalPrice"], 79 | }, **extra)) 80 | return result 81 | -------------------------------------------------------------------------------- /nonebot-plugin-ocr/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-06-29 23:16:20 4 | - LastEditTime: 2021-09-13 22:15:24 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/DiheChen 8 | """ 9 | from loguru import logger 10 | from nonebot.adapters.cqhttp.bot import Bot 11 | from nonebot.adapters.cqhttp.event import Event, MessageEvent 12 | from nonebot.exception import ActionFailed 13 | from nonebot.plugin import on_command 14 | from nonebot.typing import T_State 15 | 16 | ocr = on_command("ocr", priority=5, block=True) 17 | 18 | 19 | @ocr.handle() 20 | async def _(bot: Bot, event: Event, state: T_State): 21 | if isinstance(event, MessageEvent): 22 | if event.raw_message != "ocr": 23 | state["image"] = event.raw_message 24 | else: 25 | logger.warning("Not support: ocr.") 26 | return 27 | 28 | 29 | @ocr.got("image", prompt="请发送图片, 支持多张。") 30 | async def _(bot: Bot, event: Event): 31 | if isinstance(event, MessageEvent): 32 | result = [f"> {event.sender.card or event.sender.nickname}"] 33 | image_list = [seg.data["file"] 34 | for seg in event.message if seg.type == "image" and "file" in seg.data] 35 | for count, image in enumerate(image_list): 36 | texts = (await bot.call_api("ocr_image", image=image))["texts"] 37 | result.append( 38 | f"第 {count + 1} 张图片的识别结果是: {''.join([res['text'] for res in texts])}") 39 | try: 40 | await ocr.finish("\n".join(result)) 41 | except ActionFailed as e: 42 | logger.exception( 43 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}' 44 | ) 45 | return 46 | else: 47 | logger.warning("Not support: ocr.") 48 | return 49 | -------------------------------------------------------------------------------- /nonebot-plugin-outdated_image/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-24 11:32:12 4 | - LastEditTime: 2021-09-13 22:13:43 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/DiheChen 8 | """ 9 | from collections import Counter 10 | from time import localtime, strftime 11 | from typing import Any, Dict, List, Iterable 12 | 13 | from loguru import logger 14 | from nonebot.adapters.cqhttp.bot import Bot 15 | from nonebot.adapters.cqhttp.event import Event, GroupMessageEvent 16 | from nonebot.adapters.cqhttp.message import Message, MessageSegment 17 | from nonebot.exception import ActionFailed 18 | from nonebot.plugin import MatcherGroup 19 | from nonebot.typing import T_State 20 | 21 | from .data import ImageMessage 22 | 23 | 24 | async def _image_in_msg(bot: Bot, event: Event, state: T_State) -> bool: 25 | return isinstance(event, GroupMessageEvent) and (msg.type == "image" for msg in event.message) 26 | 27 | 28 | matchers = MatcherGroup(type="message") 29 | listen = matchers.on_message(rule=_image_in_msg, priority=5, block=False) 30 | query = matchers.on_command("查火星图", aliases={"查询火星图"}, priority=1, block=True) 31 | summary = matchers.on_command("火星排行榜", priority=1, block=True) 32 | 33 | 34 | def format_time(time_stamp: int) -> str: 35 | return strftime("%Y-%m-%d %H:%M:%S", localtime(time_stamp)) 36 | 37 | 38 | def generate_forward_msg(msgs: Iterable, self_id: int) -> List[Dict[str, Any]]: 39 | if isinstance(msgs, (str, Message, MessageSegment)): 40 | msgs = (msgs,) 41 | return [{ 42 | "type": "node", 43 | "data": { 44 | "name": "老婆~", 45 | # ↓ 旧版 go-cqhttp 与 onebot 标准不符, 若使用其它标准 onebot 协议端请修改下面字段为 `user_id` ↓ 46 | "uin": str(self_id), 47 | "content": msg 48 | } 49 | } for msg in msgs] 50 | 51 | 52 | @listen.handle() 53 | async def _(bot: Bot, event: GroupMessageEvent): 54 | if image_list := [seg.data["file"] for seg in event.message if seg.type == "image" and "file" in seg.data]: 55 | for image in image_list: 56 | if len(data := ImageMessage.select().where(ImageMessage.group_id == event.group_id, 57 | ImageMessage.image_md5 == image)) == 5: 58 | first_sender = await bot.get_group_member_info(group_id=event.group_id, user_id=data[0].user_id, 59 | no_cache=True) 60 | try: 61 | await listen.send(MessageSegment.reply(event.message_id) + "\n".join([ 62 | "⚠️ 这张图片可能是火星图!", 63 | f"它最早由 {first_sender['card'] or first_sender['nickname']} ({first_sender['user_id']}) " + 64 | f"在 {format_time(data[0].time_stamp)} 发送。"]) 65 | ) 66 | except ActionFailed as e: 67 | logger.exception( 68 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 69 | return 70 | if result := ImageMessage.get_or_none(user_id=event.user_id, group_id=event.group_id, image_md5=image): 71 | ImageMessage.replace(time_stamp=result.time_stamp, user_id=result.user_id, 72 | group_id=result.group_id, image_md5=result.image_md5, 73 | sent_count=result.sent_count + 1).execute() 74 | else: 75 | ImageMessage.replace(time_stamp=event.time, user_id=event.user_id, 76 | group_id=event.group_id, image_md5=image, sent_count=1).execute() 77 | return 78 | 79 | 80 | @query.handle() 81 | async def _(bot: Bot, event: Event, state: T_State): 82 | if isinstance(event, GroupMessageEvent): 83 | if event.raw_message != "查火星图" and event.raw_message != "查询火星图": 84 | state["image"] = event.message 85 | else: 86 | logger.warning("Not supported: outdated image.") 87 | return 88 | 89 | 90 | @query.got("image", prompt="请发送要查询的图片~") 91 | async def _(bot: Bot, event: Event, state: T_State): 92 | if isinstance(event, GroupMessageEvent): 93 | result = [f"> {event.sender.card or event.sender.nickname}"] 94 | if image_list := [seg.data['file'] for seg in event.message if seg.type == 'image' and 'file' in seg.data]: 95 | for count, img in enumerate(image_list): 96 | if len(data := ImageMessage.select().where(ImageMessage.group_id == event.group_id, 97 | ImageMessage.image_md5 == img)) > 5: 98 | first_sender = await bot.get_group_member_info(group_id=event.group_id, user_id=data[0].user_id, 99 | no_cache=True) 100 | result.append( 101 | f"第 {count + 1} 张图片是火星图, 它最早由 {first_sender['card'] or first_sender['nickname']}" + 102 | f"({first_sender['user_id']}) 在 {format_time(data[0].time_stamp)} 发送。") 103 | else: 104 | result.append(f"第 {count + 1} 张图片不是火星图哦~") 105 | try: 106 | await query.finish("\n".join(result)) 107 | except ActionFailed as e: 108 | logger.exception( 109 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 110 | return 111 | else: 112 | logger.warning("Not supported: outdated image.") 113 | return 114 | 115 | 116 | @summary.handle() 117 | async def _(bot: Bot, event: Event): 118 | if isinstance(event, GroupMessageEvent): 119 | if data := ImageMessage.select().where(ImageMessage.group_id == event.group_id).order_by( 120 | ImageMessage.image_md5): 121 | counter = Counter([d.image_md5 for d in data]) 122 | msgs = list() 123 | rank = 1 124 | for md5, count in dict(sorted(counter.items(), key=lambda x: x[1], reverse=True)[ 125 | :len(counter) if len(counter) < 5 else 5]).items(): 126 | first_sender, time_stamp = [ 127 | (i.user_id, i.time_stamp) for i in data if i.image_md5 == md5][0] 128 | sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=first_sender) 129 | msgs.append(Message("\n".join([ 130 | str(MessageSegment.image(md5)), 131 | f"本群第 {rank} 位火星图", 132 | f"已在本群出现了 {count} 次", 133 | f"由 {sender_info['card'] or sender_info['nickname']} 最早在 {format_time(time_stamp)} 发送。" 134 | ]))) 135 | rank += 1 136 | try: 137 | await bot.send_group_forward_msg(group_id=event.group_id, 138 | messages=generate_forward_msg(msgs, self_id=event.self_id)) 139 | except ActionFailed as e: 140 | logger.exception( 141 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 142 | return 143 | else: 144 | logger.warning("Not supported: outdated image.") 145 | return 146 | -------------------------------------------------------------------------------- /nonebot-plugin-outdated_image/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-24 11:32:30 4 | - LastEditTime: 2021-09-13 22:17:44 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/DiheChen 8 | """ 9 | import peewee as pw 10 | from os import path 11 | 12 | db_path = path.abspath(path.join(path.dirname(__file__), "image_msg.db")) 13 | db = pw.SqliteDatabase(db_path) 14 | 15 | 16 | class BaseModel(pw.Model): 17 | class Meta: 18 | database = db 19 | 20 | 21 | class ImageMessage(BaseModel): 22 | """ 23 | - `time_stamp` 用户第一次发送该图片的时间戳 24 | - `user_id` 用户 qq 号 25 | - `group_id` qq 群号 26 | - `image_md5` 该图片的 md5 值 27 | - `sent_count` 此人发送该图数量 28 | """ 29 | time_stamp = pw.IntegerField() 30 | user_id = pw.IntegerField() 31 | group_id = pw.IntegerField() 32 | image_md5 = pw.TextField() 33 | sent_count = pw.IntegerField() 34 | 35 | class Meta: 36 | primary_key = pw.CompositeKey("user_id", "group_id", "image_md5") 37 | 38 | 39 | if not path.exists(db_path): 40 | db.connect() 41 | db.create_tables([ImageMessage]) 42 | db.close() 43 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/README.md: -------------------------------------------------------------------------------- 1 | # nonebot-plugin-setu 2 | 3 | nonebot-plugin-setu 配置说明。 4 | 5 | ## 配置 6 | 7 | 自定义配置请修改 [config.py #L39](https://github.com/Chendihe4975/nonebot2-plugins/blob/master/nonebot-plugin-setu/config.py#L39), 8 | 9 | - `auto_fetch` : 自动缓存图片 10 | - `global_r18` : 全局 r18 开关, 禁用后 Bot 将不发送 r18 图片 11 | - `recall_time` : 经过多少秒后撤回图片, 设置成 0 时候不自动撤回 12 | - `url_only` : 仅回复 url , 不发送图片 13 | - `online_mode_pure` 纯在线模式, 该模式下 Bot 将不会缓存图片到本地 14 | - `use_forward` : 启用合并转发 15 | - `proxy`: 代理服务器地址, 仅支持 http 代理 16 | - `personalized_recommendation`: 个性化推荐开关 17 | - `public_address` : Bot 对外开放的地址, 若已设置反向代理, 该项应该填反向代理后的地址 18 | - `single_image_limit` : 限制单次发送色图数量 19 | 20 | ### public_address 21 | 22 | 要使用此项, 请在有公网 ip 的服务器上运行你的机器人, 如果你坚持在本地计算机运行, 请使用内网穿透。 23 | 24 | 我们知道 i.pximg.net 具有防盗链措施, 请求头中不含 `"Referer": "https://www.pixiv.net"` 会返回 403, 于是我们准备自己搭建一个反代服务 ( 能自建为什么要用别人的 ? ) 。 25 | 26 | 我们先来打开一张图片 ( SFW ): 27 | 28 | > https://i.pximg.net/img-original/img/2021/02/24/18/16/59/88019201_p0.jpg 29 | 30 | 你会看到大大的 403 Forbidden。 31 | 32 | 我们启动我们的 NoneBot2, 并使该插件被 Bot 加载, 假设我们的 Bot 是运行在 8080 端口上的, 将图片链接中的 `https://i.pximg.net/` 修改为 `http://127.0.0.1:8080/pixiv/` , 得到以下链接 33 | 34 | > http://127.0.0.1:8080/pixiv/img-original/img/2021/02/24/18/16/59/88019201_p0.jpg 35 | 36 | 如果我们在 NoneBot 运行的服务器上访问这个链接, 是可以访问到图片的, 但是我们需要让别人也能通过链接访问到你的反代服务的话, 你需要: 37 | 38 | - 将 NoneBot 监听的 `HOST` 修改为 `0.0.0.0` 39 | - 在服务器的防火墙面板里, 打开 8080 端口 ( 如果你的 Bot 是运行在 8080 端口上的话 ) 40 | - 为你的 OneBot 协议端及 NoneBot 设置 access_token, 防止入侵 41 | 42 | 这样的话, 别人访问以下地址也能得到图片: 43 | 44 | > http://{你服务器的公网 ip}:8080/pixiv/img-original/img/2021/02/24/18/16/59/88019201_p0.jpg 45 | 46 | 如果你这样做, 会直接暴露服务的端口, 无法使用加密通信等, 更建议你使用 Nginx, Apache 之类的 Web Server 设置反代, 47 | 48 | 最后在本插件的配置文件里面修改为 `"public_address": "http://{服务器 ip}:{机器人端口}"` ( 没设置反代的情况下 )或 `"public_address": "https://{你的域名}"` ( 设置了反代的情况下 )。 49 | 50 | ### proxy 51 | 52 | 如果你不能直连 `i.pximg.net`, 你需要借助魔法, 如果你的魔法在 1080 端口上, 请在本插件的配置文件里面填入 `"proxy": "http://127.0.0.1:1080"` 。 53 | 54 | ### online_mode_pure 55 | 56 | 纯在线模式, 设定为 True 后不会讲图片保存到本地, 为了确保该模式可用, 你需要先配置好 `public_address` 57 | 58 | OneBot 协议端将收到以下调用: 59 | 60 | ```json 61 | { 62 | "type": "image", 63 | "data": { 64 | "file": "http://{你服务器的 ip}:{NoneBot 端口}/pixiv/img-original/img/2021/02/24/18/16/59/88019201_p0.jpg" 65 | } 66 | } 67 | ``` 68 | 69 | 或者 70 | 71 | ```json 72 | { 73 | "type": "image", 74 | "data": { 75 | "file": "http://{你的域名}/pixiv/img-original/img/2021/02/24/18/16/59/88019201_p0.jpg" 76 | } 77 | } 78 | ``` 79 | 80 | 本插件不考虑使用 `base64` 发送图片。 81 | 82 | ### url_only 83 | 84 | 仅回复图片 url ( 你反代过的 ), 不发送图片, 极高响应速度, 不用怕 ghs 被封号。 85 | 86 | 你没搞好 `public_address` 别人可能打不开。 87 | 88 | ### single_image_limit 89 | 90 | 限制单次发送色图数量, 默认为 10, 可调整为 1 - 100 之间的任意整数。 91 | 92 | ### auto_fetch 93 | 94 | url_only 及 online_mode_pure 均设定为 False 的时候, 可以设定 auto_fetch 为 1 - 100 任意正整数使机器人每隔半小时自动缓存该数量的图片。 95 | 96 | ### personalized_recommendation 97 | 98 | 个性化推荐, 记录用户喜欢看的色图的 tag, 若此项设为真值, 用户未指定涩图种类时, bot 将有 50 % 的概率为他来点他喜欢的。( 如果他看所有色图里某 tag 出现了 500 次以上, 则认为他喜欢这种涩图。 ) 99 | 100 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-31 19:45:30 4 | - LastEditTime: 2021-09-27 00:23:06 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from asyncio import sleep 10 | from collections import Counter 11 | from os import mkdir, path 12 | from random import choice, random 13 | from typing import Dict, Iterable 14 | 15 | from loguru import logger 16 | from nonebot import get_driver, require 17 | from nonebot.adapters.cqhttp.bot import Bot 18 | from nonebot.adapters.cqhttp.event import (Event, GroupMessageEvent, 19 | MessageEvent, PrivateMessageEvent) 20 | from nonebot.adapters.cqhttp.message import Message, MessageSegment 21 | from nonebot.exception import ActionFailed, FinishedException 22 | from nonebot.plugin import MatcherGroup 23 | from nonebot.typing import T_State 24 | 25 | from . import route 26 | from .config import config 27 | from .data import UserXP 28 | from .lolicon_api import LoliconAPI 29 | from .util import ajax_pixiv, get_user_qq 30 | from .validation import RequestData 31 | 32 | scheduler = require("nonebot_plugin_apscheduler").scheduler 33 | 34 | num_dict = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5, 35 | "六": 6, "七": 7, "八": 8, "九": 9, "十": 10} 36 | 37 | global_config = get_driver().config 38 | 39 | image_cache_path = path.abspath(path.join(path.dirname(__file__), "image")) 40 | if not path.exists(image_cache_path): 41 | mkdir(image_cache_path) 42 | 43 | 44 | async def send_forward_msg(msgs: Iterable, bot: Bot, event: Event): 45 | if isinstance(msgs, (str, Message, MessageSegment)): 46 | msgs = (msgs,) 47 | if isinstance(event, PrivateMessageEvent): 48 | await send_msgs(msgs, bot, event) 49 | elif isinstance(event, GroupMessageEvent): 50 | msgs = [{ 51 | "type": "node", 52 | "data": { 53 | "name": choice(list(global_config.nickname)), 54 | "uin": str(event.self_id), 55 | "content": Message(msg) 56 | } 57 | } for msg in msgs] 58 | try: 59 | msg_id = (await bot.send_group_forward_msg(group_id=event.group_id, messages=msgs))["message_id"] 60 | except ActionFailed as e: 61 | logger.exception( 62 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 63 | return 64 | else: 65 | return 66 | if config.recall_time: 67 | await sleep(config.recall_time) 68 | await bot.delete_msg(message_id=msg_id) 69 | raise FinishedException 70 | 71 | 72 | async def send_msgs(msgs: Iterable, bot: Bot, event: Event): 73 | if isinstance(msgs, (str, Message, MessageSegment)): 74 | msgs = (msgs,) 75 | msg_ids = list() 76 | for msg in msgs: 77 | try: 78 | msg_ids.append((await bot.send(event, Message(msg)))["message_id"]) 79 | except ActionFailed as e: 80 | logger.exception( 81 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 82 | if config.recall_time: 83 | await sleep(config.recall_time) 84 | for msg_id in msg_ids: 85 | await bot.delete_msg(message_id=msg_id) 86 | raise FinishedException 87 | 88 | 89 | async def auto_fetch_setu(): 90 | params = { 91 | "num": config.auto_fetch if config.auto_fetch <= 100 else 100 92 | } 93 | rd = RequestData(**params) 94 | async with LoliconAPI(params=rd) as api: 95 | await api.auto_fetch() 96 | return 97 | 98 | 99 | if not config.url_only and not config.online_mode_pure and config.auto_fetch: 100 | scheduler.add_job(auto_fetch_setu, "interval", minutes=30) 101 | 102 | matchers = MatcherGroup() 103 | call_setu = matchers.on_regex( 104 | r"^[来给发]?([0-9]{1,2}|[一二三四五六七八九十])?([点份张])?([Rr]18的?)?(.{1,20})?[涩|色]图$", 105 | priority=5, block=True 106 | ) 107 | 108 | 109 | @call_setu.handle() 110 | async def _(bot: Bot, event: Event, state: T_State): 111 | if isinstance(event, MessageEvent): 112 | params = dict() 113 | num, _, is_r18, keyword = state["_matched_groups"] 114 | if num: 115 | num = int(num if num.isdigit() else num_dict[num]) 116 | params.update( 117 | {"num": config.single_image_limit if num > config.single_image_limit else num}) 118 | if is_r18: 119 | if config.global_r18: 120 | params.update({"r18": 1}) 121 | if keyword: 122 | params.update({"keyword": keyword}) 123 | else: 124 | if config.personalized_recommendation and (random() > 0.5): 125 | if res := list(UserXP.select().where(UserXP.user_id == event.user_id, UserXP.count > 500)): 126 | params.update({"tag": [[choice(res).tag]]}) 127 | if config.reverse_proxy: 128 | params.update({"proxy": config.reverse_proxy}) 129 | rd = RequestData(**params) 130 | async with LoliconAPI(params=rd) as api: 131 | statistics = dict(sorted(Counter(tags := api.tags).items(), key=lambda x: x[1], 132 | reverse=True)[:len(tags) if len(tags) < 10 else 10]) 133 | for tag, count in statistics.items(): 134 | if tag not in config.useless_tag and not tag.endswith("users入り"): 135 | if res := UserXP.get_or_none(user_id=event.user_id, tag=tag): 136 | UserXP.replace_many( 137 | [{"user_id": event.user_id, "tag": tag, 138 | "count": res.count + count}]).execute() 139 | else: 140 | UserXP.replace_many( 141 | [{"user_id": event.user_id, "tag": tag, "count": count}]).execute() 142 | if msgs := await api.generate_msgs(): 143 | if config.use_forward: 144 | await send_forward_msg(msgs, bot, event) 145 | else: 146 | await send_msgs(msgs, bot, event) 147 | else: 148 | logger.warning("Not supported: setu.") 149 | return 150 | 151 | 152 | get_pixiv_image = matchers.on_command( 153 | "/pixiv", aliases={"/pid"}, priority=1, block=True 154 | ) 155 | 156 | 157 | @get_pixiv_image.handle() 158 | async def _(bot: Bot, event: Event): 159 | if isinstance(event, MessageEvent): 160 | if (illust_id := event.get_plaintext()).isdigit() and (msgs := await ajax_pixiv(int(illust_id))): 161 | if config.use_forward: 162 | await send_forward_msg(msgs, bot, event) 163 | else: 164 | await send_msgs(msgs, bot, event) 165 | else: 166 | try: 167 | await get_pixiv_image.finish("Invalid input.") 168 | except ActionFailed as e: 169 | logger.exception( 170 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 171 | return 172 | else: 173 | logger.warning("Not supported: setu.") 174 | return 175 | 176 | get_user_xp = matchers.on_command( 177 | "查询xp", aliases={"查询XP"}, priority=1, block=True 178 | ) 179 | get_group_xp = matchers.on_command( 180 | "xp排行榜", aliases={"XP排行榜"}, priority=1, block=True 181 | ) 182 | 183 | 184 | @get_user_xp.handle() 185 | async def _(bot: Bot, event: Event): 186 | if isinstance(event, MessageEvent): 187 | if user_ids := get_user_qq(event.raw_message): 188 | pass 189 | else: 190 | user_ids = (event.user_id,) 191 | for user_id in user_ids: 192 | if res := UserXP.select().where(UserXP.user_id == user_id): 193 | if user_id != event.user_id and isinstance(event, GroupMessageEvent): 194 | user_info = await bot.get_group_member_info(group_id=event.group_id, 195 | user_id=user_id, no_cache=True) 196 | card, nickname, name = user_info["card"], user_info[ 197 | "nickname"], "他" if user_info["sex"] == "male" else "她" 198 | else: 199 | card, nickname, name = event.sender.card, event.sender.nickname, "你" 200 | buf: Dict[str, int] = dict([(r.tag, r.count) for r in res]) 201 | statistics = dict(sorted(list(buf.items()), key=lambda x: x[1], 202 | reverse=True)[:len(buf) if len(buf) < 10 else 10]) 203 | await get_user_xp.send("\n".join([ 204 | f"> {card or nickname}", 205 | f"{name}喜欢以下元素:" + ", ".join(list(statistics.keys())[ 206 | :int(len(statistics) / 2)]), 207 | f"{name}可能对以下元素感兴趣:" + ", ".join(list(statistics.keys())[ 208 | int(len(statistics) / 2):]) 209 | ])) 210 | raise FinishedException 211 | await get_user_xp.finish(f"我也不知道ta的xp呢~") 212 | else: 213 | logger.warning("Not supported: setu.") 214 | return 215 | 216 | 217 | @get_group_xp.handle() 218 | async def _(bot: Bot, event: GroupMessageEvent): 219 | if (tag := event.get_plaintext()) in config.useless_tag: 220 | await get_group_xp.finish("这个 tag 太常见了不会被统计哦~") 221 | else: 222 | if res := UserXP.select().where(UserXP.tag == tag): 223 | result = [f"> {event.sender.card or event.sender.nickname}"] 224 | group_member_list = await bot.get_group_member_list(group_id=event.group_id) 225 | user_ids = [member["user_id"] for member in group_member_list] 226 | buf: Dict[int, int] = dict( 227 | [(r.user_id, r.count) for r in res if r.user_id in user_ids]) 228 | statistics = dict(sorted(buf.items(), key=lambda x: x[1], 229 | reverse=True)[:len(buf) if len(buf) < 10 else 10]) 230 | count_ = 1 231 | for user, count in statistics.items(): 232 | user_info = [ 233 | member for member in group_member_list if member["user_id"] == user][0] 234 | card, nickname, name = user_info["card"], user_info[ 235 | "nickname"], "他" if user_info["sex"] == "male" else "她" 236 | result.append("\n".join([ 237 | f"第 {count_} 喜欢 {tag} 的人是 {card or nickname} ({user}) ~", 238 | f"{name} 已经看了 {count} 张含有 {tag} 的涩图啦 ~" 239 | ])) 240 | count_ += 1 241 | try: 242 | await get_group_xp.finish("\n".join(result)) 243 | except ActionFailed as e: 244 | logger.exception( 245 | f'ActionFailed | {e.info["msg"].lower()} | retcode = {e.info["retcode"]} | {e.info["wording"]}') 246 | return 247 | else: 248 | await get_group_xp.finish("\n".join([ 249 | f"> {event.sender.card or event.sender.nickname}", 250 | "这个xp太怪了, 没有人喜欢 ~" 251 | ])) 252 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-09-01 17:48:22 4 | - LastEditTime: 2021-09-09 12:34:29 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from pydantic import BaseModel 10 | 11 | 12 | class Config(BaseModel): 13 | """ 14 | - ·auto_fetch` : 自动缓存图片 15 | - `global_r18` : 全局 r18 开关, 禁用后 Bot 将不发送 r18 图片 16 | - `recall_time` : 经过多少秒后撤回图片, 设置成 0 时候不自动撤回 17 | - `url_only` : 仅回复 url , 不发送图片 18 | - `online_mode_pure` 纯在线模式, 该模式下 Bot 将不会缓存图片到本地 19 | - `use_forward` : 启用合并转发 20 | - `proxy`: 代理服务器地址, 仅支持 http 代理 21 | - `personalized_recommendation` : 个性化推荐开关 22 | - `public_address` : Bot 对外开放的地址, 若已设置反向代理, 该项应该填反向代理后的地址 23 | - `single_image_limit` : 限制单次发送色图数量 24 | - `useless_tag`: 不具备实际意义的 tag, 统计用户性趣的时候这些 tag 将被忽略 25 | """ 26 | auto_fetch: int 27 | global_r18: bool 28 | recall_time: int 29 | online_mode_pure: bool 30 | url_only: bool 31 | use_forward: bool 32 | proxy: str 33 | reverse_proxy: str 34 | personalized_recommendation: bool 35 | public_address: str 36 | single_image_limit: int 37 | useless_tag: list 38 | 39 | 40 | setting = { 41 | "auto_fetch": 0, 42 | "global_r18": True, 43 | "recall_time": 0, 44 | "online_mode_pure": False, 45 | "url_only": False, 46 | "use_forward": True, 47 | "proxy": "", 48 | "reverse_proxy": "", 49 | "personalized_recommendation": True, 50 | "public_address": "http://127.0.0.1:8080", 51 | "single_image_limit": 10, 52 | "useless_tag": [ 53 | "原创", "オリジナル", "女孩子", "女の子", "R-18", 54 | "アニメ", "イラスト", "創作", "SD", "漫画", 55 | "デフォルメ", "看板娘", "仕事", "仕事絵", "メスガキ", 56 | "立ち絵", "doujin", "C96", "C95", "C97" 57 | ] 58 | } 59 | 60 | config = Config(**setting) 61 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-09-04 13:34:27 4 | - LastEditTime: 2021-09-27 00:36:14 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from os import path 10 | 11 | import peewee as pw 12 | 13 | db_path = path.abspath(path.join(path.dirname(__file__), "setu_data.db")) 14 | db = pw.SqliteDatabase(db_path) 15 | 16 | 17 | class BaseModel(pw.Model): 18 | class Meta: 19 | database = db 20 | 21 | 22 | class UserXP(BaseModel): 23 | user_id = pw.IntegerField() 24 | tag = pw.CharField() 25 | count = pw.IntegerField() 26 | 27 | class Meta: 28 | primary_key = pw.CompositeKey("user_id", "tag") 29 | 30 | 31 | if not path.exists(db_path): 32 | db.connect() 33 | db.create_tables([UserXP]) 34 | db.close() 35 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/lolicon_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-09-02 11:12:21 4 | - LastEditTime: 2021-09-16 22:16:27 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from os import mkdir, path 10 | from time import localtime, strftime 11 | from typing import Any, Dict, List, Optional, Union 12 | from urllib.parse import urljoin 13 | 14 | from aiohttp import ClientSession 15 | from loguru import logger 16 | from nonebot.adapters.cqhttp.message import MessageSegment 17 | 18 | from .config import config 19 | from .util import headers 20 | from .validation import RequestData 21 | 22 | image_cache_path = path.abspath(path.join(path.dirname(__file__), "image")) 23 | 24 | 25 | class LoliconAPI: 26 | 27 | def __init__(self, params: RequestData) -> None: 28 | self.api_url = "https://api.lolicon.app/setu/v2" 29 | self.params = params 30 | self._session = ClientSession() 31 | 32 | async def __aenter__(self): 33 | res = await self._post() 34 | self.error: str = res["error"] 35 | self.response: List[Dict] = [data for data in res["data"]] 36 | return self 37 | 38 | async def _post(self) -> Optional[Dict[str, Any]]: 39 | async with self._session.post(self.api_url, json=self.params.dict()) as resp: 40 | if resp.ok: 41 | return await resp.json() 42 | 43 | async def _get_image(self, url: str) -> Union[str, MessageSegment]: 44 | if config.online_mode_pure: 45 | return MessageSegment.image(urljoin(config.public_address, 46 | "pixiv/") + "/".join(url.split("/")[3:])) 47 | try: 48 | assert not config.url_only 49 | if not path.exists(image_path := path.join(image_cache_path, url.split("/")[-1])): 50 | async with self._session.get(url, headers=headers, proxy=config.proxy, verify_ssl=False) as resp: 51 | with open(image_path, "wb") as file: 52 | file.write(await resp.read()) 53 | return MessageSegment.image("file:///" + image_path) 54 | except: 55 | return urljoin(config.public_address, "pixiv/") + "/".join(url.split("/")[3:]) 56 | 57 | async def generate_msgs(self) -> List[str]: 58 | return ["\n".join([ 59 | "标题: {}".format(res["title"]), 60 | "作者: {}".format(res["author"]), 61 | "高度 / 宽度: {}, {}".format(res["height"], res["width"]), 62 | "插画 id: {}".format(res["pid"]), 63 | "限制级作品: {}".format("是" if res["r18"] else "否"), 64 | "含有以下成分: {}".format(", ".join([i for i in res["tags"]])), 65 | "上传时间: {}".format(LoliconAPI.format_time(res["uploadDate"])), 66 | str(await self._get_image(res["urls"][self.params.size])) 67 | ]) for res in self.response] 68 | 69 | async def auto_fetch(self): 70 | for res in self.response: 71 | await self._get_image(url := res["urls"][self.params.size]) 72 | logger.info(f"Successfully downloaded image: {url}") 73 | return 74 | 75 | @property 76 | def tags(self): 77 | result: List[str] = list() 78 | for data in self.response: 79 | result.extend(data["tags"]) 80 | return result 81 | 82 | @staticmethod 83 | def format_time(time_stamp: int) -> str: 84 | return strftime("%Y-%m-%d %H:%M:%S", localtime(time_stamp / 1000)) 85 | 86 | async def __aexit__(self, *args): 87 | await self._session.close() 88 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/route.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-31 20:04:17 4 | - LastEditTime: 2021-09-06 21:40:29 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from os import path 10 | 11 | from fastapi.responses import FileResponse, Response 12 | from nonebot import get_asgi 13 | 14 | from .util import fetch_image 15 | 16 | app = get_asgi() 17 | 18 | image_cache_path = path.abspath(path.join(path.dirname(__file__), "image")) 19 | 20 | 21 | @app.get("/pixiv/{pixiv_image_url:path}") 22 | async def _(pixiv_image_url: str): 23 | if path.exists(cache := path.join(image_cache_path, pixiv_image_url.split("/")[-1])): 24 | return FileResponse(cache) 25 | if (res := await fetch_image(pixiv_image_url)) is None: 26 | return 27 | image, content_type = res 28 | headers = { 29 | "cache-control": "no-cache", 30 | "Content-Type": content_type 31 | } 32 | return Response(image, headers=headers, media_type="stream") 33 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-08-31 19:44:57 4 | - LastEditTime: 2021-09-06 21:40:33 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from os import path 10 | from re import findall 11 | from sys import platform 12 | from typing import List, Optional, Tuple, Union 13 | from urllib.parse import urljoin 14 | 15 | from aiohttp import ClientSession, request 16 | from nonebot.adapters.cqhttp.message import MessageSegment 17 | 18 | from .config import config 19 | 20 | image_cache_path = path.abspath(path.join(path.dirname(__file__), "image")) 21 | 22 | if platform == "win32": 23 | import asyncio 24 | 25 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 26 | 27 | headers = { 28 | "Referer": "https://www.pixiv.net", 29 | "User-Agent": 30 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 " 31 | "Safari/537.36", 32 | } 33 | 34 | 35 | async def fetch_image(image_path: str) -> Optional[Tuple[bytes, str]]: 36 | try: 37 | assert image_path.startswith(("img-original", "img-master", "c/")) 38 | async with request("GET", "https://i.pximg.net/" + image_path, headers=headers, 39 | proxy=config.proxy) as resp: 40 | return await resp.read(), resp.headers["Content-Type"] 41 | except: 42 | return 43 | 44 | 45 | async def ajax_pixiv(illust_id: int) -> Optional[List[Union[str, MessageSegment]]]: 46 | try: 47 | async with ClientSession() as session: 48 | async with session.get(f"https://www.pixiv.net/ajax/illust/{illust_id}", headers=headers, 49 | proxy=config.proxy, verify_ssl=False) as resp: 50 | url: str = (data := await resp.json())["body"]["urls"]["original"] 51 | count = data["body"]["userIllusts"][str( 52 | illust_id)]["pageCount"] 53 | urls = [url.replace("_p0", f"_p{i}") for i in range(count)] 54 | if config.url_only: 55 | return [urljoin(config.public_address, 56 | "pixiv/") + "/".join(url.split("/")[3:]) for url in urls] 57 | if config.online_mode_pure: 58 | return [MessageSegment.image(urljoin(config.public_address, 59 | "pixiv/") + "/".join(url.split("/")[3:])) for url in urls] 60 | for url in urls: 61 | if not path.exists(image_path := path.join(image_cache_path, url.split("/")[-1])): 62 | with open(image_path, "wb") as file: 63 | async with session.get(url, headers=headers, proxy=config.proxy) as res: 64 | file.write(await res.read()) 65 | return [MessageSegment.image("file:///" + path.join(image_cache_path, 66 | url.split("/")[-1])) for url in urls] 67 | except: 68 | return 69 | 70 | 71 | def get_user_qq(raw_message: str) -> List[int]: 72 | raw = map(int, findall(r'\[CQ:at,qq=([1-9][0-9]{4,})\]', raw_message)) 73 | return list(set(i for i in raw)) 74 | -------------------------------------------------------------------------------- /nonebot-plugin-setu/validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Author: DiheChen 3 | - Date: 2021-09-02 11:12:21 4 | - LastEditTime: 2021-09-04 17:37:32 5 | - LastEditors: DiheChen 6 | - Description: None 7 | - GitHub: https://github.com/Chendihe4975 8 | """ 9 | from typing import List, Literal, Optional 10 | 11 | from pydantic import BaseModel, validator 12 | 13 | 14 | class RequestData(BaseModel): 15 | """ 16 | - 数据效验模块 17 | """ 18 | r18: Literal[0, 1, 2] = 0 19 | num: int = 1 20 | uid: Optional[List[int]] = list() 21 | keyword: str = "" 22 | tag: Optional[List[List[str]]] = "" 23 | size: Literal["original", "regular", "small", "thumb", "mini"] = "original" 24 | proxy: str = "" 25 | dateAfter: Optional[int] = "" 26 | dateBefore: Optional[int] = "" 27 | dsc: bool = False 28 | 29 | @validator("num") 30 | def check_num(cls, value): 31 | if not 1 <= value <= 100: 32 | raise ValueError("Invalid key: num.") 33 | return value 34 | 35 | @validator("uid") 36 | def check_uid(cls, value): 37 | if len(value) > 20: 38 | raise ValueError("Parameter uid is too long.") 39 | return value 40 | 41 | @validator("tag") 42 | def check_tag(cls, value): 43 | if value: 44 | for v in value: 45 | if len(v) > 2: 46 | raise ValueError("Invalid key: tag.") 47 | return value 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot2-plugins" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Chendihe4975 "] 6 | license = "MIT License" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | nonebot2 = "^2.0.0-alpha.15" 11 | nonebot-adapter-cqhttp = "^2.0.0-alpha.15" 12 | aiohttp = "^3.7.4" 13 | peewee = "^3.14.4" 14 | ujson = "^4.1.0" 15 | Pillow = "^8.3.1" 16 | nonebot-plugin-apscheduler = "^0.1.2" 17 | aiofiles = "^0.7.0" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | --------------------------------------------------------------------------------