├── LICENSE ├── README.md ├── nonebot_plugin_setu ├── __init__.py ├── dao │ ├── __init__.py │ ├── group_dao.py │ ├── image_dao.py │ └── user_dao.py ├── file_tools.py ├── getPic.py ├── proxies.py ├── setu_api.py ├── utils.py └── withdraw.py ├── pyproject.toml └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | bw2k9A.png 7 |

nonebot_plugin_setu

8 | 基于nonebot2、loliconApi的涩图插件 9 |
10 | GitHub stars 11 | GitHub issues 12 | GitHub forks 13 | GitHub license 14 |
15 | 16 | ## 安装及更新 17 | 18 | - 安装 19 | 20 | ```bash 21 | nb plugin install nonebot_plugin_setu 22 | ``` 23 | 或者 24 | ```bash 25 | pip install nonebot_plugin_setu 26 | ``` 27 | 推荐使用`nb`进行安装 28 | - 更新 29 | 30 | ```bash 31 | nb plugin update nonebot_plugin_setu 32 | ``` 33 | 或者 34 | ```bash 35 | pip install nonebot_plugin_setu -U 36 | ``` 37 | - 导入插件 38 | 39 | 在`pyproject.toml`里的`[tool.nonebot]`中添加`plugins = ["nonebot_plugin_setu"]` 40 | 41 | **注**:如果你使用`nb`安装插件,则不需要设置此项 42 | 43 | 44 | ## 使用方式 45 | 46 |

47 | 48 | 49 | 50 |

