├── Download_image ├── Download_img.py └── README.md ├── LICENSE ├── README.md ├── nonebot_plugin_setu4 ├── __init__.py ├── config.py ├── fetch_resources.py ├── get_data.py ├── mamager_handle.py ├── permission_manager.py ├── resource │ └── lolicon.db ├── send_setu.py └── setu_message.py ├── pyproject.toml └── requirements.txt /Download_image/Download_img.py: -------------------------------------------------------------------------------- 1 | """下载图片""" 2 | import asyncio 3 | import json 4 | import random 5 | import sqlite3 6 | from pathlib import Path 7 | 8 | from httpx import AsyncClient 9 | from loguru import logger 10 | 11 | 12 | class DownloadImg: 13 | def __init__(self) -> None: 14 | """ 15 | 初始化数据库连接, 创建img文件夹, 初始化数据 16 | """ 17 | 18 | conn = sqlite3.connect("lolicon.db") 19 | cur = conn.cursor() 20 | logger.info("数据库连接成功") 21 | self.data = cur.execute( 22 | "SELECT urls,r18 from main where status!='unavailable'" 23 | ).fetchall() 24 | random.shuffle(self.data) 25 | conn.close() 26 | 27 | nsfw_path = Path("img/nsfw") 28 | sfw_path = Path("img/sfw") 29 | nsfw_path.mkdir(exist_ok=True, parents=True) 30 | sfw_path.mkdir(exist_ok=True) 31 | self.all_files = {i.name for i in nsfw_path.iterdir()} | { 32 | i.name for i in sfw_path.iterdir() 33 | } 34 | 35 | self.error_json = {} 36 | 37 | async def main(self) -> None: 38 | """ 39 | 主函数, 发起下载任务 40 | """ 41 | 42 | task_list = [] 43 | sem = asyncio.Semaphore(30) 44 | for item in self.data: 45 | url: str = item[0] 46 | file_name = url.split("/")[-1] 47 | r18: int = item[1] # 0, 1 48 | if file_name not in self.all_files: 49 | task_list.append(self.start_download(url, file_name, sem, r18)) 50 | logger.info(f"读取到 {len(task_list)} 个图片未下载,准备下载") 51 | await asyncio.gather(*task_list) 52 | 53 | async def start_download( 54 | self, url: str, file_name: str, sem: asyncio.Semaphore, r18: int 55 | ): 56 | """ 57 | 下载图片 58 | """ 59 | 60 | url = url.replace("i.pixiv.re", "setu.51246352.xyz") # 反代地址 61 | save_path = f"img/nsfw/{file_name}" if r18 else f"img/sfw/{file_name}" 62 | async with sem: 63 | try: 64 | async with AsyncClient() as client: 65 | re = await client.get(url=url, timeout=120) 66 | if re.status_code == 200: 67 | with open(save_path, "wb") as f: 68 | f.write(re.content) 69 | logger.success(f"下载完成 {file_name}") 70 | else: 71 | logger.error(f"下载失败 {file_name}, 状态码: {re.status_code}") 72 | if re.status_code not in self.error_json: 73 | self.error_json.update({re.status_code: []}) 74 | self.error_json[re.status_code].append(url) 75 | self.save_error() 76 | 77 | except Exception as e: 78 | logger.error(f"下载失败 {file_name},请求超时") 79 | if str(e) not in self.error_json: 80 | self.error_json.update({str(e): []}) 81 | self.error_json[str(e)].append(url) 82 | self.save_error() 83 | 84 | def save_error(self): 85 | """ 86 | 保存错误信息 87 | """ 88 | 89 | with open("error.json", "w", encoding="utf-8") as f: 90 | json.dump(self.error_json, f, ensure_ascii=False, indent=4) 91 | 92 | 93 | if __name__ == "__main__": 94 | download = DownloadImg() 95 | asyncio.run(download.main()) 96 | -------------------------------------------------------------------------------- /Download_image/README.md: -------------------------------------------------------------------------------- 1 | # 食用方法 2 | 3 | 4 | ### 反代服务器是racknerd一百来块买的一年的服务器, 6T流量别组团来草 5 | 6 | 7 | ## 推荐 8 | 9 | - 去nonebot_plugin_setu4/resource文件夹下把数据库复制过来 10 | - 在这个文件夹打开命令行输入 11 | ``` 12 | python Download_img.py 13 | ``` 14 | - 即可下载图片 15 | - 注意依赖于sqlite3, httpx, loguru库 16 | - 需要更换代理请找到 17 | ``` python 18 | url = url.replace('i.pixiv.re', 'setu.51246352.xyz') 19 | ``` 20 | 这行代码, 将setu.51246352.xyz换成你想要的反代地址即可 (也可以直接删掉用i.pixiv.re) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Special-Week 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nonebot_plugin_setu4 2 | 3 | 内置数据库的setu插件, 另外尝试降低因为风控发不出图的概率(图像左右镜像翻转, 随机修改左上角一颗像素点) 4 | 5 | 6 | ### tips 7 | github仓库内顺便加了一次性下载数据库内所有图片的脚本, 文件夹download_image内 8 | 请勿团体大规模爬取, 造成代理服务器不可用, 代理使用的是我自己提供的代理服务器setu.51246352.xyz 9 | 如果不可用请删除第42行代码使用数据库默认的i.pixiv.re或修改成其他 10 | 11 | 12 | 另外:数据库的表结构性能极低,如果想拿来做api什么的建议把数据洗一下,比如分成多个表,tags单独分一个表,并且建立索引 13 | 14 | ### 目前数据库去除unavailable, 共124669条记录 15 | 16 | 安装方式: 17 | 18 | pip install nonebot_plugin_setu4 19 | 20 | nb plugin install nonebot-plugin-setu4 21 | 22 | 有能力尽量从本仓库clone, 因为pypi数据库不一定最新(可能会差个几千条), 也可以试着手动下载数据库换上去, 数据库大概月更 23 | 24 | 25 | 26 | ## env 配置项 27 | 28 | >以下配置项均可不填,插件会按照默认值读取 29 | 30 | |config |type |default |example |usage | 31 | |-------------------|----------------|-----------|-----------------------------------------|----------------------------------------| 32 | |setu_disable_wlist |bool |False |setu_disable_wlist = True |是否禁用白名单检查(极度不推荐禁用)(详见权限控制系统)| 33 | |setu_enable_private|bool |False |setu_enable_private = True |是否允许未在白名单的私聊会话使用(详见权限控制系统) | 34 | |setu_perm_cfg_path |str |see example|setu_perm_cfg_path = './data/setu4' |会话(群号或QQ号)启用、r18及其他独立配置项 | 35 | |setu_save |str |None |setu_save = './data/setu4/img' |setu保存到本地的路径, 留空则不保存至本地 | 36 | |setu_database_path |str |see example|setu_database_path = see description[^1] |更新使用的数据库的地址, 默认为此项目的resource文件夹下| 37 | |scientific_agency |str |None |scientific_agency = 'http://127.0.0.1:7890'|科学上网的代理地址, 当不使用反向代理的时候(默认使用i.pixiv.re)直连i.pximg.net获取图片, 大陆服务器无法访问时填写| 38 | |setu_quality |List |[5, 75] |setu_quality = [5, 95] |setu图片的质量, 为解决轻量级服务器发送图片的带宽问题, 可以适当降低图片的质量, [5, 75]的意思是大于5张的话图片质量压缩到75, 95为最佳质量| 39 | |group_forward_msg |bool |False |group_forward_msg = True |群聊是否以转发的形式发送色图 | 40 | |sfw_withdraw |bool |True |sfw_withdraw = False |当setu_withdraw_time(色图撤回时间)不等于0时, 是否撤回非r18图片, 默认撤回| 41 | |setu_cd* |int |20 |setu_cd = 30 |setu默认cd[0,+∞], 为0时无cd | 42 | |setu_withdraw_time*|int |100 |setu_withdraw_time = 30 |setu默认撤回时间[0,100], 为0时不撤回 | 43 | |setu_max_num* |int |10 |setu_max_num = 20 |setu默认一次性最大数量[1,25] | 44 | 45 | >带有*标识的设置项可在指定群聊被setu_perm_cfg.json中的内容覆盖 46 | 47 | [^1]:"https://raw.githubusercontent.com/Special-Week/nonebot_plugin_setu4/main/nonebot_plugin_setu4/resource/lolicon.db" 48 | 49 | setu_save保存后下一次调用碰到这个setu会先从这个文件夹中进行匹配, 不需要再下载 50 | 51 | 一般无需科学上网, 但希望你确认一下图片代理是否可用: 52 | 53 | 一些也许可用的pixiv代理: "setu.51246352.xyz", "i.pixiv.re" , "sex.nyan.xyz" , "px2.rainchan.win" , "pximg.moonchan.xyz" , "piv.deception.world" , "px3.rainchan.win" , "px.s.rainchan.win" , "pixiv.yuki.sh" , "pixiv.kagarise.workers.dev" , "pixiv.kagarise.workers.dev" 54 | 55 | 使用插件提供的on_command响应器进行更换(on_command响应器注意.env内的命令头, 默认的代理为i.pixiv.re) 56 | 使用superuser账号发送: setu_proxy xxx Example: setu_proxy i.pixiv.re 57 | 警告: 这部分带了一个ping代理服务器的操作, 这个响应器是superuser only, 用了os.popen().read()操作, 请不要尝试给自己电脑注入指令 58 | 59 | Example: 60 | 数据库给的url为: https://i.pixiv.re/img-original/img/2022/07/09/18/51/03/99606781_p0.jpg 61 | 62 | 有些代理可能会暂时不可用, 可以用来换成可用的代理, 比如setu.51246352.xyz 63 | 64 | 即: https://setu.51246352.xyz/img-original/img/2022/07/09/18/51/03/99606781_p0.jpg 65 | 66 | 注: pixiv官网原连接为"i.pximg.net", 你也可以设置回去, 在你有科学上网或者大陆以外的网络环境时, 直接用官网的原链接下载图片速度应该是最理想的 67 | 68 | 能正常访问即可用 69 | 70 | ## 权限控制系统 71 | 72 | 本插件的权限控制系统分为两个部分:黑名单和白名单。此部分内容被存储在setu_perm_cfg_path中的setu_perm_cfg.json,其结构如下所示: 73 | 74 | ```python 75 | { 76 | "group_114":{ 77 | "cd" : 30, # cd时长 78 | "r18" : True, # r18开关 79 | "withdraw" : 100, # 撤回延时 80 | "maxnum" : 10 # 单次最高张数 81 | }, 82 | "last":{ # 最近一次发送setu的时间, 用于计算剩余的冷却时间, 此部分不会被积极写入文件, 仅更新内存 83 | "user_1919" : 810 84 | }, 85 | "ban":[ # 黑名单, 禁用的群组或用户,跨会话生效, 会覆盖白名单设置 86 | "user_1919", 87 | "group_810" 88 | ], 89 | "proxy": "i.pixiv.re" # 代理, 用于替换数据库中的url 90 | } 91 | ``` 92 | 93 | 在插件运行中,权限控制系统会按照如下顺序进行检查: 94 | 95 | 1. 检查该会话是否在黑名单中存在,若存在则检查不通过。 96 | 2. 检查该会话是否屏蔽了白名单(即setu_disable_wlist = True),若屏蔽则跳过第3~4步的检查(认为通过)。 97 | 3. 检查该会话是否为 `群聊类型` 或 `setu_enable_private = False 下的私聊类型`,若不是则跳过第4步的检查(认为通过)。 98 | 4. 检查该会话是否在白名单中存在,若不存在则检查不通过。 99 | 5. 检查该会话的冷却时间是否归零,若未归零则检查不通过。 100 | 6. 在以上检查通过的情况下,读取其他配置项并通过检查。 101 | 102 | 此部分的内容可以参考权限控制模块的[对应部分](./nonebot_plugin_setu4/permission_manager.py)进行理解。 103 | 104 | ## 获取setu 105 | 106 | 命令头: setu|色图|涩图|想色色|来份色色|来份色图|想涩涩|多来点|来点色图|来张setu|来张色图|来点色色|色色|涩涩 (任意一个) 107 | 108 | 张数: 1 2 3 4 ... 张|个|份 (可不填, 默认1) 109 | 110 | r18: 不填则不会出现r18图片, 填了会根据r18模式管理中的数据判断是否可返回r18图片 111 | 112 | 关键词: 任意, 多tag使用空格分开 (可不填) 113 | 114 | 参考 (空格可去掉): 115 | 116 | setu 10张 r18 白丝 117 | 118 | setu 10张 白丝 119 | 120 | setu r18 白丝 121 | 122 | setu 白丝 123 | 124 | setu 125 | 126 | ## 权限管理 127 | 128 | 注意: 129 | 130 | 1. 全部群聊或私聊默认均未在白名单, 但可以通过设置 setu_enable_private = True 将私聊默认全部开启, 群聊还需通过白名单管理指令添加。 131 | 2. superuser在任意聊天或在设置 setu_enable_private = True 的情况下好友私聊中, 会话均不受cd和白名单本身的影响, 但会受 撤回时长, r18, 最大张数 的影响。 132 | 3. 在群聊中默认以该群作为操作对象, 但在私聊需要用户提供操作对象。 133 | 4. 此部分的事件响应器均为 on_command 生成的, 触发时需要带有[命令头](https://v2.nonebot.dev/docs/api/config#Config-command_start)。 134 | 135 | 白名单管理: 136 | 137 | setu_wl add 添加会话至白名单 eg: setu_wl add user_114514/group_1919810 138 | setu_wl del 移出会话自白名单 eg: setu_wl del user_114514/group_1919810 139 | 140 | 黑名单管理: 141 | 142 | setu_ban add 添加会话至黑名单 eg: setu_ban add user_114514/group_1919810 143 | setu_ban del 移出会话自黑名单 eg: setu_ban del user_114514/group_1919810 144 | 145 | r18模式管理: 146 | 147 | setu_r18 on 开启会话的r18模式 eg: setu_r18 on group_1919810 148 | setu_r18 off 关闭会话的r18模式 eg: setu_r18 off group_1919810 149 | 150 | cd时间更新: 151 | 152 | setu_cd xxx 更新会话的冷却时间, xxx为int类型的参数 eg: setu_cd 10 group_1919810 153 | 154 | 撤回时间更新: 155 | 156 | setu_wd xxx 撤回前等待的时间, xxx为int类型的参数 eg: setu_wd 10 group_1919810 157 | 158 | 最大张数更新: 159 | 160 | setu_mn xxx 单次发送的最大图片数, xxx为int类型的参数 eg: setu_mn 10 group_1919810 161 | 162 | 更换setu代理服务器: 163 | 164 | setu_proxy xxx 使用的代理服务器, xxx 为 string 类型的参数 165 | 警告: 这部分带了一个ping代理服务器的操作, 这个响应器是superuser only, 用了os.popen().read()操作, 请不要尝试给自己电脑注入指令 166 | 167 | ​ 168 | 169 | ## 其他指令 170 | 171 | 获取插件帮助信息: 172 | 173 | "setu_help" | "setu_帮助" | "色图_help" | "色图_帮助" 174 | 175 | 查询黑白名单: 176 | 177 | "setu_roster" | "色图名单" 178 | 179 | 180 | 数据库更新: 181 | >此指令默认从 github.com[^2] 拉取数据库,如果无法访问可以考虑使用科学上网或更换镜像或者手动从仓库下载换上去。 182 | 183 | setu_db 从指定的路径拉取 lolicon.db 数据库,默认为此仓库 184 | 185 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/__init__.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from re import I 3 | 4 | from nonebot import on_command, on_regex 5 | from nonebot.permission import SUPERUSER 6 | from nonebot.plugin import PluginMetadata 7 | 8 | from .mamager_handle import manager_handle 9 | from .send_setu import send_setu 10 | 11 | with contextlib.suppress(Exception): 12 | __plugin_meta__ = PluginMetadata( 13 | name="setu4", 14 | description="内置数据库的setu插件, 尝试降低因为风控发不出图的概率", 15 | usage=r"^(setu|色图|涩图|想色色|来份色色|来份色图|想涩涩|多来点|来点色图|来张setu|来张色图|来点色色|色色|涩涩)\s?([x|✖️|×|X|*]?\d+[张|个|份]?)?\s?(r18)?\s?(.*)?", 16 | type="application", 17 | homepage="https://github.com/Special-Week/nonebot_plugin_setu4", 18 | supported_adapters={"~onebot.v11"}, 19 | extra={ 20 | "author": "Special-Week", 21 | "version": "0.16.114514", 22 | "priority": 10, 23 | }, 24 | ) 25 | 26 | 27 | # 命令正则表达式 28 | setu_regex: str = r"^(setu|色图|涩图|想色色|来份色色|来份色图|想涩涩|多来点|来点色图|来张setu|来张色图|来点色色|色色|涩涩)\s?([x|✖️|×|X|*]?\d+[张|个|份]?)?\s?(r18)?\s?(.*)?" 29 | on_regex( 30 | setu_regex, 31 | flags=I, 32 | priority=20, 33 | block=True, 34 | handlers=[send_setu.setu_handle], 35 | ) 36 | 37 | 38 | # ----- 白名单添加与解除 ----- 39 | on_command( 40 | "setu_wl", 41 | block=True, 42 | priority=10, 43 | permission=SUPERUSER, 44 | handlers=[manager_handle.open_setu], 45 | ) 46 | 47 | 48 | # ----- r18添加与解除 ----- 49 | on_command( 50 | "setu_r18", 51 | block=True, 52 | priority=10, 53 | permission=SUPERUSER, 54 | handlers=[manager_handle.set_r18], 55 | ) 56 | 57 | 58 | # ----- cd时间更新 ----- 59 | on_command( 60 | "setu_cd", 61 | block=True, 62 | priority=10, 63 | permission=SUPERUSER, 64 | handlers=[manager_handle.set_cd], 65 | ) 66 | 67 | 68 | # ----- 撤回时间更新 ----- 69 | on_command( 70 | "setu_wd", 71 | block=True, 72 | priority=10, 73 | permission=SUPERUSER, 74 | handlers=[manager_handle.set_wd], 75 | ) 76 | 77 | 78 | # ----- 最大张数更新 ----- 79 | on_command( 80 | "setu_mn", 81 | block=True, 82 | priority=10, 83 | permission=SUPERUSER, 84 | handlers=[manager_handle.set_maxnum], 85 | ) 86 | 87 | 88 | # ----- 黑名单添加与解除 ----- 89 | on_command( 90 | "setu_ban", 91 | block=True, 92 | priority=10, 93 | permission=SUPERUSER, 94 | handlers=[manager_handle.ban_setu], 95 | ) 96 | 97 | 98 | # --------------- 发送帮助信息 --------------- 99 | on_command( 100 | "setu_help", 101 | block=True, 102 | priority=10, 103 | aliases={"setu_帮助", "色图_help", "色图_帮助"}, 104 | handlers=[manager_handle.setu_help], 105 | ) 106 | 107 | 108 | # --------------- 更换代理 --------------- 109 | on_command( 110 | "更换代理", 111 | block=True, 112 | priority=10, 113 | permission=SUPERUSER, 114 | aliases={"替换代理", "setu_proxy"}, 115 | handlers=[manager_handle.replace_proxy, manager_handle.replace_proxy_got], 116 | ) 117 | 118 | 119 | # --------------- 查询黑白名单 --------------- 120 | on_command( 121 | "setu_roster", 122 | block=True, 123 | priority=10, 124 | aliases={"色图名单"}, 125 | permission=SUPERUSER, 126 | handlers=[manager_handle.query_black_white_list], 127 | ) 128 | 129 | 130 | # --------------- 数据库更新 --------------- 131 | on_command( 132 | "setu_db", 133 | block=True, 134 | priority=10, 135 | permission=SUPERUSER, 136 | handlers=[manager_handle.setu_db], 137 | ) 138 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Optional, Union 3 | 4 | from loguru import logger 5 | from nonebot import get_driver 6 | from pydantic import BaseModel, parse_obj_as 7 | 8 | DATA_PATH = Path("data/setu4") 9 | if not DATA_PATH.exists() or not DATA_PATH.is_dir(): 10 | logger.warning(f"数据目录 {DATA_PATH} 不存在, 将自动创建") 11 | DATA_PATH.mkdir(0o755, parents=True, exist_ok=True) 12 | 13 | 14 | class Config(BaseModel): 15 | setu_disable_wlist: bool = False # 是否禁用白名单检查 16 | setu_enable_private: bool = False # 是否允许未在白名单的私聊会话使用 17 | setu_save: Union[ 18 | bool, str 19 | ] = False # 保存图片的路径, 默认False, 填.env时候希望收到的是字符串而不是True 20 | # 数据库路径, 默认使用github的地址 21 | database_path: str = "https://raw.githubusercontent.com/Special-Week/nonebot_plugin_setu4/main/nonebot_plugin_setu4/resource/lolicon.db" 22 | 23 | setu_cd: int = 30 # 冷却时间 24 | setu_withdraw_time: int = 100 # 撤回时间 25 | setu_max_num: int = 10 # 最大数量 26 | group_forward_msg: bool = False # 是否合并转发 27 | 28 | setu_nsfw_path: Union[bool, str] = False 29 | setu_sfw_path: Union[bool, str] = False 30 | scientific_agency: Optional[str] = None # 科学上网代理地址 31 | setu_quality: List[int] = [5, 75] 32 | sfw_withdraw: bool = True 33 | 34 | 35 | config = parse_obj_as(Config, get_driver().config.dict()) 36 | 37 | # 规范取值范围 38 | config.setu_cd = max(0, config.setu_cd) # setu_cd不能小于0 39 | config.setu_withdraw_time = max( 40 | 0, config.setu_withdraw_time 41 | ) # 撤回时间不能小于0, 大于100 42 | config.setu_withdraw_time = min(100, config.setu_withdraw_time) 43 | config.setu_max_num = max(1, config.setu_max_num) # setu_max_num不能大于1小于25 44 | config.setu_max_num = min(25, config.setu_max_num) 45 | 46 | 47 | config.setu_nsfw_path = ( 48 | f"{config.setu_save}/nsfw" if isinstance(config.setu_save, str) else False 49 | ) # nsfw保存路径 50 | config.setu_sfw_path = ( 51 | f"{config.setu_save}/sfw" if isinstance(config.setu_save, str) else False 52 | ) # sfw保存路径 53 | 54 | 55 | try: 56 | if isinstance(config.setu_nsfw_path, str): 57 | Path(config.setu_nsfw_path).mkdir(0o755, parents=True, exist_ok=True) 58 | if isinstance(config.setu_sfw_path, str): 59 | Path(config.setu_sfw_path).mkdir(0o755, parents=True, exist_ok=True) 60 | except Exception as e: 61 | logger.error(f"创建文件夹失败: {repr(e)}") 62 | config.setu_nsfw_path = False 63 | config.setu_sfw_path = False 64 | config.setu_save = False 65 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/fetch_resources.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | from httpx import AsyncClient 5 | from nonebot.log import logger 6 | 7 | from .config import config 8 | 9 | 10 | async def download_database() -> str: 11 | """ 12 | 下载更新数据库 13 | """ 14 | 15 | headers = { 16 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) " 17 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", 18 | } 19 | async with AsyncClient(proxies=config.scientific_agency) as client: 20 | re = await client.get(url=config.database_path, headers=headers, timeout=120) 21 | if re.status_code == 200: 22 | with open(Path(__file__).parent / "resource/lolicon.db", "wb") as f: 23 | f.write(re.content) 24 | logger.success("成功获取lolicon.db") 25 | return "成功获取lolicon.db" 26 | else: 27 | logger.error(f"获取 lolicon.db 失败: {re.status_code}") 28 | return f"获取 lolicon.db 失败: {re.status_code}" 29 | 30 | 31 | async def download_pic(url: str, client: AsyncClient) -> Union[bytes, int]: 32 | """ 33 | 下载图片并且返回content(bytes),或者status_code 34 | 35 | params: url: 下载图片的地址 36 | """ 37 | 38 | try: 39 | re = await client.get( 40 | url=url, timeout=120, headers={"Referer": "https://www.pixiv.net/"} 41 | ) 42 | if re.status_code != 200: 43 | return re.status_code 44 | logger.success("成功获取图片") 45 | return re.content 46 | except Exception: 47 | return 408 48 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/get_data.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import random 4 | import sqlite3 5 | from io import BytesIO 6 | from pathlib import Path 7 | from typing import Dict, List, Union 8 | 9 | from httpx import AsyncClient 10 | from loguru import logger 11 | from PIL import Image 12 | 13 | from .config import config 14 | from .fetch_resources import download_pic 15 | from .permission_manager import pm 16 | 17 | 18 | class GetData: 19 | def __init__(self) -> None: 20 | """ 21 | 初始化保存图片的路径以及该路径下的所有文件名 22 | """ 23 | 24 | self.all_file_name: Dict[str, set] = {"nsfw": set(), "sfw": set()} 25 | if config.setu_save: 26 | self.all_file_name["nsfw"] = set(os.listdir(config.setu_nsfw_path)) 27 | self.all_file_name["sfw"] = set(os.listdir(config.setu_sfw_path)) 28 | self.database_path: Path = Path(__file__).parent / "resource/lolicon.db" 29 | 30 | @staticmethod 31 | async def change_pixel(image, quality: int) -> bytes: 32 | """ 33 | 图像镜像左右翻转, 并且随机修改左上角一个像素点 34 | """ 35 | 36 | image = image.transpose(Image.FLIP_LEFT_RIGHT) 37 | image = image.convert("RGB") 38 | image.load()[0, 0] = ( 39 | random.randint(0, 255), 40 | random.randint(0, 255), 41 | random.randint(0, 255), 42 | ) 43 | byte_data = BytesIO() 44 | image.save(byte_data, format="JPEG", quality=quality) 45 | return byte_data.getvalue() 46 | 47 | async def update_status_unavailable(self, urls: str) -> None: 48 | """ 49 | 更新数据库中的图片状态为unavailable 50 | 51 | params: urls: 图片的url 52 | """ 53 | 54 | conn = sqlite3.connect(self.database_path) 55 | cur = conn.cursor() 56 | sql = f"UPDATE main set status='unavailable' where urls='{urls}'" # 手搓sql语句 57 | cur.execute(sql) 58 | conn.commit() 59 | conn.close() 60 | 61 | async def get_setu( 62 | self, 63 | keywords: List[str], 64 | num: int = 1, 65 | r18: bool = False, 66 | quality: int = 75, 67 | ) -> List[list]: 68 | """ 69 | 返回列表,内容为setu消息(列表套娃) 70 | [ 71 | [图片(bytes), data(图片信息), True(是否拿到了图), setu_url], 72 | [Error(错误), message(错误信息), False(是否拿到了图), setu_url] 73 | ] 74 | 75 | params: keywords: 关键词列表 76 | num: 数量 77 | r18: 是否r18 78 | quality: 图片质量 79 | """ 80 | 81 | data = [] 82 | conn = sqlite3.connect(self.database_path) # 连接数据库 83 | cur = conn.cursor() 84 | # sql操作,根据keyword和r18进行查询拿到数据 85 | if not keywords: 86 | sql = f"SELECT pid,title,author,r18,tags,urls from main where r18={r18} and status!='unavailable' order by random() limit {num}" 87 | elif len(keywords) == 1: 88 | sql = f"SELECT pid,title,author,r18,tags,urls from main where (tags like '%{keywords[0]}%' or title like '%{keywords[0]}%' or author like '%{keywords[0]}%') and r18={r18} and status!='unavailable' order by random() limit {num}" 89 | else: # 多tag的情况下的sql语句 90 | tag_sql = "".join( 91 | f"tags like '%{i}%'" if i == keywords[-1] else f"tags like '%{i}%' and " 92 | for i in keywords 93 | ) 94 | sql = f"SELECT pid,title,author,r18,tags,urls from main where (({tag_sql}) and r18={r18} and status!='unavailable') order by random() limit {num}" 95 | db_data = cur.execute(sql).fetchall() 96 | # 断开数据库连接 97 | conn.close() 98 | # 如果没有返回结果 99 | if db_data == []: 100 | raise ValueError(f"图库中没有搜到关于{keywords}的图。") 101 | # 并发下载图片 102 | async with AsyncClient(proxies=config.scientific_agency) as client: 103 | tasks = [ 104 | self.pic(setu, quality, client, pm.read_proxy()) for setu in db_data 105 | ] 106 | data = await asyncio.gather(*tasks) 107 | return data 108 | 109 | async def pic( 110 | self, setu: List, quality: int, client: AsyncClient, setu_proxy: str 111 | ) -> List: # sourcery skip: low-code-quality 112 | """ 113 | 返回setu消息列表 114 | [Error(错误), message(错误信息), False(是否拿到了图), setu_url] 115 | 或者 116 | [图片(bytes), data(图片信息), True(是否拿到了图), setu_url] 117 | 118 | params: setu: 数据库中的一条数据 119 | quality: 图片质量 120 | client: httpx的AsyncClient 121 | setu_proxy: 反向代理的url 122 | """ 123 | 124 | setu_pid: int = setu[0] # pid 125 | setu_title: str = setu[1] # 标题 126 | setu_author: str = setu[2] # 作者 127 | setu_r18: str = "True" if setu[3] == 1 else "False" # r18 128 | setu_tags: str = setu[4] # 标签 129 | setu_url: str = setu[5].replace("i.pixiv.re", setu_proxy) # 图片url 130 | 131 | data = f"标题:{setu_title}\npid:{setu_pid}\n画师:{setu_author}" 132 | 133 | logger.info(f"\n{data}\ntags:{setu_tags}\nR18:{setu_r18}") # 打印信息 134 | file_name = setu_url.split("/")[-1] # 获取文件名 135 | 136 | # 判断文件是否本地存在 137 | is_nsfw = setu_r18 == "True" 138 | # 当不保存的时候, save_path为False, 保存的时候, save_path为路径 139 | save_path: Union[bool, str] = ( 140 | config.setu_nsfw_path if is_nsfw else config.setu_sfw_path 141 | ) 142 | is_in_all_file_name = ( 143 | file_name in self.all_file_name["nsfw" if is_nsfw else "sfw"] 144 | ) 145 | if is_in_all_file_name: 146 | logger.info("图片本地存在") 147 | try: 148 | image = Image.open(f"{save_path}/{file_name}") # 尝试打开图片 149 | except Exception as e: 150 | return [ 151 | "Error", 152 | f"本地图片打开失败, 错误信息: {repr(e)}\nfile_name:{file_name}", 153 | False, 154 | setu_url, 155 | ] 156 | else: 157 | logger.info(f"图片本地不存在,正在去{setu_proxy}下载") 158 | content: Union[bytes, int] = await download_pic(setu_url, client) 159 | if isinstance( 160 | content, int 161 | ): # 如果返回的是int, 那么就是状态码, 表示下载失败 162 | if ( 163 | content == 404 164 | ): # 如果是404, 404表示文件不存在, 说明作者删除了图片, 那么就把这个url的status改为unavailable, 下次sql操作的时候就不会再拿到这个url了 165 | await self.update_status_unavailable( 166 | setu[5] 167 | ) # setu[5]是原始url, 不能拿换过代理的url 168 | logger.error(f"图片下载失败, 状态码: {content}") # 返回错误信息 169 | return ["Error", f"图片下载失败, 状态码: {content}", False, setu_url] 170 | # 错误处理, 如果content是空bytes, 那么Image.open会报错, 跳到except, 如果能打开图片, 图片应该不成问题, 171 | try: 172 | image = Image.open(BytesIO(content)) # 打开图片 173 | except Exception as e: 174 | return ["Error", f"图片打开失败, 错误信息: {repr(e)}", False, setu_url] 175 | # 保存图片, 如果save_path不为空, 以及图片不在all_file_name中, 那么就保存图片 176 | if save_path: 177 | try: 178 | with open(f"{save_path}/{file_name}", "wb") as f: 179 | f.write(content) 180 | self.all_file_name["nsfw" if is_nsfw else "sfw"].add(file_name) 181 | except Exception as e: 182 | logger.error(f"图片存储失败: {repr(e)}") 183 | try: 184 | # 尝试修改图片 185 | pic = await self.change_pixel(image, quality) 186 | return [pic, data, True, setu_url] 187 | except Exception as e: 188 | return ["Error", f"图片处理失败: {repr(e)}", False, setu_url] 189 | 190 | 191 | # 实例化 192 | get_data = GetData() 193 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/mamager_handle.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent 5 | from nonebot.matcher import Matcher 6 | from nonebot.params import CommandArg 7 | 8 | from .fetch_resources import download_database 9 | from .permission_manager import pm 10 | from .setu_message import HELP_MSG 11 | 12 | 13 | class ManagerHandle: 14 | """ 15 | 这个类里面是一大堆setu其他指令的handle函数 16 | """ 17 | 18 | @staticmethod 19 | def verify_sid(sid: str) -> bool: 20 | """验证会话sid是否合法""" 21 | try: 22 | stype, sid = sid.split("_") 23 | return bool(stype in ["group", "user"] and sid.isdigit()) 24 | except Exception: 25 | return False 26 | 27 | async def open_setu( 28 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 29 | ) -> None: 30 | """ 31 | 开启或关闭会话的setu功能 32 | """ 33 | 34 | # 获取命令后面的参数 35 | msg = cmd.extract_plain_text().strip() 36 | # 分析是新增还是删除 37 | if "add" in msg: 38 | add_mode = True 39 | elif "del" in msg: 40 | add_mode = False 41 | else: 42 | await matcher.finish( 43 | f"无效参数: {msg}, 请输入 add 或 del 为参数, eg: add group_114514" 44 | ) 45 | 46 | if isinstance(event, GroupMessageEvent): 47 | sid = f"group_{str(event.group_id)}" 48 | else: 49 | sid = msg.replace("add", "").replace("del", "").strip() 50 | if not self.verify_sid(sid): 51 | await matcher.finish(f"无效目标对象: {sid}") 52 | await matcher.finish(pm.update_white_list(sid, add_mode)) 53 | 54 | async def set_r18( 55 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 56 | ) -> None: 57 | """ 58 | 开启或关闭会话的r18模式 59 | """ 60 | 61 | # 获取命令后面的参数 62 | msg = cmd.extract_plain_text().strip() 63 | # 分析是开启还是关闭 64 | if "on" in msg: 65 | r18mode = True 66 | elif "off" in msg: 67 | r18mode = False 68 | else: 69 | await matcher.finish( 70 | f"无效参数: {msg}, 请输入 on 或 off 为参数, eg: on group_114514" 71 | ) 72 | if isinstance(event, GroupMessageEvent): 73 | sid = f"group_{str(event.group_id)}" 74 | else: 75 | sid = msg.replace("on", "").replace("off", "").strip() 76 | if not self.verify_sid(sid): 77 | await matcher.finish(f"无效目标对象: {sid}") 78 | await matcher.finish(pm.update_r18(sid, r18mode)) 79 | 80 | async def set_cd( 81 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 82 | ) -> None: 83 | """ 84 | 获取setu的cd时间 85 | """ 86 | 87 | # 获取命令后面的参数 88 | msg = cmd.extract_plain_text().strip() 89 | if msg.isdigit() and isinstance(event, GroupMessageEvent): 90 | cd_time = int(msg) 91 | sid = f"group_{str(event.group_id)}" 92 | await matcher.finish(pm.update_cd(sid, cd_time)) 93 | else: 94 | args = msg.split(" ") 95 | if len(args) != 2 or not args[0].isdigit() or not self.verify_sid(args[1]): 96 | await matcher.finish( 97 | f"无效参数: {msg}, 请输入\n正整数或零 + 空格 + 会话类型_会话id\n为参数\n例如: 0 group_114514" 98 | ) 99 | else: 100 | cd_time = int(args[0]) 101 | sid = args[1] 102 | await matcher.finish(pm.update_cd(sid, cd_time)) 103 | 104 | async def set_wd( 105 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 106 | ) -> None: 107 | """ 108 | 获取setu的撤回时间 109 | """ 110 | 111 | # 获取命令后面的参数 112 | msg = cmd.extract_plain_text().strip() 113 | if msg.isdigit() and isinstance(event, GroupMessageEvent): 114 | wd_time = int(msg) 115 | sid = f"group_{str(event.group_id)}" 116 | await matcher.finish(pm.update_withdraw_time(sid, wd_time)) 117 | else: 118 | args = msg.split(" ") 119 | if len(args) != 2 or not args[0].isdigit() or not self.verify_sid(args[1]): 120 | await matcher.finish( 121 | f"无效参数: {msg}, 请输入\n正整数 + 空格 + 会话类型_会话id\n为参数\n例如: 100 group_114514" 122 | ) 123 | else: 124 | wd_time = int(args[0]) 125 | sid = args[1] 126 | await matcher.finish(pm.update_withdraw_time(sid, wd_time)) 127 | 128 | async def set_maxnum( 129 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 130 | ) -> None: 131 | """ 132 | 获取一次性setu的最大张数 133 | """ 134 | 135 | # 获取命令后面的参数 136 | msg = cmd.extract_plain_text().strip() 137 | if msg.isdigit() and isinstance(event, GroupMessageEvent): 138 | max_num = int(msg) 139 | sid = f"group_{str(event.group_id)}" 140 | await matcher.finish(pm.update_max_num(sid, max_num)) 141 | else: 142 | args = msg.split(" ") 143 | if len(args) != 2 or not args[0].isdigit() or not self.verify_sid(args[1]): 144 | await matcher.finish( 145 | f"无效参数: {msg}, 请输入\n正整数 + 空格 + 会话类型_会话id\n为参数\n例如: 10 group_114514" 146 | ) 147 | else: 148 | max_num = int(args[0]) 149 | sid = args[1] 150 | await matcher.finish(pm.update_max_num(sid, max_num)) 151 | 152 | async def ban_setu( 153 | self, matcher: Matcher, event: MessageEvent, cmd: Message = CommandArg() 154 | ) -> None: 155 | """ 156 | 开启或关闭会话的setu功能 157 | """ 158 | 159 | # 获取命令后面的参数 160 | msg = cmd.extract_plain_text().strip() 161 | # 分析是新增还是删除 162 | if "add" in msg: 163 | add_mode = True 164 | elif "del" in msg: 165 | add_mode = False 166 | else: 167 | await matcher.finish( 168 | f"无效参数: {msg}, 请输入 add 或 del 为参数, eg: add group_114514" 169 | ) 170 | 171 | if isinstance(event, GroupMessageEvent): 172 | sid = f"group_{str(event.group_id)}" 173 | else: 174 | sid = msg.replace("add", "").replace("del", "").strip() 175 | if not self.verify_sid(sid): 176 | await matcher.finish(f"无效目标对象: {sid}") 177 | await matcher.finish(pm.update_ban_list(sid, add_mode)) 178 | 179 | @staticmethod 180 | async def setu_help( 181 | matcher: Matcher, 182 | ) -> None: 183 | """ 184 | setu指令帮助 185 | """ 186 | 187 | await matcher.finish(HELP_MSG) 188 | 189 | @staticmethod 190 | async def setu_db( 191 | matcher: Matcher, 192 | ) -> None: 193 | """ 194 | 拉取数据库 195 | """ 196 | 197 | await matcher.send( 198 | "此功能由于大陆对github的半墙, 国内服务器可能造成数据丢失或无法写入等错误, 不确定性较大, 万一数据库丢失请重新clone" 199 | ) 200 | try: 201 | remsg = await download_database() 202 | except Exception as e: 203 | remsg = f"获取 lolicon.db 失败: {repr(e)}" 204 | await matcher.finish(remsg) 205 | 206 | @staticmethod 207 | async def query_black_white_list(matcher: Matcher) -> None: 208 | """ 209 | 查新黑白名单 210 | """ 211 | 212 | res = pm.read_cfg() 213 | key_list = list(res.keys()) # 拿到所有的keys 214 | for element in ["ban", "last", "proxy"]: 215 | if element in key_list: 216 | key_list.remove(element) 217 | # 黑名单内容则在res['ban'] 218 | try: 219 | await matcher.send(f"白名单: {key_list}\n\n黑名单: {res['ban']}") 220 | except KeyError: 221 | await matcher.send(f"白名单: {key_list}\n\n黑名单为空") 222 | 223 | @staticmethod 224 | async def set_proxy(proxy) -> str: 225 | """ 226 | 设置代理并且ping 227 | """ 228 | 229 | pm.update_proxy(proxy) 230 | plat = platform.system().lower() # 获取系统 231 | return ( 232 | os.popen(f"ping {proxy}").read() 233 | if plat == "windows" 234 | else os.popen(f"ping -c 4 {proxy}").read() 235 | ) 236 | 237 | async def replace_proxy_got(self, matcher: Matcher, event: MessageEvent) -> None: 238 | """ 239 | 没参数的情况下 240 | """ 241 | 242 | msg: str = str(event.get_message()) # 获取消息文本 243 | if not msg or msg.isspace(): 244 | await matcher.finish("需要输入proxy") 245 | await matcher.send(f"{msg}已经替换, 正在尝试ping操作验证连通性") # 发送消息 246 | result = await self.set_proxy(msg.strip()) 247 | await matcher.send( 248 | f"{result}\n如果丢失的数据比较多, 请考虑重新更换代理" 249 | ) # 发送消息 250 | 251 | async def replace_proxy( 252 | self, matcher: Matcher, arg: Message = CommandArg() 253 | ) -> None: 254 | """ 255 | 有参数的情况 256 | """ 257 | 258 | msg = arg.extract_plain_text().strip() # 获取消息文本 259 | if not msg or msg.isspace(): 260 | await matcher.pause( 261 | f"请输入你要替换的proxy, 当前proxy为:{pm.read_proxy()}\ntips: 一些也许可用的proxy\ni.pixiv.re\nsex.nyan.xyz\npx2.rainchan.win\npximg.moonchan.xyz\npiv.deception.world\npx3.rainchan.win\npx.s.rainchan.win\npixiv.yuki.sh\npixiv.kagarise.workers.dev\nsetu.woshishaluan.top\npixiv.a-f.workers.dev\n等等....\n\neg:px2.rainchan.win\n警告:不要尝试命令行注入其他花里胡哨的东西, 可能会损伤你的电脑" 262 | ) 263 | else: 264 | await matcher.send(f"{msg}已经替换, 正在尝试ping操作验证连通性") # 发送消息 265 | result = await self.set_proxy(msg) 266 | await matcher.finish( 267 | f"{result}\n如果丢失的数据比较多, 请考虑重新更换代理" 268 | ) # 发送消息 269 | 270 | 271 | manager_handle = ManagerHandle() 272 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/permission_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import time 4 | 5 | from loguru import logger 6 | 7 | from .config import DATA_PATH, config 8 | from .setu_message import setu_sendcd 9 | 10 | """ 11 | { 12 | 'group_114':{ 13 | 'cd' : 30, # cd时长 14 | 'r18' : True, # r18开关 15 | 'withdraw' : 100, # 撤回延时 16 | 'maxnum' : 10 # 单次最高张数 17 | }, 18 | 'last':{ 19 | 'user_1919' : 810 # 最近一次发送setu的时间 20 | }, 21 | 'ban':[ 22 | 'user_1919', # 禁用的群组或用户,跨会话生效,会覆盖白名单设置 23 | 'group_810' 24 | ], 25 | 'proxy':'i.pixiv.re' # 代理地址 26 | } 27 | """ 28 | 29 | 30 | class PermissionManager: 31 | def __init__(self) -> None: 32 | """ 33 | 初始化一些配置 34 | """ 35 | 36 | self.setu_perm_cfg_filepath = DATA_PATH / "setu_perm_cfg.json" 37 | self.setu_cd = config.setu_cd 38 | self.setu_withdraw_time = config.setu_withdraw_time 39 | self.setu_max_num = config.setu_max_num 40 | self.setu_disable_wlist = config.setu_disable_wlist 41 | self.setu_enable_private = config.setu_enable_private 42 | self.group_forward_msg = config.group_forward_msg 43 | self.read_cfg() 44 | 45 | def read_cfg(self) -> dict: 46 | """ 47 | 读取配置文件 48 | """ 49 | 50 | try: 51 | # 尝试读取 52 | with open(self.setu_perm_cfg_filepath, "r", encoding="utf-8") as f: 53 | self.cfg: dict = json.loads(f.read()) 54 | except Exception as e: 55 | # 读取失败 56 | logger.warning(f"setu_perm_cfg.json 读取失败, 尝试重建\n{repr(e)}") 57 | self.cfg = {"proxy": "i.pixiv.re"} 58 | self.write_cfg() 59 | return self.cfg 60 | 61 | def write_cfg(self): 62 | """ 63 | 写入配置文件 64 | """ 65 | 66 | with open(self.setu_perm_cfg_filepath, "w", encoding="utf-8") as f: 67 | f.write(json.dumps(self.cfg)) 68 | 69 | # --------------- 文件读写 开始 --------------- 70 | 71 | # --------------- 查询系统 开始 --------------- 72 | def read_last_send(self, session_id: str) -> float: 73 | """ 74 | 查询最后一次发送时间 75 | """ 76 | 77 | try: 78 | return self.cfg["last"][session_id] 79 | except KeyError: 80 | return 0 81 | 82 | def read_cd(self, session_id: str) -> int: 83 | """ 84 | 查询cd 85 | """ 86 | 87 | try: 88 | return self.cfg[session_id]["cd"] 89 | except KeyError: 90 | return self.setu_cd 91 | 92 | def read_withdraw_time(self, session_id: str) -> int: 93 | """ 94 | 查询撤回时间 95 | """ 96 | 97 | try: 98 | return self.cfg[session_id]["withdraw"] 99 | except KeyError: 100 | return self.setu_withdraw_time 101 | 102 | def read_max_num(self, session_id: str) -> int: 103 | """ 104 | 查询最大张数 105 | """ 106 | 107 | try: 108 | return self.cfg[session_id]["maxnum"] 109 | except KeyError: 110 | return self.setu_max_num 111 | 112 | def read_r18(self, session_id: str) -> bool: 113 | """ 114 | 查询r18 115 | """ 116 | 117 | try: 118 | return self.cfg[session_id]["r18"] 119 | except KeyError: 120 | return False 121 | 122 | def read_ban_list(self, session_id: str) -> bool: 123 | """ 124 | 查询黑名单 125 | """ 126 | 127 | try: 128 | return session_id in self.cfg["ban"] 129 | except KeyError: 130 | return False 131 | 132 | # --------------- 查询系统 结束 --------------- 133 | 134 | # --------------- 逻辑判断 开始 --------------- 135 | def check_permission( 136 | self, session_id: str, r18flag: bool, num: int, user_type: str = "group" 137 | ): 138 | """ 139 | 查询权限, 并返回修正过的参数 140 | 141 | Args: 142 | sessionId (str): [会话信息] 143 | r18flag (bool): [是否开启r18] 144 | num (int): [需求张数] 145 | su (bool, optional): [是否为管理员]. Defaults to False. 146 | 147 | Raises: 148 | PermissionError: [未在白名单] 149 | PermissionError: [cd时间未到] 150 | 151 | Returns: 152 | [bool, int, int]: [r18是否启用, 图片张数, 撤回时间] 153 | """ 154 | 155 | # 优先采用黑名单检查 156 | if self.read_ban_list(session_id): 157 | logger.warning(f"涩图功能对 {session_id} 禁用!") 158 | raise PermissionError(f"该功能对 {session_id} 禁用!") 159 | # 采用白名单检查, 如果白名单被禁用则跳过 160 | if ( 161 | not self.setu_disable_wlist 162 | and ( 163 | user_type == "group" 164 | or ((not self.setu_enable_private) and user_type == "private") 165 | ) 166 | and session_id not in self.cfg.keys() 167 | ): 168 | logger.warning(f"涩图功能在 {session_id} 会话中未启用") 169 | raise PermissionError("该功能在此会话中未启用!") 170 | 171 | # 查询冷却时间 172 | tile_left = ( 173 | self.read_cd(session_id) + self.read_last_send(session_id) - time.time() 174 | ) 175 | if tile_left > 0 and user_type != "SU": 176 | hours, minutes, seconds = 0, 0, 0 177 | if tile_left >= 60: 178 | minutes, seconds = divmod(tile_left, 60) 179 | hours, minutes = divmod(minutes, 60) 180 | else: 181 | seconds = tile_left 182 | cd_msg = f"{f'{str(round(hours))}小时' if hours else ''}{f'{str(round(minutes))}分钟' if minutes else ''}{f'{str(round(seconds, 3))}秒' if seconds else ''}" 183 | logger.warning(f"setu的cd还有{cd_msg}") 184 | raise PermissionError(f"{random.choice(setu_sendcd)} 你的CD还有{cd_msg}!") 185 | 186 | # 检查r18权限, 图片张数, 撤回时间 187 | r18 = bool(r18flag and self.read_r18(session_id)) 188 | num_ = ( 189 | num 190 | if num <= self.read_max_num(session_id) 191 | else self.read_max_num(session_id) 192 | ) 193 | return r18, num_, self.read_withdraw_time(session_id) 194 | 195 | # --------------- 逻辑判断 结束 --------------- 196 | 197 | # --------------- 冷却更新 开始 --------------- 198 | def update_last_send(self, session_id: str): 199 | """ 200 | 更新最后一次发送时间 201 | """ 202 | 203 | try: 204 | self.cfg["last"][session_id] = time.time() 205 | except KeyError: 206 | self.cfg["last"] = {session_id: time.time()} 207 | 208 | # --------------- 冷却更新 结束 --------------- 209 | 210 | # --------------- 增删系统 开始 --------------- 211 | def update_white_list(self, session_id: str, add_mode: bool) -> str: 212 | """ 213 | 更新白名单 214 | """ 215 | 216 | if add_mode: 217 | if session_id in self.cfg.keys(): 218 | return f"{session_id}已在白名单" 219 | self.cfg[session_id] = {} 220 | self.write_cfg() 221 | return f"成功添加{session_id}至白名单" 222 | # 移除出白名单 223 | else: 224 | if session_id in self.cfg.keys(): 225 | self.cfg.pop(session_id) 226 | self.write_cfg() 227 | return f"成功移除{session_id}出白名单" 228 | return f"{session_id}不在白名单" 229 | 230 | def update_cd(self, session_id: str, cd_time: int) -> str: 231 | """ 232 | 更新cd时间 233 | """ 234 | 235 | # 检查是否已在白名单, 不在则结束 236 | if session_id not in self.cfg.keys(): 237 | return f"{session_id}不在白名单, 请先添加至白名单后操作" 238 | # 检查数据是否超出范围,超出则设定至范围内 239 | cd_time = max(cd_time, 0) 240 | # 读取原有数据 241 | try: 242 | cd_time_old = self.cfg[session_id]["cd"] 243 | except Exception: 244 | cd_time_old = "未设定" 245 | # 写入新数据 246 | self.cfg[session_id]["cd"] = cd_time 247 | self.write_cfg() 248 | # 返回信息 249 | return f"成功更新冷却时间 {cd_time_old} -> {cd_time}" 250 | 251 | def update_withdraw_time(self, session_id: str, withdraw_time: int) -> str: 252 | """ 253 | 更新撤回时间 254 | """ 255 | 256 | # 检查是否已在白名单, 不在则结束 257 | if session_id not in self.cfg.keys(): 258 | return f"{session_id}不在白名单, 请先添加至白名单后操作" 259 | # 检查数据是否超出范围,超出则设定至范围内 260 | withdraw_time = max(withdraw_time, 0) 261 | withdraw_time = min(withdraw_time, 100) 262 | # 读取原有数据 263 | try: 264 | withdraw_time_old = self.cfg[session_id]["withdraw"] 265 | except KeyError: 266 | withdraw_time_old = "未设定" 267 | # 写入新数据 268 | self.cfg[session_id]["withdraw"] = withdraw_time 269 | self.write_cfg() 270 | # 返回信息 271 | return f"成功更新撤回时间 {withdraw_time_old} -> {withdraw_time}" 272 | 273 | def update_max_num(self, session_id: str, max_num: int) -> str: 274 | """ 275 | 更新最大张数 276 | """ 277 | 278 | # 检查是否已在白名单, 不在则结束 279 | if session_id not in self.cfg.keys(): 280 | return f"{session_id}不在白名单, 请先添加至白名单后操作" 281 | # 检查数据是否超出范围,超出则设定至范围内 282 | max_num = max(max_num, 1) 283 | max_num = min(max_num, 25) 284 | # 读取原有数据 285 | try: 286 | max_num_old = self.cfg[session_id]["maxnum"] 287 | except KeyError: 288 | max_num_old = "未设定" 289 | # 写入新数据 290 | self.cfg[session_id]["maxnum"] = max_num 291 | self.write_cfg() 292 | # 返回信息 293 | return f"成功更新最大张数 {max_num_old} -> {max_num}" 294 | 295 | def update_r18(self, session_id: str, r18_mode: bool) -> str: 296 | # sourcery skip: extract-duplicate-method 297 | """ 298 | 更新r18权限 299 | """ 300 | 301 | # 检查是否已在白名单, 不在则结束 302 | if session_id not in self.cfg.keys(): 303 | return f"{session_id}不在白名单, 请先添加至白名单后操作" 304 | 305 | if r18_mode: 306 | if self.read_r18(session_id): 307 | return f"{session_id}已开启r18" 308 | self.cfg[session_id]["r18"] = True 309 | self.write_cfg() 310 | return f"成功开启{session_id}的r18权限" 311 | else: 312 | if self.read_r18(session_id): 313 | self.cfg[session_id]["r18"] = False 314 | self.write_cfg() 315 | return f"成功关闭{session_id}的r18权限" 316 | return f"{session_id}未开启r18" 317 | 318 | def update_ban_list(self, session_id: str, add_mode: bool) -> str: 319 | """ 320 | 更新黑名单 321 | """ 322 | 323 | if add_mode: 324 | try: 325 | if session_id in self.cfg["ban"]: 326 | return f"{session_id}已在黑名单" 327 | except KeyError: 328 | self.cfg["ban"] = [] 329 | self.cfg["ban"].append(session_id) 330 | self.write_cfg() 331 | return f"成功添加{session_id}至黑名单" 332 | # 移出黑名单 333 | else: 334 | try: 335 | self.cfg["ban"].remove(session_id) 336 | self.write_cfg() 337 | return f"成功移除{session_id}出黑名单" 338 | except ValueError: 339 | return f"{session_id}不在黑名单" 340 | 341 | def update_proxy(self, proxy: str) -> None: 342 | """ 343 | 更新代理 344 | """ 345 | 346 | self.cfg["proxy"] = proxy 347 | self.write_cfg() 348 | 349 | def read_proxy(self) -> str: 350 | """ 351 | 查询代理 352 | """ 353 | 354 | try: 355 | return self.cfg["proxy"] 356 | except KeyError: 357 | return "i.pixiv.re" 358 | 359 | # --------------- 增删系统 结束 --------------- 360 | 361 | 362 | # 实例化权限管理 363 | pm = PermissionManager() 364 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/resource/lolicon.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Special-Week/nonebot_plugin_setu4/a3cd0a92fe4449b8b5ea99dbc0a12b96b168aecf/nonebot_plugin_setu4/resource/lolicon.db -------------------------------------------------------------------------------- /nonebot_plugin_setu4/send_setu.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | import random 4 | import time 5 | from re import sub 6 | from typing import Tuple 7 | 8 | import nonebot 9 | from loguru import logger 10 | from nonebot.adapters.onebot.v11 import ( 11 | Bot, 12 | GroupMessageEvent, 13 | Message, 14 | MessageEvent, 15 | MessageSegment, 16 | ) 17 | from nonebot.matcher import Matcher 18 | from nonebot.params import RegexGroup 19 | 20 | from .config import config 21 | from .get_data import get_data 22 | from .permission_manager import pm 23 | from .setu_message import setu_sendmessage 24 | 25 | 26 | class SendSetu: 27 | def __init__(self) -> None: 28 | ... 29 | # ⡆⣐⢕⢕⢕⢕⢕⢕⢕⢕⠅⢗⢕⢕⢕⢕⢕⢕⢕⠕⠕⢕⢕⢕⢕⢕⢕⢕⢕⢕ 30 | # ⢐⢕⢕⢕⢕⢕⣕⢕⢕⠕⠁⢕⢕⢕⢕⢕⢕⢕⢕⠅⡄⢕⢕⢕⢕⢕⢕⢕⢕⢕ 31 | # ⢕⢕⢕⢕⢕⠅⢗⢕⠕⣠⠄⣗⢕⢕⠕⢕⢕⢕⠕⢠⣿⠐⢕⢕⢕⠑⢕⢕⠵⢕ 32 | # ⢕⢕⢕⢕⠁⢜⠕⢁⣴⣿⡇⢓⢕⢵⢐⢕⢕⠕⢁⣾⢿⣧⠑⢕⢕⠄⢑⢕⠅⢕ 33 | # ⢕⢕⠵⢁⠔⢁⣤⣤⣶⣶⣶⡐⣕⢽⠐⢕⠕⣡⣾⣶⣶⣶⣤⡁⢓⢕⠄⢑⢅⢑ 34 | # ⠍⣧⠄⣶⣾⣿⣿⣿⣿⣿⣿⣷⣔⢕⢄⢡⣾⣿⣿⣿⣿⣿⣿⣿⣦⡑⢕⢤⠱⢐ 35 | # ⢠⢕⠅⣾⣿⠋⢿⣿⣿⣿⠉⣿⣿⣷⣦⣶⣽⣿⣿⠈⣿⣿⣿⣿⠏⢹⣷⣷⡅⢐ 36 | # ⣔⢕⢥⢻⣿⡀⠈⠛⠛⠁⢠⣿⣿⣿⣿⣿⣿⣿⣿⡀⠈⠛⠛⠁⠄⣼⣿⣿⡇⢔ 37 | # ⢕⢕⢽⢸⢟⢟⢖⢖⢤⣶⡟⢻⣿⡿⠻⣿⣿⡟⢀⣿⣦⢤⢤⢔⢞⢿⢿⣿⠁⢕ 38 | # ⢕⢕⠅⣐⢕⢕⢕⢕⢕⣿⣿⡄⠛⢀⣦⠈⠛⢁⣼⣿⢗⢕⢕⢕⢕⢕⢕⡏⣘⢕ 39 | # ⢕⢕⠅⢓⣕⣕⣕⣕⣵⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣷⣕⢕⢕⢕⢕⡵⢀⢕⢕ 40 | # ⢑⢕⠃⡈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢃⢕⢕⢕ 41 | # ⣆⢕⠄⢱⣄⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢁⢕⢕⠕⢁ 42 | # ⣿⣦⡀⣿⣿⣷⣶⣬⣍⣛⣛⣛⡛⠿⠿⠿⠛⠛⢛⣛⣉⣭⣤⣂⢜⠕⢑⣡⣴⣿ 43 | 44 | @staticmethod 45 | def session_id(event: MessageEvent) -> str: 46 | """ 47 | 根据会话类型生成session_id, 一般返回str而不是None, 一般消息事件不是私聊就是群聊 48 | """ 49 | 50 | if isinstance(event, GroupMessageEvent): 51 | return f"group_{str(event.group_id)}" 52 | else: 53 | return f"user_{str(event.user_id)}" 54 | 55 | @staticmethod 56 | async def setu_handle( 57 | bot: Bot, matcher: Matcher, event: MessageEvent, args: Tuple = RegexGroup() 58 | ) -> None: # sourcery skip: low-code-quality 59 | """ 60 | 发送色图的处理函数 61 | """ 62 | 63 | # 获取用户输入的参数 64 | r18flag = args[2] 65 | key = sub("['\"]", "", args[3]) # 去掉引号防止sql注入 66 | num = int(sub(r"[张|个|份|x|✖️|×|X|*]", "", args[1])) if args[1] else 1 67 | 68 | # 根据会话类型生成session_id 69 | if isinstance(event, GroupMessageEvent): 70 | session_id = f"group_{event.group_id}" 71 | user_type = "group" 72 | else: 73 | session_id = f"user_{event.user_id}" 74 | user_type = "private" 75 | 76 | # 权限检查 77 | try: 78 | user_type = ( 79 | "SU" 80 | if (str(event.user_id) in nonebot.get_driver().config.superusers) 81 | else user_type 82 | ) 83 | r18, num, withdraw_time = pm.check_permission( 84 | session_id, r18flag, num, user_type 85 | ) 86 | except PermissionError as e: 87 | await matcher.finish(repr(e)) 88 | # 检查是否需要撤回, 如果withdraw_time为0 或者 用户在env设置了sfw_withdraw=False切图片非r18时, 那么就不撤回 89 | will_withdraw = bool(withdraw_time != 0 and (r18 or config.sfw_withdraw)) 90 | # quality = 95是最佳质量(图片质量太高发起来太费时间了) 91 | quality = 95 92 | if num > config.setu_quality[0]: 93 | quality = config.setu_quality[1] 94 | await matcher.send("由于数量过多请等待片刻") 95 | 96 | # key按照空格切割为数组, 用于多关键词搜索, 并且把数组中的空元素去掉 97 | key = [word.strip() for word in key.split(" ") if word.strip()] 98 | 99 | # 控制台输出 100 | flag_log = f"\nR18 == {r18}\nkeyword == {key}\nnum == {num}\n" 101 | logger.info(f"key = {key}\tr18 = {r18}\tnum = {num}") 102 | # 记录时间, 计算CD用 103 | pm.update_last_send(session_id) 104 | 105 | # data是数组套娃, 数组中的每个元素内容为: [图片, 信息, True/False, url] 106 | try: 107 | data = await get_data.get_setu(key, num, r18, quality) 108 | except Exception as e: 109 | await matcher.finish(repr(e)) 110 | 111 | # 发送的消息列表 112 | message_list = [] 113 | for pic in data: 114 | # 如果状态为True,说明图片拿到了 115 | if pic[2]: 116 | message_list.append( 117 | f"{random.choice(setu_sendmessage)}{flag_log}" 118 | + Message(pic[1]) 119 | + MessageSegment.image(pic[0]) 120 | ) 121 | flag_log = "" 122 | # 状态为false的消息,图片没拿到 123 | else: 124 | message_list.append(pic[0] + pic[1]) 125 | 126 | # 为后面撤回消息做准备 127 | setu_msg_id = [] 128 | 129 | # 尝试发送 130 | try: 131 | start_time = time.time() # 记录开始发送的时间 132 | # 如果是群聊并且env设置了群聊转发, 那么就转发 133 | if isinstance(event, GroupMessageEvent) and pm.group_forward_msg: 134 | msgs = [ 135 | { 136 | "type": "node", 137 | "data": { 138 | "name": "setu-bot", 139 | "uin": bot.self_id, 140 | "content": msg, 141 | }, 142 | } 143 | for msg in message_list 144 | ] 145 | # 发送转发消息, 并且记录消息id, 撤回用 146 | setu_msg_id.append( 147 | ( 148 | await bot.call_api( 149 | "send_group_forward_msg", 150 | group_id=event.group_id, 151 | messages=msgs, 152 | ) 153 | )["message_id"] 154 | ) 155 | else: 156 | # 非群聊直接发送 157 | for msg in message_list: 158 | setu_msg_id.append((await matcher.send(msg))["message_id"]) 159 | await asyncio.sleep(0.5) 160 | except Exception as e: 161 | logger.warning(repr(e)) 162 | await matcher.finish( 163 | message=f"消息可能被风控了,图发不出来,错误信息{repr(e)}" 164 | ) 165 | 166 | # 自动撤回涩图 167 | if will_withdraw: 168 | with contextlib.suppress(Exception): 169 | time_left = ( 170 | withdraw_time + start_time - time.time() 171 | ) # 计算从开始发送到目前仍剩余的保留时间 172 | await asyncio.sleep(1 if time_left <= 0 else time_left) 173 | for msg_id in setu_msg_id: 174 | await bot.delete_msg(message_id=msg_id) 175 | 176 | 177 | send_setu = SendSetu() 178 | -------------------------------------------------------------------------------- /nonebot_plugin_setu4/setu_message.py: -------------------------------------------------------------------------------- 1 | setu_sendmessage = [ 2 | "这是你的🐍图", 3 | "你是大色批", 4 | "看!要色图的色批出现了!", 5 | "喏,图", 6 | "给给给个🐍图", 7 | "色图有我好冲吗?", 8 | "呐呐呐,欧尼酱别看色图了呐", 9 | "有什么好色图有给发出来让大伙看看!", 10 | "没有,有也不给(骗你的~)", 11 | "天天色图色图的,今天就把你变成色图!", 12 | "咱没有色图(骗你的~)", 13 | "哈?你的脑子一天都在想些什么呢,咱才没有这种东西啦。", 14 | "呀!不要啊!等一...下~", 15 | "呜...不要啦!太色了咱~", 16 | "不要这样子啦(*/ω\*)", 17 | "Hen....Hentai!。", 18 | "讨....讨厌了(脸红)", 19 | "你想...想做什么///", 20 | "啊.....你...你要干什么?!走开.....走开啦大hentai!一巴掌拍飞!(╯‵□′)╯︵┻━┻", 21 | "变态baka死宅?", 22 | "已经可以了,现在很多死宅也都没你这么恶心了", 23 | "噫…你这个死变态想干嘛!居然想叫咱做这种事,死宅真恶心!快离我远点,我怕你污染到周围空气了(嫌弃脸)", 24 | "这么喜欢色图呢?不如来点岛风色图?", 25 | "hso!", 26 | "这么喜欢看色图哦?变态?", 27 | "eee,死肥宅不要啦!恶心心!", 28 | "不管到了哪里,变态可疑分子果然还是变态可疑分子呢!", 29 | "变态可疑分子先生,你要的色图", 30 | "不许导,积回去!", 31 | "∫1/(1+x^4)dx", 32 | "做涩涩的事情是对的!", 33 | "有色鬼,我不说是谁!", 34 | "涩涩了一整天,好累哦", 35 | "呃...好像冲了好多次...感觉不太好呢...", 36 | "注意身体,色图看太多对身体不好", 37 | ] 38 | setu_sendcd = [ 39 | "憋冲了!你已经冲不出来了!", 40 | "憋住,不准冲!", 41 | "你的色图不出来了!", 42 | "注意身体,色图看太多对身体不好", 43 | "憋再冲了!", 44 | "呃...好像冲了好多次...感觉不太好呢...", 45 | "???", 46 | "你急啥呢?", 47 | "你这么喜欢色图,还不快点冲!", 48 | ] 49 | 50 | HELP_MSG = r"""setu指令: 51 | ^(setu|色图|涩图|想色色|来份色色|来份色图|想涩涩|多来点|来点色图|来张setu|来张色图|来点色色|色色|涩涩)\s?([x|✖️|×|X|*]?\d+[张|个|份]?)?\s?(r18)?\s?(.*)? 52 | 53 | 白名单管理: 54 | setu_wl add 添加会话至白名单 eg: setu_wl add user_114514/group_1919810 55 | setu_wl del 移出会话自白名单 eg: setu_wl del user_114514/group_1919810 56 | 57 | 黑名单管理: 58 | setu_ban add 添加会话至黑名单 eg: setu_ban add user_114514/group_1919810 59 | setu_ban del 移出会话自黑名单 eg: setu_ban del user_114514/group_1919810 60 | 61 | r18模式管理: 62 | setu_r18 on 开启会话的r18模式 eg: setu_r18 on group_1919810 63 | setu_r18 off 关闭会话的r18模式 eg: setu_r18 off group_1919810 64 | 65 | cd时间更新: 66 | setu_cd xxx 更新会话的冷却时间, xxx为int类型的参数 eg: setu_cd 10 group_1919810 67 | 68 | 撤回时间更新: 69 | setu_wd xxx 撤回前等待的时间, xxx为int类型的参数 eg: setu_wd 10 group_1919810 70 | 71 | 最大张数更新: 72 | setu_mn xxx 单次发送的最大图片数, xxx为int类型的参数 eg: setu_mn 10 group_1919810 73 | 74 | 查询黑白名单: 75 | setu_roster 76 | 77 | 更换代理: 78 | setu_proxy xxx xxx为代理url, 例如i.pixiv.re""" 79 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "nonebot_plugin_setu4" 3 | version = "0.16.114514" 4 | description = "内置数据库的setu插件, 尝试降低因为风控发不出图的概率" 5 | authors = [{ name = "Special-Week", email = "HuaMing27499@gmail.com" }] 6 | dependencies = [ 7 | "pillow>=9.1.1", 8 | "nonebot2>=2.0.0b5", 9 | "nonebot-adapter-onebot>=2.2.3", 10 | "httpx", 11 | ] 12 | requires-python = ">=3.9.0" 13 | readme = "README.md" 14 | license = { text = "MIT" } 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow>=9.1.1 2 | nonebot2>=2.0.0b5 3 | nonebot-adapter-onebot>=2.2.3 4 | httpx --------------------------------------------------------------------------------