├── .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 |
3 |
4 |
5 |
6 |
7 | # nonebot2-plugins
8 |
9 | _✨ ~~女生自用~~ nonebot2 插件。 ✨_
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------