51 | 52 | 53 | 首先运行一遍robot,然后在robot目录的data目录下修改setu_config.json配置文件,然后重启robot 54 | 55 | ### 添加配置 56 | 57 | - **在你的setu_config.json文件中修改如下配置:** 58 | 59 | SUPERUSERS = ["主人的qq号"],可添加多个 60 | 61 | PROXIES_HTTP = 'HTTP魔法地址(例如`http://127.0.0.1:7890`),这与你使用的魔法有关' 62 | 63 | PROXIES_SOCKS = 'SOCKS5魔法地址(例如`socks5://127.0.0.1:10808`),这与你使用的魔法有关' 64 | 65 | **注**:若没有魔法或者不会设置可不填 66 | 67 | 68 | 69 | ### 正式使用 70 | 71 | | 命令 | 举例 | 说明 | 72 | |----------------------------------|-----------------------------------|:-------------------------------------------------------------------------------------| 73 | | 下载涩图/色图+数量 | 下载涩图12345、下载色图12345 | 下载涩图:下载非涩涩图片;下载色图:下载色色图片 | 74 | | 涩图、setu、无内鬼、色图 | setu | 发送图片 | 75 | | @用户cd+时间(秒) | @张三cd12345 | 指定用户cd | 76 | | 群cd+时间(秒) | 群cd12345 | 指定群cd | 77 | | 开启/关闭在线发图 | 开启在线发图 | 在线发图开启之后,图片将不再从本地发送而是从网上下载后在线发送,不会占用服务器存储资源 | 78 | | 开启/关闭魔法 | 关闭魔法 | 魔法关闭之后,图片的下载以及在线发送将不再通过魔法而是通过镜像来完成,如果没有魔法或者不会设置推荐关闭 | 79 | | 涩图tagA和B和C(最多指定三个tag) | 涩图tag碧蓝航线、涩图tag公主连结和白丝 | 为了保证尽可能多地获取tag指定的内容,tag指定的图片都会在线获取而不从本地寻找,是否存储依然遵循在线发图开关 | 80 | | 撤回间隔+时间(秒) | 撤回间隔20、撤回间隔0 | 设置撤回间隔之后,机器人将会在指定间隔后撤回发送的图片,撤回间隔为0时,机器人将不会进行撤回。同时撤回间隔以群聊为单位,每个群都能设置不同的间隔,私聊将不会触发撤回操作 | 81 | | 涩图api、设置api地址+`服务器ip地址或域名:机器人端口` | 涩图api、设置api地址`123.456.789.0:8080` | 设置api并开放防火墙端口之后,就能把服务器中的图库数据转为api供他人调用,本地api调试请访问`http://localhost:机器人端口/setu/docs` | 82 | | 开启涩涩、开启私聊涩涩、关闭涩涩、关闭私聊涩涩 | 开启涩涩、开启私聊涩涩 | 开启涩涩之后,机器人将会发送色色图片,涩涩以群聊为单位,支持不同群是否开启 | 83 | | 涩图帮助 | 涩图帮助 | 获取命令列表 | 84 | | 涩图转发者名字 | 涩图转发者名字bot | 修改发送转发消息时的转发者名字 | 85 | 86 | #### 注: 87 | 88 | - 用户cd和群cd同时存在时,以用户cd为准 89 | - 群cd默认3600s 90 | - 开放api时请保证机器人监听的地址为0.0.0.0 91 | - 仅好友才能私聊发图,私聊发图时若未指定用户cd,则默认cd3600s 92 | 93 | ## TODO 94 | 95 | - [ ] 数据可视化 96 | 97 | 98 | 99 | ## 更新日志 100 | 101 | ### 2023/1/9 102 | 103 | - 修复无法捕获异常bug 104 | - 优化图片下载以及异常通知 105 | - 修复发送的图片为空白的bug 106 | 107 | ### 2022/12/27 108 | 109 | - 将图片发送方式改为转发 110 | - 新增修改转发者名字 111 | 112 | ### 2022/7/29[v1.1.2] 113 | 114 | - 增加帮助,修复下载图片失败bug 115 | 116 | ### 2022/4/2[v1.0.14] 117 | 118 | - 新增自动撤回 119 | - 新增自建图库与图库api 120 | - 新增涩涩模式 121 | 122 | 123 | 124 | ### 2022/3/19[v1.0.11] 125 | 126 | - 新增指定tag,可指定tag进行发图,tag最多指定三个 127 | 128 | 129 | 130 | ### 2022/3/17[v1.0.9] 131 | 132 | - 新增在线发图开关,图片可以在线发送而不占用服务器存储空间 133 | - 新增魔法开关,没有魔法也能够正常使用 134 | 135 | 注:旧版本用户请删除setu_config.json然后重新配置一遍 136 | 137 | 138 | 139 | ### 2022/3/13[v1.0.5] 140 | 141 | - 删除SETU_CD,修改cd配置,不再依赖userscd.json,转为依赖数据库文件 142 | - 添加用户cd和群cd,可由管理员进行指定和更改 143 | - 引入数据库存储图片信息,修改图片存储格式从jpg转为图片原本对应样式 144 | 145 | 注:旧版本用户请删除setu_config.json然后重新配置一遍 146 | 147 | 148 | 149 | ### 2022/3/9[v1.0.4] 150 | 151 | - 更改异常捕获范围,修复无法捕获异常的bug 152 | 153 | 154 | 155 | ### 2022/3/8[v1.0.3] 156 | 157 | - 删除配置:SETU_NUM,可下载指定数量的图片 158 | - 新增下载图片进度条 159 | 160 | 161 | 162 | ### 2022/3/5 [v1.0.1] 163 | 164 | - 支持nonebot[v2.0.0-beta2],请更新至最新版nonebot使用 165 | - 更改图片的名字为对应pid 166 | - 更改文件的配置方式,不再依赖.env文件 167 | 168 | 169 | 170 | ### 2022/1/26 [v1.0.0a1] 171 | 172 | - 支持nonebot[v2.0.0-beta1],beta1之前的请使用0.0.6版本 173 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import re 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | import httpx 9 | from nonebot import get_driver 10 | from nonebot.adapters.onebot.v11 import Bot, Message, Event, MessageSegment 11 | from nonebot.exception import NoneBotException 12 | from nonebot.log import logger 13 | from nonebot.plugin import on_regex 14 | 15 | from .file_tools import Config 16 | from .dao.group_dao import GroupDao 17 | from .dao.image_dao import ImageDao 18 | from .dao.user_dao import UserDao 19 | from .getPic import get_url, down_pic 20 | from .setu_api import setu_api 21 | from .utils import send_forward_msg, get_file_num, img_num_detect 22 | from .withdraw import add_withdraw_job 23 | 24 | setu = on_regex("^涩图$|^setu$|^无内鬼$|^色图$|^涩图tag.+$") 25 | downLoad = on_regex(r"^下载涩图[1-9]\d*$|^下载色图[1-9]\d*$") 26 | user_cd = on_regex(r"^\[CQ:at,qq=[1-9][0-9]{4,10}\] cd\d+$") 27 | group_cd = on_regex(r"^群cd0$|^群cd[1-9]\d*$") 28 | online_switch = on_regex(r"^开启在线发图$|^关闭在线发图$") 29 | proxy_switch = on_regex(r"^开启魔法$|^关闭魔法$") 30 | api = on_regex(r"^涩图api$|^设置api地址.+$") 31 | withdraw_interval = on_regex(r"^撤回间隔0$|^撤回间隔[1-9]\d*$") 32 | r18_switch = on_regex(r"^开启涩涩$|^关闭涩涩$|^开启私聊涩涩$|^关闭私聊涩涩$") 33 | setu_help = on_regex(r"^涩图帮助$") 34 | msg_forward_name = on_regex(r"^涩图转发者名字.+$") 35 | 36 | super_user = Config().super_users 37 | driver = get_driver() 38 | driver.server_app.mount('/setu', setu_api, name='setu_plugin') 39 | 40 | 41 | @setu.handle() 42 | async def _(bot: Bot, event: Event): 43 | bot_name = Config().get_file_args(args_name='FORWARD_NAME') 44 | is_group_chat = hasattr(event, 'group_id') 45 | r18 = UserDao().get_r18_private_chat() \ 46 | if not is_group_chat else GroupDao().get_group_r18(event.group_id) 47 | if img_num_detect(r18) == 0: 48 | await setu.finish('没有涩图啦,请下载或使用在线模式', at_sender=True) 49 | img_path = Path(f"loliconImages{'/r18' if r18 else ''}").resolve() 50 | images = os.listdir(img_path) 51 | if r18 == 0: 52 | images.remove('r18') 53 | file_name = '' if Config().online_switch else images[random.randint(0, len(images) - 1)] 54 | pid = re.sub(r'\D+', '', file_name) 55 | remain_time = 0 if event.get_user_id() in Config().super_users else UserDao().get_user_remain_time(event) 56 | if remain_time != 0: 57 | hour = int(remain_time / 3600) 58 | minute = int((remain_time / 60) % 60) 59 | await setu.finish(f'要等{hour}小时{minute}分钟才能再要涩图哦', at_sender=True) 60 | msg = event.get_plaintext() 61 | tag_flag = 0 62 | if bool(re.search(r"^涩图tag.+$", msg)): 63 | tag_flag = 1 64 | tags = re.sub(r'^涩图tag', '', msg).split('和') 65 | if len(tags) > 3: 66 | UserDao().delete_user_cd(event.get_user_id()) 67 | await setu.finish('涩图tag最多只能有三个哦', at_sender=True) 68 | else: 69 | try: 70 | file_name = await get_url(num=1, tags=tags, online_switch=Config().online_switch, r18=r18) 71 | except httpx.HTTPError: 72 | UserDao().delete_user_cd(event.get_user_id()) 73 | await setu.finish(message=Message('网络错误,请重试'), at_sender=True) 74 | except Exception as e: 75 | UserDao().delete_user_cd(event.get_user_id()) 76 | await setu.finish(message=Message(f'{e}'), at_sender=True) 77 | if Config().online_switch == 0: 78 | pid = re.sub(r'\D+', '', file_name) 79 | if file_name == "": 80 | UserDao().delete_user_cd(event.get_user_id()) 81 | await setu.finish('没有找到相关涩图,请更换tag', at_sender=True) 82 | interval = 0 if not is_group_chat else GroupDao().get_group_interval(event.group_id) 83 | try: 84 | if Config().online_switch == 1: 85 | img = file_name if tag_flag == 1 else await get_url(num=1, online_switch=1, r18=r18) 86 | message_list = [MessageSegment.image(img['base64']), f"https://pixiv.net/artworks/{img['pid']}"] 87 | msg_info = await send_forward_msg(bot, event, bot_name, bot.self_id, message_list, is_group_chat) 88 | else: 89 | message_list = [MessageSegment.image(f"file:///{img_path.joinpath(file_name)}"), 90 | f"https://pixiv.net/artworks/{pid}"] 91 | msg_info = await send_forward_msg(bot, event, bot_name, bot.self_id, message_list, is_group_chat) 92 | await add_withdraw_job(bot, **msg_info, withdraw_interval=interval) 93 | except httpx.HTTPError: 94 | UserDao().delete_user_cd(event.get_user_id()) 95 | await setu.finish(message=Message('获取图片出错,本次涩图不计入cd'), at_sender=True) 96 | except NoneBotException as e: 97 | UserDao().delete_user_cd(event.get_user_id()) 98 | logger.error(f"{type(e)}") 99 | await setu.finish(message=Message('机器人被风控了,本次涩图不计入cd'), at_sender=True) 100 | except Exception as e: 101 | UserDao().delete_user_cd(event.get_user_id()) 102 | await setu.finish(message=Message(f"{e},本次涩图不计入cd"), at_sender=True) 103 | 104 | 105 | @msg_forward_name.handle() 106 | async def _(bot: Bot, event: Event): 107 | if event.get_user_id() in super_user: 108 | forward_name = re.sub(r"^涩图转发者名字", '', event.get_plaintext()) 109 | Config.set_file_args('FORWARD_NAME', forward_name) 110 | await bot.send(message=f"修改涩图转发者名字为{forward_name}成功", event=event, at_sender=True) 111 | else: 112 | await msg_forward_name.send("只有主人才有权限哦", at_sender=True) 113 | 114 | 115 | @downLoad.handle() 116 | async def _(event: Event): 117 | num = int(re.search(r"\d+", event.get_plaintext()).group()) 118 | if event.get_user_id() in super_user: 119 | try: 120 | r18 = 1 if event.get_plaintext().find('色图') != -1 else 0 121 | await downLoad.send(f"开始下载...") 122 | await get_url(num=num, online_switch=0, r18=r18) 123 | await downLoad.send(f"下载涩图成功,图库中涩图数量{get_file_num('loliconImages')}", at_sender=True) 124 | except httpx.HTTPError as e: 125 | await downLoad.send(f"下载时出现异常{str(type(e))}", at_sender=True) 126 | except Exception as e: 127 | await downLoad.send(f"{e}", at_sender=True) 128 | else: 129 | await downLoad.send('只有主人才有权限哦', at_sender=True) 130 | 131 | 132 | @user_cd.handle() 133 | async def _(event: Event): 134 | msg = event.get_message() 135 | user_id = event.get_user_id() 136 | if user_id in super_user: 137 | user_id = msg[0].get('data')['qq'] 138 | cd = int(event.get_plaintext().replace(' cd', '')) 139 | user = UserDao().get_user_cd(user_id) 140 | if user is None: 141 | UserDao().add_user_cd(user_id, UserDao.datetime_to_seconds(datetime.now()), cd) 142 | else: 143 | UserDao().update_user_cd(user_id, '', cd) 144 | await user_cd.send(f'设置用户{user_id}的cd成功,cd时间为{cd}s', at_sender=True) 145 | else: 146 | await user_cd.send('只有主人才有权限哦', at_sender=True) 147 | 148 | 149 | @group_cd.handle() 150 | async def _(bot: Bot, event: Event): 151 | user_id = event.get_user_id() 152 | if user_id in super_user: 153 | cd = int(event.get_plaintext().replace('群cd', '')) 154 | if not hasattr(event, 'group_id'): 155 | await group_cd.send('请在群里使用', at_sender=True) 156 | group_id = GroupDao().get_group_cd(event.group_id) 157 | if group_id is None: 158 | GroupDao().set_group_cd(event.group_id, cd) 159 | else: 160 | GroupDao().update_group_cd(event.group_id, cd) 161 | 162 | await group_cd.send(f'设置群{event.group_id}的cd成功,cd时间为{cd}s', at_sender=True) 163 | else: 164 | await group_cd.send('只有主人才有权限哦', at_sender=True) 165 | 166 | 167 | @online_switch.handle() 168 | async def _(bot: Bot, event: Event): 169 | msg = event.get_plaintext() 170 | switch = 1 if msg == "开启在线发图" else 0 171 | if event.get_user_id() in super_user: 172 | Config.set_file_args('ONLINE_SWITCH', switch) 173 | await online_switch.send(f'{msg}成功') 174 | else: 175 | await online_switch.send('只有主人才有权限哦', at_sender=True) 176 | 177 | 178 | @proxy_switch.handle() 179 | async def _(bot: Bot, event: Event): 180 | msg = event.get_plaintext() 181 | switch = 1 if msg == "开启魔法" else 0 182 | if event.get_user_id() in super_user: 183 | Config.set_file_args('PROXIES_SWITCH', switch) 184 | await proxy_switch.send(f'{msg}成功') 185 | else: 186 | await proxy_switch.send('只有主人才有权限哦', at_sender=True) 187 | 188 | 189 | @withdraw_interval.handle() 190 | async def _(bot: Bot, event: Event): 191 | msg = event.get_plaintext() 192 | interval = int(msg.replace('撤回间隔', '')) 193 | if event.get_user_id() in super_user: 194 | if interval > 120: 195 | await withdraw_interval.send('间隔不能超过120s', at_sender=True) 196 | else: 197 | if not hasattr(event, 'group_id'): 198 | await withdraw_interval.finish("请在群里使用此功能") 199 | group_id = event.group_id 200 | GroupDao().set_or_update_group_interval(group_id=group_id, interval=interval) 201 | await withdraw_interval.send(f'设置群{group_id}撤回间隔{interval}s成功') 202 | else: 203 | await withdraw_interval.send('只有主人才有权限哦', at_sender=True) 204 | 205 | 206 | @api.handle() 207 | async def _(event: Event): 208 | msg = event.get_plaintext() 209 | if msg == '涩图api': 210 | if ImageDao().get_api() is None: 211 | await api.send(f'请设置api地址(格式:http://服务器公网ip或域名:机器人端口)') 212 | else: 213 | await api.send( 214 | f'涩图api已开启,请访问\n{ImageDao().get_api()}/setu/docs\n{ImageDao().get_api()}/setu/redoc\n查看api文档') 215 | else: 216 | if event.get_user_id() in super_user: 217 | address = re.sub('^设置api地址', '', msg) 218 | ImageDao().set_or_update_api(address) 219 | await api.send(f"设置api地址{address}成功") 220 | else: 221 | await api.send("只有主人才有权限哦", at_sender=True) 222 | 223 | 224 | @r18_switch.handle() 225 | async def _(event: Event): 226 | msg = event.get_plaintext() 227 | if event.get_user_id() in super_user: 228 | if msg == "开启涩涩" or msg == "关闭涩涩": 229 | if not hasattr(event, 'group_id'): 230 | await r18_switch.finish('私聊请使用开启/关闭私聊涩涩') 231 | GroupDao().set_or_update_group_r18(event.group_id, 1 if msg == "开启涩涩" else 0) 232 | await r18_switch.finish(f"群{event.group_id}{msg}成功") 233 | else: 234 | UserDao().set_or_update_r18(1 if msg == "开启私聊涩涩" else 0) 235 | await r18_switch.finish(f"{msg}成功") 236 | else: 237 | await r18_switch.finish('只有主人才有权限哦', at_sender=True) 238 | 239 | @setu_help.handle() 240 | async def _(): 241 | import pkg_resources 242 | try: 243 | _dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot_plugin_setu") 244 | _help = f'涩图插件版本:{_dist.version}\n' \ 245 | '主人专用:\n' \ 246 | '1、下载涩图:下载涩图(非r18)+数量,下载色图(r18)+数量,例如:下载涩图20,下载色图333\n' \ 247 | '2、指定用户cd:@用户cd+时间(秒),例如:@张三cd123\n' + \ 248 | '3、指定群cd:群cd+时间(秒),例如群cd123\n' \ 249 | '4、指定图片是否存储:开启/关闭在线发图\n' \ 250 | '5、指定获取图片是否使用代理:开启/关闭魔法\n' \ 251 | '6、指定撤回间隔:撤回间隔+时间(秒),例如:撤回间隔123,撤回间隔为0时将不进行撤回\n' \ 252 | '7、指定api地址:设置api地址+地址,例如:设置api地址123.456.789.111:8080\n' \ 253 | '8、获取api地址:涩图api\n' \ 254 | '9、开启/关闭涩涩:开启/关闭涩涩,开启/关闭私聊涩涩。用于指定是否开启r18\n' \ 255 | '10、修改涩图转发者名字:涩图转发者名字+你要修改的名字,例如:涩图转发者名字bot\n' \ 256 | '全员可用功能:\n' \ 257 | '1、发送涩图:涩图、setu、无内鬼、色图' \ 258 | '2、指定tag:涩图tagA(和B和C),最多指定三个tag' 259 | await setu_help.send(_help, at_sender=True) 260 | except Exception as e: 261 | logger.error(e) 262 | await setu_help.send(f'出错了,错误信息{e}', at_sender=True) -------------------------------------------------------------------------------- /nonebot_plugin_setu/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayanamiblhx/nonebot_plugin_setu/bd7165bf407f4574e1c172f1a3fa8be501b930f9/nonebot_plugin_setu/dao/__init__.py -------------------------------------------------------------------------------- /nonebot_plugin_setu/dao/group_dao.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class GroupDao: 5 | def __init__(self): 6 | self.db_path = 'data/lolicon.db' 7 | self.conn = sqlite3.connect(self.db_path) 8 | self.cursor = self.conn.cursor() 9 | self.cursor.execute( 10 | 'create table if not exists group_cd(group_id text primary key, cd integer)') 11 | self.cursor.execute( 12 | 'create table if not exists group_interval(group_id text primary key, group_interval integer default 0)') 13 | self.cursor.execute( 14 | 'create table if not exists group_r18(group_id text primary key, r18 integer default 0)') 15 | self.conn.commit() 16 | self.conn.close() 17 | 18 | def get_group_cd(self, group_id): 19 | conn = sqlite3.connect(self.db_path) 20 | cursor = conn.cursor() 21 | cursor.execute( 22 | 'SELECT cd FROM group_cd WHERE group_id = ?', 23 | (group_id,) 24 | ) 25 | result = cursor.fetchone() 26 | conn.close() 27 | return result[0] if result else None 28 | 29 | def set_group_cd(self, group_id, cd): 30 | conn = sqlite3.connect(self.db_path) 31 | cursor = conn.cursor() 32 | cursor.execute( 33 | 'INSERT OR IGNORE INTO group_cd (group_id, cd) VALUES (?, ?)', 34 | (group_id, cd) 35 | ) 36 | conn.commit() 37 | conn.close() 38 | 39 | def update_group_cd(self, group_id, cd): 40 | conn = sqlite3.connect(self.db_path) 41 | cursor = conn.cursor() 42 | cursor.execute( 43 | 'UPDATE group_cd SET cd = ? WHERE group_id = ?', 44 | (cd, group_id) 45 | ) 46 | conn.commit() 47 | conn.close() 48 | 49 | def get_group_interval(self, group_id): 50 | conn = sqlite3.connect(self.db_path) 51 | cursor = conn.cursor() 52 | cursor.execute( 53 | 'SELECT group_interval FROM group_interval WHERE group_id = ?', 54 | (group_id,) 55 | ) 56 | result = cursor.fetchone() 57 | conn.close() 58 | return result[0] if result else None 59 | 60 | def set_or_update_group_interval(self, group_id, interval): 61 | conn = sqlite3.connect(self.db_path) 62 | cursor = conn.cursor() 63 | cursor.execute( 64 | 'INSERT OR IGNORE INTO group_interval (group_id, group_interval) VALUES (?, ?)', 65 | (group_id, interval) 66 | ) 67 | cursor.execute( 68 | 'UPDATE group_interval SET group_interval = ? WHERE group_id = ?', 69 | (interval, group_id) 70 | ) 71 | conn.commit() 72 | conn.close() 73 | 74 | def get_group_r18(self, group_id): 75 | conn = sqlite3.connect(self.db_path) 76 | cursor = conn.cursor() 77 | cursor.execute( 78 | 'SELECT r18 FROM group_r18 WHERE group_id = ?', 79 | (group_id,) 80 | ) 81 | result = cursor.fetchone() 82 | conn.close() 83 | return result[0] if result else 0 84 | 85 | def set_or_update_group_r18(self, group_id, r18: int = 0): 86 | conn = sqlite3.connect(self.db_path) 87 | cursor = conn.cursor() 88 | cursor.execute( 89 | 'INSERT OR IGNORE INTO group_r18 (group_id, r18) VALUES (?, ?)', 90 | (group_id, r18) 91 | ) 92 | cursor.execute( 93 | 'UPDATE group_r18 SET r18 = ? WHERE group_id = ?', 94 | (r18, group_id) 95 | ) 96 | conn.commit() 97 | conn.close() 98 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/dao/image_dao.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sqlite3 3 | from typing import List 4 | 5 | 6 | class ImageDao: 7 | def __init__(self): 8 | self.db_path = 'data/lolicon.db' 9 | self.conn = sqlite3.connect(self.db_path) 10 | self.cursor = self.conn.cursor() 11 | self.cursor.execute( 12 | 'create table if not exists lolicon_images(pid text primary key, uid text, title text,' 13 | ' author text, r18 text ,width integer,height integer,ext text,urls text,upload_date text)') 14 | self.cursor.execute( 15 | 'create table if not exists lolicon_tags(' 16 | 'id integer primary key AUTOINCREMENT, pid text, tags text,unique (pid,tags))') 17 | self.cursor.execute( 18 | 'create table if not exists lolicon_api(id integer primary key AUTOINCREMENT, address text)') 19 | self.conn.commit() 20 | self.conn.close() 21 | 22 | def add_images(self, datas): 23 | conn = sqlite3.connect(self.db_path) 24 | cursor = conn.cursor() 25 | for data in datas: 26 | pid = data['pid'] 27 | uid = data['uid'] 28 | title = data['title'] 29 | author = data['author'] 30 | r18 = data['r18'] 31 | width = data['width'] 32 | height = data['height'] 33 | ext = data['ext'] 34 | urls = json.dumps(data['urls']) 35 | upload_date = data['uploadDate'] 36 | sql = "INSERT OR IGNORE INTO lolicon_images(pid, uid, title, author, r18, width, height, ext, urls, upload_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" 37 | cursor.execute(sql, (pid, uid, title, author, r18, width, height, ext, urls, upload_date)) 38 | tags = data['tags'] 39 | for tag in tags: 40 | sql = "INSERT OR IGNORE INTO lolicon_tags(pid, tags) VALUES (?, ?)" 41 | cursor.execute(sql, (pid, tag)) 42 | conn.commit() 43 | conn.close() 44 | 45 | def get_images_by_pid(self, pid): 46 | conn = sqlite3.connect(self.db_path) 47 | cursor = conn.cursor() 48 | sql = f"select * from lolicon_images where pid='{pid}'" 49 | data = self.processing_data(conn, cursor, sql) 50 | return data 51 | 52 | def get_images_by_tags(self, tags: List[str], r18: bool = False, num: int = 1, **kwargs): 53 | conn = sqlite3.connect(self.db_path) 54 | cursor = conn.cursor() 55 | sql = "select * from lolicon_images where pid in (select pid from lolicon_tags" 56 | sql += f" where tags like '%{tags[0]}%'" 57 | for tag in tags[1:]: 58 | sql += f" or tags like '%{tag}%'" 59 | sql += f") group by pid having r18={r18} order by random() limit {num}" 60 | img_data = self.processing_data(conn, cursor, sql) 61 | return img_data 62 | 63 | @staticmethod 64 | def processing_data(conn, cursor, sql): 65 | cursor.execute(sql) 66 | img_infos = cursor.fetchall() 67 | img_description = cursor.description 68 | img_datas = {"data": []} 69 | for img_info in img_infos: 70 | data = {} 71 | for idx, col in enumerate(img_description): 72 | if col[0] == 'urls': 73 | data[col[0]] = json.loads(img_info[idx]) 74 | elif col[0] == 'r18': 75 | data[col[0]] = img_info[idx] == '1' 76 | else: 77 | data[col[0]] = img_info[idx] 78 | tags = [] 79 | sql = f"select tags from lolicon_tags where pid='{data['pid']}'" 80 | tag_infos = cursor.execute(sql).fetchall() 81 | for tag_info in tag_infos: 82 | tags.append(tag_info[0]) 83 | data['tags'] = tags 84 | img_datas['data'].append(data) 85 | conn.close() 86 | return img_datas 87 | 88 | def get_top_tags(self): 89 | conn = sqlite3.connect(self.db_path) 90 | cursor = conn.cursor() 91 | sql = "select tags,count(*) as count from lolicon_tags group by tags order by count desc limit 10" 92 | cursor.execute(sql) 93 | top_tags = cursor.fetchall() 94 | data = [] 95 | for tag in top_tags: 96 | data.append({'tags': tag[0], 'count': tag[1]}) 97 | conn.close() 98 | return data 99 | 100 | def get_random_images(self, num: int, r18: bool = False): 101 | conn = sqlite3.connect(self.db_path) 102 | cursor = conn.cursor() 103 | sql = f"select * from lolicon_images where r18={r18} order by random() limit {num}" 104 | data = self.processing_data(conn, cursor, sql) 105 | return data 106 | 107 | def get_images_by_uid(self, uid, num: int = 1, r18: bool = False, **kwargs): 108 | conn = sqlite3.connect(self.db_path) 109 | cursor = conn.cursor() 110 | sql = f"select * from lolicon_images where uid='{uid}' and r18={r18} order by random() limit {num}" 111 | data = self.processing_data(conn, cursor, sql) 112 | return data 113 | 114 | def get_api(self): 115 | conn = sqlite3.connect(self.db_path) 116 | cursor = conn.cursor() 117 | sql = "select address from lolicon_api where id=1" 118 | cursor.execute(sql) 119 | data = cursor.fetchone() 120 | conn.close() 121 | return data[0] if data else None 122 | 123 | def set_or_update_api(self, api_address: str): 124 | conn = sqlite3.connect(self.db_path) 125 | cursor = conn.cursor() 126 | sql_insert = "INSERT OR IGNORE INTO lolicon_api VALUES (?, ?)" 127 | cursor.execute(sql_insert, (1, api_address)) 128 | sql_update = "UPDATE lolicon_api SET address=? WHERE id=1" 129 | cursor.execute(sql_update, (api_address,)) 130 | conn.commit() 131 | conn.close() 132 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/dao/user_dao.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import time 3 | from datetime import datetime 4 | from .group_dao import GroupDao 5 | from nonebot.adapters.onebot.v11 import Event 6 | 7 | DEFAULT_CD = 3600 8 | 9 | 10 | class UserDao: 11 | def __init__(self): 12 | self.db_path = 'data/lolicon.db' 13 | self.conn = sqlite3.connect(self.db_path) 14 | self.cursor = self.conn.cursor() 15 | self.cursor.execute( 16 | 'create table if not exists user_cd(user_id text primary key, last_time integer ,cd integer)') 17 | self.cursor.execute( 18 | 'create table if not exists r18_private_chat(id integer primary key AUTOINCREMENT, r18 integer default 0)') 19 | self.conn.commit() 20 | self.conn.close() 21 | pass 22 | 23 | @staticmethod 24 | def datetime_to_seconds(time_obj): 25 | return int(time_obj.timestamp()) 26 | 27 | def get_user_cd(self, user_id): 28 | conn = sqlite3.connect(self.db_path) 29 | cursor = conn.cursor() 30 | cursor.execute(f"SELECT * FROM user_cd WHERE user_id = '{user_id}'") 31 | user = cursor.fetchone() 32 | if user is None: 33 | return None 34 | data = {} 35 | for idx, val in enumerate(cursor.description): 36 | data[val[0]] = user[idx] 37 | conn.close() 38 | return data 39 | pass 40 | 41 | def add_user_cd(self, user_id, last_time, cd): 42 | conn = sqlite3.connect(self.db_path) 43 | cursor = conn.cursor() 44 | cursor.execute("INSERT OR IGNORE INTO user_cd VALUES (?, ?, ?)", (user_id, last_time, cd)) 45 | conn.commit() 46 | conn.close() 47 | pass 48 | 49 | def get_user_remain_time(self, event: Event): 50 | user_id = event.get_user_id() 51 | user_cd = self.get_user_cd(user_id) 52 | this_time = self.datetime_to_seconds(datetime.now()) 53 | if not hasattr(event, 'group_id'): 54 | if user_cd is None: 55 | self.add_user_cd(user_id, this_time, -1) 56 | return 0 57 | else: 58 | cd = DEFAULT_CD if user_cd['cd'] == -1 else user_cd['cd'] 59 | if this_time - user_cd['last_time'] < cd: 60 | return cd - (this_time - user_cd['last_time']) 61 | else: 62 | self.update_user_cd(user_id, this_time, user_cd['cd']) 63 | return 0 64 | else: 65 | group_id = event.group_id 66 | if user_cd is None: 67 | cd = GroupDao().get_group_cd(group_id) 68 | if cd is None: 69 | cd = DEFAULT_CD 70 | GroupDao().set_group_cd(group_id, cd) 71 | self.add_user_cd(user_id, this_time, -1) 72 | return 0 73 | cd = GroupDao().get_group_cd(group_id) if user_cd['cd'] == -1 else user_cd['cd'] 74 | last_time = user_cd['last_time'] 75 | time_diff = this_time - last_time 76 | if time_diff >= cd: 77 | self.update_user_cd(user_id, this_time, '') 78 | return 0 79 | else: 80 | return cd - time_diff 81 | pass 82 | 83 | def update_user_cd(self, user_id, last_time, cd): 84 | conn = sqlite3.connect(self.db_path) 85 | cursor = conn.cursor() 86 | if cd == '': 87 | cursor.execute(f"UPDATE user_cd SET last_time = '{last_time}' WHERE user_id = '{user_id}'") 88 | elif last_time == '': 89 | cursor.execute(f"UPDATE user_cd SET cd = '{cd}' WHERE user_id = '{user_id}'") 90 | else: 91 | cursor.execute(f"UPDATE user_cd SET last_time = '{last_time}', cd = '{cd}' WHERE user_id = '{user_id}'") 92 | conn.commit() 93 | conn.close() 94 | pass 95 | 96 | def delete_user_cd(self, user_id): 97 | conn = sqlite3.connect(self.db_path) 98 | cursor = conn.cursor() 99 | cursor.execute(f"UPDATE user_cd SET last_time = '0' WHERE user_id = '{user_id}'") 100 | conn.commit() 101 | conn.close() 102 | pass 103 | 104 | def get_r18_private_chat(self): 105 | conn = sqlite3.connect(self.db_path) 106 | cursor = conn.cursor() 107 | cursor.execute("SELECT * FROM r18_private_chat WHERE id = 1") 108 | r18 = cursor.fetchone() 109 | if r18 is None: 110 | return 0 111 | conn.close() 112 | return r18[1] 113 | 114 | def set_or_update_r18(self, r18: int = 0): 115 | conn = sqlite3.connect(self.db_path) 116 | cursor = conn.cursor() 117 | cursor.execute("INSERT OR IGNORE INTO r18_private_chat VALUES (?, ?)", (1, r18)) 118 | cursor.execute("UPDATE r18_private_chat SET r18 = '{}' WHERE id = 1".format(r18)) 119 | conn.commit() 120 | conn.close() 121 | pass 122 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/file_tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sqlite3 4 | import pathlib 5 | 6 | setu_config = { 7 | 'SUPERUSERS': [""], 8 | 'PROXIES_HTTP': '', 9 | 'PROXIES_SOCKS': '', 10 | 'PROXIES_SWITCH': 0, 11 | 'ONLINE_SWITCH': 1, 12 | } 13 | 14 | 15 | class Config: 16 | def __init__(self): 17 | self.create_file() 18 | with open('data/setu_config.json', 'r', encoding='utf-8') as file: 19 | self.config = json.load(file) 20 | self.super_users = self.config['SUPERUSERS'] 21 | self.proxies_http = self.config['PROXIES_HTTP'] 22 | self.proxies_socks = self.config['PROXIES_SOCKS'] 23 | self.proxies_switch = self.config['PROXIES_SWITCH'] 24 | self.online_switch = self.config['ONLINE_SWITCH'] 25 | 26 | @staticmethod 27 | def create_file(): 28 | pathlib.Path('data').mkdir(parents=True, exist_ok=True) 29 | pathlib.Path('loliconImages/r18').mkdir(parents=True, exist_ok=True) 30 | if not os.path.exists('data/setu_config.json'): 31 | with open('data/setu_config.json', 'w', encoding='utf-8') as f: 32 | json.dump(setu_config, f, indent=4) 33 | if not os.path.exists('data/lolicon.db'): 34 | conn = sqlite3.connect('data/lolicon.db') 35 | conn.close() 36 | 37 | @staticmethod 38 | def get_file_args(args_name: str): 39 | with open('data/setu_config.json', 'r', encoding='utf-8') as file: 40 | content = json.load(file) 41 | if args_name not in content: 42 | content.update({args_name: ' '}) 43 | with open('data/setu_config.json', 'w', encoding='utf-8') as file_new: 44 | json.dump(content, file_new, indent=4) 45 | return content[args_name] 46 | 47 | @staticmethod 48 | def set_file_args(args_name: str, args_value: str): 49 | with open('data/setu_config.json', 'r', encoding='utf-8') as file: 50 | setu_content = json.load(file) 51 | setu_content.update({args_name: args_value}) 52 | with open('data/setu_config.json', 'w', encoding='utf-8') as file_new: 53 | json.dump(setu_content, file_new, indent=4) 54 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/getPic.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | from io import BytesIO 4 | 5 | from httpx import AsyncClient, TimeoutException, HTTPError 6 | from nonebot.log import logger 7 | from tqdm import tqdm 8 | 9 | from .file_tools import Config 10 | from .dao.image_dao import ImageDao 11 | from .proxies import proxy_http, proxy_socks 12 | 13 | 14 | async def get_url(num: int, online_switch: int, tags: list = "", r18: int = 0): 15 | head = { 16 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 17 | "Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.36", 18 | } 19 | params = { 20 | "r18": r18, 21 | "size": 'regular', 22 | "tag": tags, 23 | } 24 | async with AsyncClient(proxies=None) as client: 25 | times = int(num / 20) + 1 26 | remain = num % 20 27 | datas = [] 28 | i = 0 29 | while i < times: 30 | i += 1 31 | num = 20 if i != times else remain 32 | if num == 0: 33 | break 34 | req_url = f"https://api.lolicon.app/setu/v2?num={num}" 35 | flag = 0 36 | while True: 37 | try: 38 | flag += 1 39 | if flag > 10: 40 | raise Exception(f"获取api内容失败次数过多,请检查网络链接") 41 | res = await client.get(req_url, params=params, headers=head, timeout=10) 42 | if res.status_code == 200: 43 | break 44 | except TimeoutException as e: 45 | logger.error(f"获取loliconAPI内容超时{type(e)}") 46 | except HTTPError as e: 47 | logger.error(f"{type(e)}") 48 | raise e 49 | except Exception as e: 50 | logger.error(f"{e}") 51 | raise e 52 | res = json.loads(res.text) 53 | data = res['data'] 54 | if not data: 55 | return "" 56 | datas.extend(data) 57 | ImageDao().add_images(datas) 58 | img = await down_pic(datas, online_switch, r18) 59 | return img 60 | 61 | 62 | async def down_pic(datas, online_switch: int, r18: int = 0): 63 | head = { 64 | 'referer': 'https://www.pixiv.net/', 65 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.36" 66 | } 67 | http = proxy_http if Config().proxies_switch else None 68 | socks = proxy_socks if Config().proxies_switch else None 69 | async with AsyncClient(proxies=http, transport=socks) as client: 70 | pbar = tqdm(datas, desc='Downloading', colour='green') 71 | tag_img = "" 72 | for data in datas: 73 | proxy_url = data['urls']['regular'].replace('i.pixiv.re', 'i.pximg.net') 74 | url = data['urls']['regular'] 75 | url = proxy_url if Config().proxies_switch else url 76 | pid = data['pid'] 77 | ext = data['ext'] 78 | tag_img = str(pid) + "." + ext 79 | flag = 0 80 | while True: 81 | try: 82 | flag += 1 83 | if flag > 10: 84 | raise Exception(f"获取图片内容失败次数过多,请检查网络链接") 85 | response = await client.get(url=url, headers=head, timeout=10) 86 | if response.status_code == 200: 87 | break 88 | except TimeoutException as e: 89 | logger.error(f"获取图片内容超时: {type(e)}") 90 | except HTTPError as e: 91 | logger.error(f"{type(e)}") 92 | raise e 93 | except Exception as e: 94 | logger.error(f"{e}") 95 | raise e 96 | pbar.update(1) 97 | if online_switch == 1: 98 | img_info = {'pid': pid, 99 | 'base64': f"base64://{base64.b64encode(BytesIO(response.content).getvalue()).decode()}"} 100 | return img_info 101 | img_path = f"loliconImages/{'r18/' if r18 else ''}{pid}.{ext}" 102 | with open(img_path, 'wb') as f: 103 | f.write(response.content) 104 | pbar.close() 105 | return tag_img 106 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/proxies.py: -------------------------------------------------------------------------------- 1 | from httpx_socks import AsyncProxyTransport 2 | from nonebot.log import logger 3 | 4 | from .file_tools import Config 5 | 6 | proxy_config = Config() 7 | 8 | socks = proxy_config.get_file_args('PROXIES_SOCKS') 9 | http = proxy_config.get_file_args('PROXIES_HTTP') 10 | if socks != '': 11 | logger.info('已配置socks代理,http代理将被忽略') 12 | proxy_config.set_file_args('PROXIES_SWITCH', 1) 13 | proxy_socks = AsyncProxyTransport.from_url(socks) 14 | proxy_http = None 15 | elif http != '': 16 | logger.info('已配置http代理,socks代理将被忽略') 17 | proxy_config.set_file_args('PROXIES_SWITCH', 1) 18 | proxy_socks = None 19 | proxy_http = http 20 | else: 21 | logger.info('未配置代理') 22 | proxy_config.set_file_args('PROXIES_SWITCH', 0) 23 | proxy_socks = None 24 | proxy_http = None 25 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/setu_api.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import pkg_resources 4 | from fastapi import FastAPI, Query, Body 5 | from pydantic import BaseModel, Field 6 | 7 | from .dao.image_dao import ImageDao 8 | 9 | setu_api = FastAPI( 10 | title="涩图Api", 11 | version=pkg_resources.get_distribution("nonebot_plugin_setu").version, 12 | contact={ 13 | "name": "nonebot_plugin_setu", 14 | "url": "https://github.com/ayanamiblhx/nonebot_plugin_setu" 15 | } 16 | ) 17 | 18 | 19 | class SetuItem(BaseModel): 20 | pid: str = Field(None, max_length=20, description="pid", example="") 21 | uid: str = Field(None, max_length=20, description="uid", example="") 22 | num: Optional[int] = Field(1, ge=1, le=100, description="图片数量,默认为1,最大为100") 23 | proxy: bool = Field(False, description="是否使用代理,默认不使用") 24 | tags: Optional[List[str]] = Field(None, description="标签,最多3个", max_items=3, example=["碧蓝航线", "碧蓝幻想"]) 25 | r18: bool = False 26 | 27 | 28 | @setu_api.get("/v1", tags=["获取涩图"], description="GET请求") 29 | async def get_img(pid: str = Query(None, max_length=20, description="pid"), 30 | uid: str = Query(None, max_length=20, description="uid"), 31 | num: Optional[int] = Query(1, ge=1, le=100, description="图片数量,默认为1,最大为100"), 32 | tags: Optional[List[str]] = Query(None, max_items=3, description="标签,最多3个"), 33 | proxy: bool = Query(False, description="是否使用代理地址,默认不使用"), 34 | r18: bool = False): 35 | if pid is not None and pid != "": 36 | datas = ImageDao().get_images_by_pid(pid=pid) 37 | elif uid is not None and uid != "": 38 | datas = ImageDao().get_images_by_uid(uid=uid, num=num, r18=r18) 39 | elif tags is not None and tags.__len__() > 0: 40 | datas = ImageDao().get_images_by_tags(tags=tags, r18=r18, num=num) 41 | else: 42 | datas = ImageDao().get_random_images(num=num, r18=r18) 43 | if proxy: 44 | for data in datas['data']: 45 | data['urls']['regular'] = data["urls"]['regular'].replace('i.pixiv.cat', 'i.pixiv.re') 46 | else: 47 | for data in datas['data']: 48 | data["urls"]['regular'] = data["urls"]['regular'].replace('i.pixiv.cat', 'i.pximg.net') 49 | return datas 50 | 51 | 52 | @setu_api.post("/v1", tags=["获取涩图"], description="POST请求") 53 | async def post_img(setu_item: SetuItem = Body(...)): 54 | if setu_item.pid is not None and setu_item.pid != "": 55 | datas = ImageDao().get_images_by_pid(pid=setu_item.pid) 56 | elif setu_item.uid is not None and setu_item.uid != "": 57 | datas = ImageDao().get_images_by_uid(**setu_item.dict()) 58 | elif setu_item.tags is not None and setu_item.tags.__len__() > 0: 59 | datas = ImageDao().get_images_by_tags(**setu_item.dict()) 60 | else: 61 | datas = ImageDao().get_random_images(num=setu_item.num, r18=setu_item.r18) 62 | if setu_item.proxy: 63 | for data in datas['data']: 64 | data['urls']['regular'] = data["urls"]['regular'].replace('i.pixiv.cat', 'i.pixiv.re') 65 | else: 66 | for data in datas['data']: 67 | data["urls"]['regular'] = data["urls"]['regular'].replace('i.pixiv.cat', 'i.pximg.net') 68 | return datas 69 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from nonebot.adapters.onebot.v11 import Bot, Message, Event 5 | 6 | from .file_tools import Config 7 | 8 | 9 | async def send_forward_msg( 10 | bot: Bot, 11 | event: Event, 12 | name: str, 13 | uin: str, 14 | msgs: List, 15 | is_group_chat: bool, 16 | ): 17 | def to_json(msg: Message): 18 | return {"type": "node", "data": {"name": name, "uin": uin, "content": msg}} 19 | messages = [to_json(msg) for msg in msgs] 20 | forward_api = 'send_private_forward_msg' if not is_group_chat else 'send_group_forward_msg' 21 | if is_group_chat: 22 | return await bot.call_api(forward_api, group_id=event.group_id, messages=messages) 23 | else: 24 | return await bot.call_api(forward_api, user_id=event.get_user_id(), messages=messages) 25 | 26 | 27 | def get_file_num(path): 28 | file_num = 0 29 | for root, dirs, files in os.walk(path): 30 | file_num += len(files) 31 | return file_num 32 | 33 | 34 | def img_num_detect(r18: int): 35 | if os.listdir('loliconImages').__len__() == 1 and not Config().online_switch: 36 | return 0 37 | elif os.listdir('loliconImages/r18').__len__() == 0 \ 38 | and r18 == 1 and not Config().online_switch: 39 | return 0 40 | else: 41 | return 1 42 | -------------------------------------------------------------------------------- /nonebot_plugin_setu/withdraw.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from nonebot.adapters.onebot.v11 import Bot 3 | from nonebot.log import logger 4 | 5 | 6 | async def add_withdraw_job(bot: Bot, message_id: int, withdraw_interval: int = 0, **kwargs): 7 | if withdraw_interval: 8 | tasks = [] 9 | logger.info(f'添加撤回定时任务,撤回间隔:{withdraw_interval}秒') 10 | tasks.append(withdraw_msg(bot, message_id)) 11 | await asyncio.sleep(withdraw_interval) 12 | asyncio.gather(*tasks).add_done_callback(lambda x: logger.info(f'撤回任务已完成,撤回消息id:{message_id}')) 13 | 14 | 15 | async def withdraw_msg(bot: Bot, message_id: int): 16 | await bot.delete_msg(message_id=message_id) 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot_plugin_setu" 3 | version = "1.1.11" 4 | description = "基于lolicon api的涩图插件" 5 | authors = ["ayanamiblhx <1196818079@qq.com>"] 6 | license = "Apache License 2.0" 7 | homepage = "https://github.com/ayanamiblhx/nonebot_plugin_setu" 8 | documentation = "https://github.com/ayanamiblhx/nonebot_plugin_setu/blob/main/README.md" 9 | readme="README.md" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.8" 13 | httpx = "^0.23.0" 14 | httpx-socks = "^0.7.3" 15 | nonebot2 = "^2.0.0b2" 16 | nonebot-adapter-onebot = "^2.0.0b1" 17 | tqdm = "^4.61.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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.75.0 2 | httpx==0.21.3 3 | httpx_socks==0.7.3 4 | nonebot==1.8.2 5 | pydantic==1.9.0 6 | tqdm==4.61.0 7 | --------------------------------------------------------------------------------