├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── code ├── Arsenal │ ├── __init__.py │ ├── basic │ │ ├── BNConnect.py │ │ ├── __init__.py │ │ ├── bot_tool.py │ │ ├── command_split.py │ │ ├── datetime_tool.py │ │ ├── db_pool.py │ │ ├── file_handler.py │ │ ├── log_record.py │ │ ├── msg_temp.py │ │ ├── plugin_class.py │ │ ├── plugin_res_directory.py │ │ ├── restart_mybot.py │ │ ├── short_link.py │ │ ├── sys_info.py │ │ ├── task_processor.py │ │ ├── thread_pool.py │ │ └── user_data.py │ ├── bot_blhx_build_pool │ │ ├── __init__.py │ │ ├── blhx_tool.py │ │ ├── bot_blhx_build_pool.py │ │ ├── build_card.py │ │ ├── build_pool.py │ │ └── data_handler.py │ ├── bot_color_img │ │ ├── __init__.py │ │ ├── bot_color_img.py │ │ ├── bot_gaussianblur_img │ │ │ ├── __init__.py │ │ │ └── bot_gaussianblur_img.py │ │ ├── bot_phantom_img │ │ │ ├── __init__.py │ │ │ └── bot_phantom_img.py │ │ └── color_img.py │ ├── bot_img_search │ │ ├── __init__.py │ │ ├── bot_img_search.py │ │ ├── img_search_tool.py │ │ ├── sites │ │ │ ├── __init__.py │ │ │ ├── ascii2d.py │ │ │ ├── doujin.py │ │ │ └── saucenao.py │ │ ├── test │ │ │ ├── test_ascii2d.py │ │ │ └── test_saucenao.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── ascii2d.py │ │ │ └── saucenao.py │ ├── bot_random_text │ │ ├── __init__.py │ │ └── bot_random_text.py │ ├── bot_report_time │ │ ├── __init__.py │ │ └── bot_report_time.py │ ├── bot_suolink │ │ ├── __init__.py │ │ └── bot_suolink.py │ ├── bot_whatanime │ │ ├── __init__.py │ │ └── bot_whatanime.py │ └── coding │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── bot_day_illust │ │ ├── __init__.py │ │ └── bot_day_illust.py │ │ ├── bot_rj_info │ │ ├── __init__.py │ │ └── bot_rj_info.py │ │ ├── error.py │ │ ├── trash │ │ ├── bot_blhx_prediction │ │ │ ├── __init__.py │ │ │ └── bot_blhx_prediction.py │ │ └── bot_pokenman │ │ │ ├── __init__.py │ │ │ └── bot_pokenman.py │ │ ├── yandex_demo.jpg │ │ └── yandex_upload_demo.py ├── __init__.py ├── church.py ├── config.yaml ├── dynamic_import.py ├── executor.py ├── level_manager.py ├── version └── 更新日志.md ├── doc └── sql │ └── mybot.sql └── test ├── Readme.md ├── test_bot_day_illust.py └── test_db_pool.py /.gitignore: -------------------------------------------------------------------------------- 1 | ##### Mybot project .gitignore Start ##### 2 | # log 3 | /code/log/* 4 | 5 | # resource 6 | /code/resource/* 7 | 8 | # workspace 9 | /code/workspace/* 10 | 11 | ##### Mybot project .gitignore End ##### 12 | 13 | 14 | ##### Github Python.gitignore Start ##### 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | cover/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | db.sqlite3 76 | db.sqlite3-journal 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | .pybuilder/ 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | # For a library or package, you might want to ignore these files since the code is 101 | # intended to run in multiple environments; otherwise, check them in: 102 | # .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # poetry 112 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 113 | # This is especially recommended for binary packages to ensure reproducibility, and is more 114 | # commonly ignored for libraries. 115 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 116 | #poetry.lock 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | ##### Github Python.gitignore End ##### -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "./code" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## mybot v1.0.0 2 | 3 | 2021/1/23 16点53分 初版 4 | 5 | ``` 6 | . 7 | |-- Readme.md 8 | |-- code // mybot代码 9 | |-- doc // 相关文档 10 | |-- others // 其他文件 11 | |-- test // 测试代码 12 | ``` 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /code/Arsenal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/code/Arsenal/__init__.py -------------------------------------------------------------------------------- /code/Arsenal/basic/BNConnect.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : BNConnect.py 4 | @Time : 2020/07/15 11:22 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import time 12 | import requests 13 | import cloudscraper 14 | # 强制取消警告 15 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 16 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 17 | 18 | from Arsenal.basic.log_record import logger 19 | 20 | # pixiv 21 | pixiv_headers = { 22 | "Host": "www.pixiv.net", 23 | "referer": "https://www.pixiv.net/", 24 | "origin": "https://accounts.pixiv.net", 25 | "accept-language": "zh-CN,zh;q=0.9", # 返回translation,中文翻译 26 | "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 27 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', 28 | } 29 | 30 | 31 | general_headers = { 32 | "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 33 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', 34 | } 35 | 36 | 37 | def baseRequest(options, 38 | method = "GET", 39 | data = None, 40 | params = None, 41 | retry_num = 5 42 | ): 43 | ''' 44 | :params options: 请求参数 45 | {"headers":"your headers","url":"example.com"} 46 | :params method: 47 | "GET"/"POST" 48 | :params data: POST data 49 | :params params: GET params 50 | :params retry_num 重试次数 51 | :return response or False 52 | 53 | options支持自定义headers,否则使用默认的headers 54 | 55 | 添加referer时,referer需要是上一个页面的url,比如:画师/作品页面的url时,则可以自定义请求头 56 | demo如下: 57 | demo_headers = headers.copy() 58 | demo_headers['referer'] = 'www.example.com' 59 | options ={ 60 | "url": origin_url, 61 | "headers": demo_headers 62 | } 63 | baseRequest(options = options) 64 | 这样baseRequest中使用的headers则是定制化的headers,而非默认headers 65 | ''' 66 | if "headers" in options.keys(): 67 | base_headers = options["headers"] 68 | elif "pixiv" in options["url"] and "headers" not in options.keys(): 69 | base_headers = pixiv_headers 70 | elif "headers" not in options.keys(): 71 | base_headers = general_headers 72 | 73 | logger.debug(f" - {options}") 74 | logger.debug(f" - {base_headers}") 75 | try: 76 | response = requests.request( 77 | method, 78 | options["url"], 79 | data = data, 80 | params = params, 81 | cookies = options.get("cookies",""), 82 | headers = base_headers, 83 | verify = False, 84 | timeout = options.get("timeout",5), 85 | ) 86 | response.encoding = "utf8" 87 | return response 88 | except Exception as e: 89 | logger.warning(f" - network requests err | - {e}") 90 | if retry_num > 0: 91 | time.sleep(0.2) 92 | return baseRequest(options, method, data, params, retry_num=retry_num-1) 93 | else: 94 | logger.info(f" - {options}") 95 | logger.warning(f" - network requests err | no retry times") 96 | return 97 | 98 | def scraperRequest(options, 99 | method = "GET", 100 | scraper = None, 101 | data = None, 102 | params = None, 103 | retry_num = 5 104 | ): 105 | ''' 106 | :params options: 请求参数 107 | {"headers":"your headers","url":"example.com"} 108 | :params method: 109 | "GET"/"POST" 110 | :params scraper: cloudscraper.CloudScraper 111 | :params data: POST data 112 | :params params: GET params 113 | :params retry_num 重试次数 114 | :return response or False 115 | 116 | options支持自定义headers,否则使用默认的headers 117 | ''' 118 | if "headers" in options.keys(): 119 | base_headers = options["headers"] 120 | elif "pixiv" in options["url"] and "headers" not in options.keys(): 121 | base_headers = pixiv_headers 122 | elif "headers" not in options.keys(): 123 | base_headers = general_headers 124 | 125 | logger.debug(f" - {options}") 126 | logger.debug(f" - {base_headers}") 127 | 128 | if not scraper: 129 | scraper = cloudscraper.create_scraper() 130 | 131 | try: 132 | response = scraper.request( 133 | method, 134 | options["url"], 135 | data = data, 136 | params = params, 137 | cookies = options.get("cookies",""), 138 | headers = base_headers, 139 | # verify = False, 140 | timeout = options.get("timeout",10), 141 | ) 142 | response.encoding = "utf8" 143 | return response 144 | except Exception as e: 145 | logger.warning(f" - network requests err | - {e}") 146 | if retry_num > 0: 147 | time.sleep(0.2) 148 | return scraperRequest(options, method, scraper, data, params, retry_num=retry_num-1) 149 | else: 150 | logger.info(f" - {options}") 151 | logger.warning(f" - network requests err | no retry times") 152 | return -------------------------------------------------------------------------------- /code/Arsenal/basic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/code/Arsenal/basic/__init__.py -------------------------------------------------------------------------------- /code/Arsenal/basic/bot_tool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : bot_tool.py 4 | @Time : 2021/05/20 11:13:31 5 | @Author : Coder-Sakura 6 | @Version : 1.2 7 | @Contact : 1508015265@qq.com 8 | @Desc : bot配置 CQ码转换等可复用方法 9 | ''' 10 | # here put the import lib· 11 | import os 12 | 13 | from Arsenal.basic.db_pool import DBClient 14 | from Arsenal.basic.log_record import logger 15 | from Arsenal.basic.file_handler import loadFile 16 | from Arsenal.basic.BNConnect import baseRequest 17 | from Arsenal.basic.msg_temp import CONFIG_CQ_CODE,TOOL_TEMP 18 | 19 | config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 20 | "..","..","config.yaml") 21 | default_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 22 | "..","..","temp","resource","default.yaml") 23 | 24 | 25 | def init_config(): 26 | """初始化mybot配置文件""" 27 | if not os.path.exists(config_path): 28 | with open(config_path,"w") as f1: 29 | with open(default_path) as f2: 30 | f1.write(f2.read()) 31 | config_yaml = loadFile.by_yaml(config_path) 32 | return config_yaml 33 | 34 | 35 | class Config: 36 | """ 37 | 全局变量配置 38 | """ 39 | def __init__(self): 40 | # 初始化 41 | self.config = init_config() 42 | self.user_limit_flag = self.config["Level"]["user_limit"]["enable"] 43 | self.level = self.read_level_info() 44 | # 统一使用bot_tool的db 45 | self.db = DBClient 46 | 47 | # ===== 初始化log ===== 48 | logger.info(TOOL_TEMP["load_config_success"]) 49 | logger.warning(TOOL_TEMP["debug_status"].format(self.config['Debug'])) 50 | logger.debug(TOOL_TEMP["config_info"].format(self.config)) 51 | logger.debug(TOOL_TEMP["config_path_info"].format(config_path)) 52 | 53 | # 通信 54 | # HTTP or WebSocket 55 | self.protocol = "http://" # or ws:// 56 | self.coolq_http_api_ip = self.get_items('["Bot"]["http"]["coolq_http_api_ip"]') 57 | self.coolq_http_api_port = self.get_items('["Bot"]["http"]["coolq_http_api_port"]') 58 | self.comm_address = f"{self.protocol}{self.coolq_http_api_ip}:{self.coolq_http_api_port}" 59 | 60 | self.send_group_url = TOOL_TEMP["cq_http_send_group_url"].format(self.comm_address) 61 | self.send_private_url = TOOL_TEMP["cq_http_send_private_url"].format(self.comm_address) 62 | 63 | # admin uid 64 | self.admin = int(self.get_items('["Bot"]["admin"]["uid"]')) 65 | 66 | # ===== PLUGIN ===== 67 | # saucenao 68 | # self.saucenao_api_key = self.get_items('["Plugin"]["saucenao"]["api_key"]') 69 | 70 | # 标志位 - 实验性 71 | # PLUGIN_BLOCK 72 | # 主动式插件消息命中解析规则返回 73 | # self.PLUGIN_BLOCK = 0 74 | # PLUGIN_IGNORE 75 | # 主动式插件消息未命中解析规则 76 | # 被动式插件跳过自身 77 | # self.PLUGIN_IGNORE = 1 78 | # ===== PLUGIN ===== 79 | 80 | def get_items(self,value_path="",obj="self.config"): 81 | """ 82 | 获取配置文件中value 83 | :params obj: 指定获取数据的对象 84 | :params value_path: 字段路径 85 | :return: value/None 86 | 87 | exp: 88 | get_items(value_path='["Bot"]["mysql"]["db_user"]') - 'pixiv' 89 | get_items(value_path='["Other"]["mysql"]["db_user"]') - None 90 | """ 91 | try: 92 | return eval("{}{}".format(obj,value_path)) 93 | except: 94 | return None 95 | 96 | def reload_bot_config(self): 97 | """重新加载配置文件""" 98 | self.config = init_config() 99 | return self.config 100 | 101 | def read_level_info(self): 102 | """加载配置文件中的权限设置""" 103 | return { 104 | "general_user_level": int(self.config["Level"]["user"]["general"]), 105 | "vip_user_level": int(self.config["Level"]["user"]["vip"]), 106 | "admin_user_level": int(self.config["Level"]["user"]["admin"]), 107 | "general_group_level": int(self.config["Level"]["group"]["general"]), 108 | "vip_group_level": int(self.config["Level"]["group"]["vip"]) 109 | } 110 | 111 | # === API start=== 112 | # TODO 测试后删除 2022/1/17 113 | def send_group_msg(self,params,url=None): 114 | """ 115 | 私聊先留空 116 | 发送群聊信息到go-cqhttp接口 117 | """ 118 | if not url: 119 | url = self.send_group_url 120 | return baseRequest({"url":url},params=params) 121 | 122 | # TODO 测试后删除 2022/1/17 123 | def send_private_msg(self,params,url=None): 124 | """ 125 | 发送私聊信息到go-cqhttp接口 126 | """ 127 | if not url: 128 | url = self.send_private_url 129 | return baseRequest({"url":url},params=params) 130 | 131 | def auto_report_err(self,err_msg,channel="cq"): 132 | """ 133 | 向管理员实时汇报err信息 134 | """ 135 | status = {} 136 | # 选择qq通知需在config.yaml中填写 137 | # 路径为 ["Bot"]["admin"]["uid"] 138 | if channel == "cq": 139 | params = self.private_msg_temp( 140 | self.admin, 141 | err_msg 142 | ) 143 | status = self.send_private_msg(params=params) 144 | logger.info(f" - {status}") 145 | return status 146 | 147 | def auto_send_msg(self, mybot_data)->bool: 148 | """ 149 | 根据mybot_data进行群组/私聊信息发送 150 | """ 151 | # 群聊信息 152 | if mybot_data["sender"]["type"] == "group": 153 | params = self.group_msg_temp(mybot_data) 154 | self.send_cq_client(params, api="cq_http_send_group_url") 155 | # 私聊信息 156 | elif mybot_data["sender"]["type"] == "private": 157 | params = self.private_msg_temp(mybot_data) 158 | self.send_cq_client(params, api="cq_http_send_private_url") 159 | else: 160 | logger.debug(f" - {mybot_data}") 161 | return False 162 | 163 | logger.debug(f" - {mybot_data}") 164 | return True 165 | 166 | def send_cq_client(self,params,api=None,url=None): 167 | """ 168 | 获取go-cq接口数据 169 | """ 170 | # 直接指定url 171 | if url: 172 | api_url = url 173 | 174 | # 通过接口名 175 | if api: 176 | api_url = TOOL_TEMP[api].format(self.comm_address) 177 | 178 | return baseRequest({"url": api_url}, params=params) 179 | 180 | def group_msg_temp(self, mybot_data)->dict: 181 | """ 182 | 群聊消息 183 | """ 184 | if mybot_data["at"]: 185 | mybot_data["message"] = self.CQ_AT(int(mybot_data["sender"]["user_id"])) + \ 186 | "\n" + mybot_data["message"] 187 | group_msg = { 188 | "group_id": int(mybot_data["sender"]["group_id"]), 189 | "message": mybot_data["message"] 190 | } 191 | logger.debug(f" - {group_msg}") 192 | return group_msg 193 | 194 | def private_msg_temp(self, mybot_data)->dict: 195 | """ 196 | 私聊消息 197 | """ 198 | if mybot_data["at"]: 199 | mybot_data["message"] = self.CQ_AT(int(mybot_data["sender"]["user_id"])) + \ 200 | "\n" + mybot_data["message"] 201 | user_msg = { 202 | "user_id": int(mybot_data["sender"]["user_id"]), 203 | "message": mybot_data["message"] 204 | } 205 | logger.debug(f" - {user_msg}") 206 | return user_msg 207 | 208 | # # === API end=== 209 | # === CQ code start === 210 | 211 | def CQ_IMG_URL(self,img_url): 212 | """ 213 | CQ码: 网络图片链接 214 | :parmas img_url: 图片链接 215 | """ 216 | return CONFIG_CQ_CODE["reply_img"].format(img_url) 217 | 218 | def CQ_IMG_LOCAL(self,img_path): 219 | """ 220 | CQ码: 本地图片 221 | :parmas img_path: 本地图片路径 222 | """ 223 | return CONFIG_CQ_CODE["reply_local_img"].format(img_path) 224 | 225 | def CQ_AT(self,user_id): 226 | """ 227 | CQ码: @ 228 | :parmas user_id: 用户id 229 | """ 230 | return CONFIG_CQ_CODE["reply_at"].format(user_id) 231 | 232 | def CQ_AUDIO_URL(self,audio_url): 233 | """ 234 | CQ码: 网络语音 235 | :parmas audio_url: 网络语音url 236 | """ 237 | return CONFIG_CQ_CODE["reply_audio"].format(audio_url) 238 | 239 | def CQ_AUDIO_LOAL(self,audio_path): 240 | """ 241 | CQ码: 本地语音 242 | :parmas audio_path: 本地语音路径 243 | """ 244 | return CONFIG_CQ_CODE["reply_local_audio"].format(audio_path) 245 | 246 | # === CQ code end === 247 | 248 | 249 | tool = Config() -------------------------------------------------------------------------------- /code/Arsenal/basic/command_split.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : command_split.py 4 | @Time : 2022/05/06 15:19:01 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 将群组消息拆解为kw关键词/mode模式/params参数 8 | ''' 9 | 10 | # here put the import lib 11 | 12 | 13 | def translate(command:str)->dict: 14 | """ 15 | :params command: 输入需要解析的命令 16 | :return: {exp} or {} 17 | 18 | :exp: 19 | { 20 | "kw": keyword, 21 | "mode": [mode1, mode2...], 22 | "params": { 23 | key1: value1, 24 | key2: value2, 25 | key3: value3 26 | } 27 | } 28 | """ 29 | result = {} 30 | result["kw"] = command.split(" ", 1)[0] 31 | result["mode_list"] = [] 32 | result["params"] = {} 33 | 34 | command_list = command.split(" ")[1:] 35 | for _ in command_list: 36 | if "=" not in _ and "-" in _: 37 | result["mode_list"].append(_.replace("-", "")) 38 | 39 | if "=" in _ and "-" in _: 40 | k = _.split("-")[-1].split("=")[0] 41 | v = _.split("=")[-1] 42 | result["params"][k] = v 43 | 44 | return result 45 | -------------------------------------------------------------------------------- /code/Arsenal/basic/datetime_tool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : datetime_tool.py 4 | @Time : 2022/01/19 16:28:58 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import datetime 12 | 13 | def datetime_now()->datetime.datetime: 14 | return datetime.datetime.now() 15 | 16 | def datetime_offset(now_time:datetime.datetime, offset:int)->datetime.datetime: 17 | """ 18 | return datetime by now_time + offset 19 | :params offset: 秒数 20 | """ 21 | offset_seconds = datetime.timedelta(seconds=int(offset)) 22 | return now_time + offset_seconds 23 | 24 | def seconds2time(seconds:int)->list: 25 | """ 26 | input: 86400 27 | output: [day, hour, minute, seconds] 28 | :params seconds: 秒 29 | """ 30 | m,s = divmod(int(seconds), 60) 31 | h,m = divmod(m,60) 32 | d,h = divmod(h,24) 33 | return [d, h, m, s] 34 | 35 | def str2datetime(now_time:str)->datetime.datetime: 36 | return datetime.datetime.strptime(now_time, "%Y-%m-%d %H:%M:%S") -------------------------------------------------------------------------------- /code/Arsenal/basic/db_pool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : db_pool.py 4 | @Time : 2021/06/23 16:44:57 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import pymysql 12 | import datetime 13 | from DBUtils.PooledDB import PooledDB 14 | # DictCursor,返回结果由tuple转为dict 15 | from pymysql.cursors import DictCursor 16 | 17 | 18 | from Arsenal.basic.log_record import logger, init_config 19 | from Arsenal.basic.msg_temp import DB_TEMP,DB_SQL_TEMP,DB_INSERT_DEFAULT_TEMP 20 | 21 | 22 | class db_client: 23 | def __init__(self): 24 | self.config = init_config() 25 | self.db_type = "mysql" 26 | self.db_config = self.config["Bot"].get(self.db_type,"") 27 | if not self.db_config: 28 | logger.error(DB_TEMP["db_config_error"].format(self.db_type)) 29 | exit() 30 | 31 | self.pool = self.create_tool() 32 | 33 | def create_tool(self)->PooledDB: 34 | """ 35 | 创建数据库连接池 36 | :return: PooledDB 37 | """ 38 | try: 39 | pool = PooledDB( 40 | creator=pymysql, 41 | maxconnections=16, 42 | mincached=1, 43 | maxcached=1, 44 | blocking=True, 45 | host=self.db_config["db_host"], 46 | port=self.db_config["db_port"], 47 | user=self.db_config["db_user"], 48 | passwd=self.db_config["db_passwd"], 49 | db=self.db_config["db_database"], 50 | charset=self.db_config["db_charset"] 51 | ) 52 | except pymysql.err.OperationalError as e: 53 | logger.error(DB_TEMP["db_disable"]) 54 | logger.error(e) 55 | exit() 56 | return pool 57 | 58 | def get_conn(self): 59 | conn = self.pool.connection() 60 | cur = conn.cursor(DictCursor) 61 | return conn,cur 62 | 63 | def select_records(self, 64 | mybot_data=None, 65 | table="users", 66 | limit=10, 67 | **kwargs 68 | )->list: 69 | """ 70 | 精确查询,查询符合kwargs字典组条件的记录并返回 71 | 72 | :params mybot_data: mybot_data内部消息体 73 | :params table: 指定数据表 74 | :params limit: 自定义返回记录数量, 0 -> ALL return 75 | :params kwargs: 指定查询条件 76 | 查询条件有字符串需要用引号包括 77 | {"user_id": 123, "group_id": 456} 78 | :return: {} or {result} 79 | 80 | DBClient.select_records(table="messages",limit=1,**{"group_name":"测试群组"}) 81 | """ 82 | # 拼接额外参数 83 | cond = " AND ".join([f"{k} = {repr(v)}" if isinstance(v,str) else f"{k} = {v}" for k,v in kwargs.items()]) 84 | if not cond: 85 | cond = "1 = 1" 86 | 87 | func_sql = DB_SQL_TEMP["select_sql"] 88 | # 0 -> ALL 89 | if isinstance(limit,int) and int(limit) > 0: 90 | func_sql += f"LIMIT {limit}" 91 | elif isinstance(limit,int) and int(limit) < 0: 92 | limit = 10 93 | func_sql += f"LIMIT 10" 94 | 95 | func_sql = func_sql.format(table,cond) 96 | logger.debug(func_sql) 97 | 98 | conn,cur = self.get_conn() 99 | try: 100 | cur.execute(func_sql) 101 | # 执行sql出错 102 | except Exception as e: 103 | logger.warning(f"Exception - {e}") 104 | logger.info(f"func_sql - {func_sql}") 105 | logger.info(f"kwargs - {kwargs}") 106 | return [] 107 | else: 108 | res = cur.fetchall() 109 | logger.debug(f"{len(res)} recods - {res}") 110 | finally: 111 | cur.close() 112 | conn.close() 113 | 114 | # 返回所有查询结果 115 | if len(res) != 0: 116 | if limit == 0: 117 | return res 118 | else: 119 | return res[:limit] 120 | else: 121 | return [] 122 | 123 | def insert_records(self, 124 | mybot_data=None, 125 | table="users", 126 | **kwargs 127 | )->dict: 128 | """ 129 | 插入数据 130 | :params mybot_data: mybot_data内部消息体 131 | :params table: 指定数据表 132 | :params kwargs: 133 | insert_data -> 指定插入数据,需要按照数据表顺序; 134 | level -> 指定用户level; 135 | :return: True Or False 136 | DBClient.insert_records(mybot_data={"user_id": 123,"group_id": 456}) 137 | DBClient.insert_records(mybot_data={"user_id": 123,"group_id": 456}, **{"user_level":50}) 138 | """ 139 | # 无指定的插入数据,使用默认模板插入 140 | if not kwargs.get("insert_data",""): 141 | _data = DB_INSERT_DEFAULT_TEMP["New_User"] 142 | _data["user_id"] = int(mybot_data["sender"]["user_id"]) 143 | _data["group_id"] = int(mybot_data["sender"]["group_id"]) 144 | 145 | now_time = datetime.datetime.now() 146 | # user_limit_cycle 147 | user_limit_cycle = int(self.config["Level"]["user_limit"]["seconds"]) 148 | offset = datetime.timedelta(seconds=user_limit_cycle) 149 | create_date = now_time.strftime('%Y-%m-%d %H:%M:%S') 150 | last_call_date = create_date 151 | cycle_expiration_time = (now_time + offset).strftime('%Y-%m-%d %H:%M:%S') 152 | 153 | _data["user_limit_cycle"] = user_limit_cycle 154 | _data["create_date"] = create_date 155 | _data["last_call_date"] = last_call_date 156 | _data["cycle_expiration_time"] = cycle_expiration_time 157 | 158 | if kwargs.get("user_level",""): 159 | _data["user_level"] = kwargs["user_level"] 160 | else: 161 | _data = kwargs["insert_data"] 162 | 163 | if isinstance(_data, dict): 164 | _data_tuple = tuple(_data.values()) 165 | else: 166 | logger.warning(f"<_data> unlawful. -{_data}") 167 | return {} 168 | 169 | 170 | if not mybot_data and kwargs.get("insert_data",""): 171 | sql_keys_str = ",".join(kwargs["insert_data"].keys()) 172 | else: 173 | sql_keys_str = DB_SQL_TEMP["insert_sql_keys_str"] 174 | 175 | # perch = ",".join(["%s" for i in range(len(DB_SQL_TEMP["insert_sql_keys_str"].split(",")))]) 176 | perch = ",".join(["%s" for i in range(len(sql_keys_str.split(",")))]) 177 | insert_sql = DB_SQL_TEMP["insert_sql"].format(table,sql_keys_str,perch) 178 | 179 | logger.debug(f"insert_sql - {insert_sql}") 180 | logger.debug(f"<_data> - {_data}") 181 | conn,cur = self.get_conn() 182 | try: 183 | cur.execute(insert_sql,_data_tuple) 184 | conn.commit() 185 | except Exception as e: 186 | conn.rollback() 187 | logger.warning(f"records insert fail.") 188 | logger.debug(f"Exception - {e}") 189 | logger.debug(f"rollback success.") 190 | return {} 191 | else: 192 | logger.debug(f"records insert success.") 193 | return _data 194 | finally: 195 | cur.close() 196 | conn.close() 197 | 198 | def update_records(self, 199 | mybot_data=None, 200 | table="users", 201 | **kwargs 202 | )->bool: 203 | """ 204 | 更新数据 205 | :params mybot_data: mybot_data内部消息体 206 | :params table: 指定数据表 207 | :params kwargs: 额外参数 208 | update_data 更新后的数据 209 | judge_data 指定判断条件 210 | **{"update_data":{...}, "judge_data":{...}} 211 | :return: True Or False 212 | 213 | DBClient.update_records(**{"update_data":{"user_level":20}, "judge_data":{"uid":1508015265, "gid":835006}}) 214 | """ 215 | # 检测update_data是否存在 216 | if not kwargs.get("update_data"): 217 | logger.warning(f"Null Value - {kwargs}") 218 | return False 219 | 220 | # 检测judge_data是否存在 221 | if not kwargs.get("judge_data"): 222 | logger.warning(f"Null Value - {kwargs}") 223 | return False 224 | 225 | # 拼接更新数据及条件数据 226 | update_cond = ",".join([f"{k}=%s" for k,v in kwargs.get("update_data",{}).items()]) 227 | judge_cond = " AND ".join([f"{k}=%s" for k,v in kwargs.get("judge_data",{}).items()]) 228 | update_sql = DB_SQL_TEMP["update_sql"] 229 | update_sql = update_sql.format(table,update_cond,judge_cond) 230 | 231 | if isinstance(kwargs.get("update_data"),dict) and isinstance(kwargs.get("judge_data"),dict): 232 | update_data = tuple(kwargs.get("update_data").values()) 233 | judge_data = tuple(kwargs.get("judge_data").values()) 234 | update_data = update_data + judge_data 235 | else: 236 | logger.warning(f""" or unlawful.""") 237 | logger.warning(f""" - {kwargs.get("update_data")}""") 238 | logger.warning(f""" - {kwargs.get("judge_data")}""") 239 | return False 240 | 241 | logger.debug(f" - {update_sql}") 242 | logger.debug(f" - {update_data}") 243 | conn,cur = self.get_conn() 244 | try: 245 | cur.execute(update_sql,update_data) 246 | conn.commit() 247 | except Exception as e: 248 | conn.rollback() 249 | logger.warning(f"records update fail.") 250 | logger.debug(f"Exception - {e}") 251 | logger.debug(f"rollback success.") 252 | return False 253 | else: 254 | logger.debug(f"records update success.") 255 | return True 256 | finally: 257 | cur.close() 258 | conn.close() 259 | 260 | def delete_records(self, 261 | mybot_data=None, 262 | table="users", 263 | **kwargs 264 | )->bool: 265 | """ 266 | 删除数据 267 | :params mybot_data: mybot_data内部消息体 268 | :params table: 指定数据表 269 | :params kwargs: 额外参数 270 | judge_data 判断条件,只提供= 271 | :return: True Or False 272 | 273 | DBClient.delete_records(**{"judge_data":{"create_date": "2021-11-09 16:37:25"}}) 274 | """ 275 | # 检测judge_data是否存在 276 | if not kwargs.get("judge_data"): 277 | logger.warning(f"Null Value ") 278 | return False 279 | 280 | # 拼接条件 281 | judge_cond = " AND ".join([f"{k}=%s" for k,v in kwargs.get("judge_data",{}).items()]) 282 | delete_sql = DB_SQL_TEMP["delete_sql"] 283 | delete_sql = delete_sql.format(table,judge_cond) 284 | 285 | if isinstance(kwargs.get("judge_data"),dict): 286 | judge_data = tuple(kwargs.get("judge_data").values()) 287 | else: 288 | logger.warning(f""" unlawful.""") 289 | logger.warning(f""" - {kwargs.get("judge_data")}""") 290 | return False 291 | 292 | logger.debug(f" - {delete_sql}") 293 | logger.debug(f" - {judge_data}") 294 | conn,cur = self.get_conn() 295 | try: 296 | cur.execute(delete_sql,judge_data) 297 | conn.commit() 298 | except Exception as e: 299 | conn.rollback() 300 | logger.warning(f"records delete fail.") 301 | logger.debug(f"Exception - {e}") 302 | logger.debug(f"rollback success.") 303 | return False 304 | else: 305 | logger.debug(f"records delete success.") 306 | return True 307 | finally: 308 | cur.close() 309 | conn.close() 310 | 311 | DBClient = db_client() -------------------------------------------------------------------------------- /code/Arsenal/basic/file_handler.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : fileio.py 4 | @Time : 2021/06/24 15:05:59 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import json 12 | import yaml 13 | # from yaml import cyaml 14 | from yaml.loader import Loader, SafeLoader 15 | 16 | 17 | from Arsenal.basic.log_record import logger 18 | 19 | 20 | def download_file(resp = None, 21 | filepath = None, 22 | mode = "ab"): 23 | """ 24 | 文件写入 25 | """ 26 | if not resp or not filepath: 27 | logger.warning("download_file: PARAMS ERROR!!") 28 | return 29 | 30 | try: 31 | with open(filepath, mode) as f: 32 | f.write(resp.content) 33 | except Exception as e: 34 | logger.warning(f"Exception - {e}") 35 | return 36 | return filepath 37 | 38 | 39 | class LoadFile: 40 | # read yaml file 41 | @logger.catch 42 | def by_yaml(self, yaml_path, encoding='utf8', loader=SafeLoader): 43 | try: 44 | config_yaml = yaml.load(open(yaml_path, encoding=encoding), Loader=loader) 45 | except yaml.parser.ParserError as err: 46 | logger.warning(f"[yaml.load Error]: {err}") 47 | logger.warning(f"[yaml.path : {yaml_path}") 48 | return {} 49 | else: 50 | return config_yaml 51 | 52 | # read json file 53 | @logger.catch 54 | def by_json(self, json_path, encoding='utf8'): 55 | try: 56 | with open(json_path, encoding=encoding) as f: 57 | json_data = json.load(f) 58 | except json.decoder.JSONDecodeError as err: 59 | logger.warning(f"[json.load Error]: {err}") 60 | logger.warning(f"[json.path : {json_path}") 61 | return {} 62 | else: 63 | return json_data 64 | 65 | class DumpFile: 66 | # dump yaml file 67 | @logger.catch 68 | def by_yaml(self, data, yaml_path, mode="w", encoding='utf8'): 69 | try: 70 | with open(yaml_path, mode=mode, encoding=encoding) as f: 71 | yaml.dump(data, f) 72 | except yaml.parser.ParserError as err: 73 | logger.warning(f"[yaml.dump Error]: {err}") 74 | logger.warning(f"[yaml.path : {yaml_path}") 75 | logger.warning(f"[yaml.data : {data}") 76 | return False 77 | else: 78 | return True 79 | 80 | # dump json file 81 | @logger.catch 82 | def by_json(self, data, json_path, mode="w", encoding='utf8'): 83 | try: 84 | with open(json_path, mode=mode, encoding=encoding) as f: 85 | json.dump(data, f, ensure_ascii=False) 86 | except json.decoder.JSONDecodeError as err: 87 | logger.warning(f"[json.dump Error]: {err}") 88 | logger.warning(f"[json.path : {json_path}") 89 | logger.warning(f"[json.data : {data}") 90 | return False 91 | else: 92 | return True 93 | 94 | loadFile = LoadFile() 95 | dumpFile = DumpFile() -------------------------------------------------------------------------------- /code/Arsenal/basic/log_record.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : log_record.py 4 | @Time : 2021/05/08 14:50:15 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Contact : 1508015265@qq.com 8 | @Desc : None 9 | ''' 10 | 11 | # here put the import lib 12 | import os 13 | import sys 14 | import yaml 15 | from loguru import logger 16 | from yaml.loader import SafeLoader 17 | 18 | 19 | from Arsenal.basic.msg_temp import TOOL_TEMP 20 | 21 | 22 | def init_config(): 23 | config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 24 | "..","..","config.yaml") 25 | default_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 26 | "..","..","resource","temp","default.yaml") 27 | if not os.path.exists(config_path): 28 | with open(config_path,"w") as f1: 29 | with open(default_path) as f2: 30 | f1.write(f2.read()) 31 | 32 | config_yaml = yaml.load(open(config_path, encoding="utf8"), Loader=SafeLoader) 33 | return config_yaml 34 | 35 | 36 | 37 | config_yaml = init_config() 38 | if config_yaml["Debug"]: 39 | level = "DEBUG" 40 | else: 41 | level = "INFO" 42 | 43 | log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "log") 44 | # remove default handler 45 | logger.remove() 46 | # 控制台输出 47 | logger.add( 48 | sys.stderr, 49 | level=level 50 | ) 51 | # 日志写入 52 | logger.add( 53 | os.path.join(log_path, "{time}.log"), 54 | encoding="utf-8", 55 | rotation="00:00", 56 | enqueue=True, 57 | level=level 58 | ) 59 | 60 | """ 61 | DEBUG 10 logger.debug() 62 | INFO 20 logger.info() 63 | SUCCESS 25 logger.success() 64 | WARNING 30 logger.warning() 65 | ERROR 40 logger.error() 66 | CRITICAL 50 logger.critical() 67 | """ 68 | 69 | 70 | """ 71 | 使用@logger.catch可以直接进行 Traceback 的记录 72 | @logger.catch 73 | def my_function(x, y, z): 74 | return 1 / (x + y + z) 75 | 76 | my_function(0, 0, 0) 77 | """ -------------------------------------------------------------------------------- /code/Arsenal/basic/msg_temp.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : msg_temp.py 4 | @Time : 2020/09/19 19:54:04 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 模板消息 8 | ''' 9 | 10 | 11 | # ===== basic start ===== 12 | # 消息收发 13 | USER_MSG_TEMP = { 14 | "qqBlocker_user_msg": "[私]收到来自屏蔽用户{}的信息: {}. 已丢弃", 15 | "qqBlocker_group_msg": "[群]收到群:{} 屏蔽用户{} 的信息: {}. 已丢弃", 16 | "general_user_msg": "[私]收到用户{} 的消息: {}", 17 | "general_group_msg": "[群]收到群:{} 用户:{} 的消息: {}", 18 | "general_unknown_msg": "[unknown]收到群:{} 用户:{} 的消息: {}", 19 | "limit_user_msg": "[私][level=1]收到用户:{} 的消息: {}", 20 | "limit_group_msg": "[群][level=1]收到群:{} 用户:{} 的消息: {}" 21 | } 22 | 23 | 24 | # basic:CQ码 25 | CONFIG_CQ_CODE = { 26 | "reply_img": "[CQ:image,file={}]", 27 | "reply_local_img": "[CQ:image,file=file:///{}]", 28 | "reply_at": "[CQ:at,qq='{}']", 29 | "reply_audio": "[CQ:record,file={}]", 30 | "reply_local_audio": "[CQ:record,file=file:///{}]" 31 | } 32 | 33 | 34 | # basic:bot_tool 35 | 36 | # 主动式插件消息未命中解析规则 37 | # 被动式插件跳过解析 38 | PLUGIN_IGNORE = 1 39 | 40 | 41 | # 主动式插件消息命中解析规则 42 | PLUGIN_BLOCK = 0 43 | 44 | 45 | TOOL_TEMP = { 46 | "load_config_success": "MybotConfigLoadSuccess - 配置加载成功", 47 | "load_config_error": "MybotConfigLoadError - 配置加载异常", 48 | "debug_status": "DEBUG STATUS: {}", 49 | "config_path_info": "MybotConfigFilePath - {}", 50 | "config_info": "MybotConfig - {}", 51 | 52 | # 消息发送 - 群 53 | "cq_http_send_group_url": "{}/send_group_msg", 54 | # 消息发送 - 私 55 | "cq_http_send_private_url": "{}/send_private_msg", 56 | # 消息发送(合并) - 群 57 | "cq_seng_group_forward_msg": "{}/send_group_forward_msg", 58 | # 获取消息 59 | "cq_get_msg": "{}/get_msg", 60 | # 撤回消息 - 撤回其他人的群消息需管理员权限 61 | "cq_delete_msg": "{}/delete_msg", 62 | # 获取群列表 63 | "cq_get_group_list": "{}/get_group_list", 64 | # 获取群信息 65 | "cq_get_group_info": "{}/get_group_info", 66 | # 处理加群请求 67 | "cq_set_group_add_request": "{}/set_group_add_request" 68 | } 69 | 70 | 71 | # basic:db_pool 72 | DB_TEMP = { 73 | "db_config_error": "请检查配置文件中Bot.{}的配置是否正确", 74 | "db_disable": "无法连接数据库,请启动数据库", 75 | "is_qqBlocked": 0 76 | } 77 | 78 | 79 | # basic:db_pool 80 | DB_SQL_TEMP = { 81 | "isExists_sql": "SELECT COUNT(1) FROM {} WHERE {} ", 82 | "select_sql": "SELECT * FROM {} WHERE {} ", 83 | "insert_sql": "INSERT INTO {} ({}) VALUES({})", 84 | "insert_sql_keys_str": """\ 85 | uid, gid, user_level, user_limit_cycle, user_limit_count, 86 | user_call_count, magic_thing, is_qqBlocked, create_date, 87 | last_call_date, cycle_expiration_time""" 88 | .replace(" ","").replace("\n",""), 89 | "update_sql": "UPDATE {} SET {} WHERE {}", 90 | "delete_sql": "DELETE FROM {} WHERE {}" 91 | } 92 | 93 | 94 | # basic:db_pool users表默认模板 95 | DB_INSERT_DEFAULT_TEMP = { 96 | "New_User": { 97 | "user_id": "", 98 | "group_id": "", 99 | "user_level": 10, 100 | "user_limit_cycle": 10, 101 | "user_limit_count": 3, 102 | "user_call_count": 0, 103 | "magic_thing": 0, 104 | "is_qqBlocked": 1, 105 | "create_date": "", 106 | "last_call_date": "", 107 | "cycle_expiration_time": "" 108 | } 109 | } 110 | 111 | 112 | # basic:task_processor 错误信息模板 113 | TASK_PROCESSOR_TEMP = { 114 | "TASK_START": "task start", 115 | "TASK_BREAK_TERMINAL": "task break by ", 116 | "TASK_NO_RECORD": "check:TABLE no tasks", 117 | "TASK_END": "task end. Sleep {}", 118 | "NULL_VALUE": "<{}> is null value", 119 | "NOT_DATETIME_DATA": "<{}> is not DATETIME data" 120 | } 121 | 122 | 123 | # basic:file_handler 124 | # FILEIO_ERROR_INFO = { 125 | # "loadYamlErrorInfo": "", 126 | # "YamlPath": "" 127 | # } 128 | 129 | 130 | # ===== basic end ===== 131 | 132 | 133 | # ===== module start ===== 134 | # module:bot_img_search 135 | IMG_SEARCH_TEMP_GENG = { 136 | "param_err": "Param Value - {}" 137 | } 138 | 139 | 140 | SEARCH_IMG_MSG = { 141 | "search_image_enable": "开启搜图", 142 | "search_image_quit": "关闭搜图", 143 | "reply_image": "[CQ:at,qq={}]\n搜图模式开启成功!\n请连续发送图片进行搜图吧~\n发送非图片信息自动退出搜图模式!", 144 | # 观察者对此项进行监控,重复开启则进入限制模式 145 | "reply_tip": "[CQ:at,qq={}]\n已开启搜图模式,请勿重复开启!", 146 | "reply_image_quit": "[CQ:at,qq={}]\n搜图模式关闭成功!", 147 | # 观察者对此项进行监控,重复开启则进入限制模式 148 | "reply_enable_search": "[CQ:at,qq={}]\n未启用搜图模式!请勿重复关闭!\n(加入黑名单)", 149 | "reply_bot_quit": "[CQ:at,qq={}]\n检测到发送非图片信息\n搜图模式自动关闭", 150 | } 151 | WHATANIME_MSG = { 152 | "enbale": "开启搜番" 153 | } 154 | 155 | 156 | # ===== module end ===== 157 | 158 | 159 | # ===== main start ===== 160 | # main:error_code 错误码 161 | MYBOT_ERR_CODE = { 162 | "Arsenal_Not_Found": "Arsenal目录不存在,请重新检查并创建", 163 | "Generic_Exception_Info": "Exception : {}", 164 | "Generic_Value_Info": "{} - {}", 165 | } 166 | 167 | 168 | # main:church.identify_data模板 169 | CHURCH_IDENTIFY_MSG = [ 170 | {"code": -100, "description": "识别到心跳包或其他未知原因"}, 171 | {"code": -1, "description": "识别到用户处于黑名单列表,将忽略信息"}, 172 | {"code": 0, "description": "距离下次调用还有{}秒,当前非法调用:{}次"}, 173 | {"code": 1, "description": "识别到用户再次过快调用,请等待20秒再调用"}, 174 | {"code": 10, "description": "识别到用户状态正常"}, 175 | {"code": 200, "description": "识别通过,用户状态正常"} 176 | ] 177 | 178 | 179 | # main:priority 优先级 180 | EXECUTOR_FUNCTION_LIST = [ 181 | {"priority": 500, "function": "SauceNao", "module": ""}, 182 | {"priority": 500, "function": "Ascii2d", "module": ""} 183 | ] 184 | 185 | 186 | # main:executor 187 | EXECUTOR_TASK_STATUS_INFO = { 188 | "info": "task_status:<{}> nums:<{}>" 189 | } 190 | 191 | 192 | # main:level_manager 193 | USER_LIMIT_TEMP = { 194 | "temp1": "请等待{}秒后再尝试...", 195 | "temp2": "请不要过快调用,至少等待{}秒后再使用" 196 | } 197 | # ===== main end ===== -------------------------------------------------------------------------------- /code/Arsenal/basic/plugin_class.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : PluginClass.py 4 | @Time : 2022/02/07 22:01:39 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | # 导入内置库及第三方库 12 | pass 13 | 14 | # 导入自定义模块 15 | from Arsenal.basic.plugin_res_directory import pdr 16 | from Arsenal.basic.log_record import logger 17 | from Arsenal.basic.msg_temp import PLUGIN_BLOCK, PLUGIN_IGNORE 18 | 19 | # 插件主类命名以模块文件(如:bot_ascii2d_img.py) 20 | # bot_之后的部分为名,并且首字母大写 21 | # 如:class Ascii2d_Img 22 | class PluginClass: 23 | """简要注释类功能""" 24 | 25 | def __init__(self): 26 | # 必须 27 | self.plugin_name = type(self).__name__ 28 | 29 | # 可选-插件别名 - 展示给用户,无则使用plugin_name 30 | self.plugin_nickname = "测试插件" 31 | 32 | # 插件类型 - 1-主动式插件,0-被动式插件,默认1 33 | self.plugin_type = 1 34 | # self.plugin_type = your plugin_type 35 | 36 | # 插件权限 - 10~999为主动式插件,1~9为被动式插件,默认为10 37 | self.plugin_level = [v for k,v in {1: 10, 0: 5}.items() if self.plugin_type == k][0] 38 | # self.plugin_level = your plugin_level 39 | 40 | # 静态资源文件目录 41 | self.resource_path = pdr.resource 42 | # self.resource_path = your resource path 43 | 44 | # 插件工作目录 45 | self.workspace = pdr.get_plus_res(self.plugin_name) 46 | # self.workspace = your workspace 47 | 48 | def help_info(self): 49 | """插件介绍""" 50 | return """|============|\n"""\ 51 | """| 暂无相关 |\n"""\ 52 | """| 插件介绍 |\n"""\ 53 | """|============|""" 54 | 55 | @logger.catch 56 | def parse(self,mybot_data:dict) -> dict: 57 | """ 58 | 每个功能插件的主类必须存在一个parse函数 59 | 用以解析mybot_data和执行插件功能 60 | 61 | :param mybot_data: mybot内部消息体 62 | :return: PLUGIN_BLOCK / PLUGIN_IGNORE 63 | """ 64 | 65 | """ 66 | # ===== 主动式插件demo ===== 67 | 68 | message = mybot_data["arrange"].get("message") 69 | # your expression 70 | if "hello world" in message: 71 | # send message to go-cq or anything 72 | # do something 73 | # 命中解析规则 74 | return PLUGIN_BLOCK 75 | elif "#time" in message: 76 | mybot_data["at"] = True 77 | mybot_data["message"] = now_time 78 | tool.auto_send_msg(mybot_data) 79 | return PLUGIN_BLOCK 80 | 81 | 82 | # 未命中,使用下一个插件解析规则进行解析 83 | return PLUGIN_IGNORE 84 | 85 | # ===== 主动式插件demo ===== 86 | # ===== 被动式插件demo ===== 87 | 88 | message = mybot_data["arrange"].get("message") 89 | # your expression 90 | if "hello world" in message: 91 | # do something 92 | # 无论是否命中解析规则,都返回PLUGIN_IGNORE 93 | return PLUGIN_IGNORE 94 | 95 | return PLUGIN_IGNORE 96 | 97 | # ===== 被动式插件demo ===== 98 | 99 | """ 100 | return PLUGIN_IGNORE 101 | 102 | # 实例化对象以'Bot_' + 类名来命名 103 | # 插件加载器通过识别'Bot_'(特征之一)来载入实例对象 104 | # 此处仅为注释说明使用 105 | # Bot_Ascii2d_Img = Ascii2d_Img() -------------------------------------------------------------------------------- /code/Arsenal/basic/plugin_res_directory.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : plus_res_directory.py 4 | @Time : 2021/03/18 14:28:41 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 提供目录相关功能 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | 13 | 14 | # from your_module import your func 15 | # from log_record import logger 16 | from Arsenal.basic.log_record import logger 17 | 18 | 19 | class PRDirectory: 20 | """检测/创建创建功能模块的res目录(工作目录)""" 21 | def __init__(self): 22 | # resource 静态资源文件目录 23 | self.resource = os.path.join(os.path.dirname(os.path.abspath(__file__)),"..\\..\\resource") 24 | # 插件工作目录 25 | self.workspace = os.path.join(os.path.dirname(os.path.abspath(__file__)),"..\\..\\workspace") 26 | if not os.path.exists(self.workspace): 27 | os.mkdir(self.workspace) 28 | 29 | def exists_path(self,path): 30 | """判断path是否存在""" 31 | return os.path.exists(path) 32 | 33 | def create_path(self,path): 34 | """根据path创建文件夹""" 35 | try: 36 | if not self.exists_path(path): 37 | os.mkdir(path) 38 | except Exception as e: 39 | logger.warning(f" - {e}") 40 | logger.warning(f" - {path}") 41 | return False 42 | else: 43 | return True 44 | 45 | def get_plus_res(self,plus_name): 46 | """返回插件对应的res文件目录 47 | :params plus_name: 功能插件名称 48 | :return: plus_res_path/None 49 | """ 50 | plus_res_path = os.path.join(self.workspace,plus_name) 51 | # 创建失败 52 | if not self.create_path(plus_res_path): 53 | return None 54 | return plus_res_path 55 | 56 | 57 | pdr = PRDirectory() -------------------------------------------------------------------------------- /code/Arsenal/basic/restart_mybot.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : restart_mybot.py 4 | @Time : 2022/01/26 23:18:48 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 重启mybot 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import sys 13 | 14 | def restart(): 15 | python = sys.executable 16 | os.execl(python, python, * sys.argv) -------------------------------------------------------------------------------- /code/Arsenal/basic/short_link.py: -------------------------------------------------------------------------------- 1 | # 网络请求函数修正后再统一导入使用 2 | # from BNConnect import baseRequest 3 | 4 | import requests 5 | 6 | class so985: 7 | def __init__(self): 8 | # http://api.985.so/api.php?format=json&url=http%3a%2f%2fwww.baidu.com&apikey=18816765357@94f53154fef918eb70d7b74f9a5dfc67 9 | # {"status":1,"url":"http://r6f.cn/Mjqh","err":""} 10 | self.suolink_api = "http://api.985.so/api.php" 11 | # self.host = "https://dlj.li/{}" 12 | 13 | def parse_msg(self): 14 | pass 15 | 16 | def get_slink(self,url): 17 | """ 18 | :params url: 需要缩短的url 19 | :return: 短链,错误信息 20 | 不报错则为空,报错则为err 21 | """ 22 | # 调用BNC模块 23 | try: 24 | # 这样不会直接编码 25 | # u = "https://dlj.li/api.php?url={}".format(url) 26 | params = { 27 | "url":url, 28 | "apikey":"18816765357@94f53154fef918eb70d7b74f9a5dfc67", 29 | } 30 | resp = requests.get(self.suolink_api,params=params) 31 | print(resp.text) 32 | except Exception as e: 33 | print("so985 ",e) 34 | return "短链生成失败-dlj" 35 | else: 36 | return resp.text 37 | 38 | if __name__ == '__main__': 39 | preview_url = "https://trace.moe/122137/[Ohys-Raws] Dogeza de Tanondemita - 08 (AT-X 1280x720 x264 AAC).mp4?start=101.58&end=123&token=FM2mP_l8IPxUaWAQ--D70A" 40 | so985().get_slink(preview_url) 41 | -------------------------------------------------------------------------------- /code/Arsenal/basic/sys_info.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | 4 | class SysInfo: 5 | def size2Mb(self,size,key=0): 6 | unit = ["KB","MB","GB","TB"] 7 | if size/1024 > 1024: 8 | return self.size2Mb(size/1024,key=key+1) 9 | else: 10 | return "%.2f%s" %(size/1024,unit[key]) 11 | 12 | def get_cpu_info(self): 13 | cpuper = psutil.cpu_percent(interval=1) 14 | # text 15 | CPU_TEXT = "CPU: {}% (In 1s)".format(cpuper) 16 | return CPU_TEXT 17 | 18 | def get_mem_info(self): 19 | # 获取内存的完整信息 20 | mem = psutil.virtual_memory() 21 | # 总内存大小 -- MB 22 | max_mem = self.size2Mb(mem.total) 23 | # 已使用内存 -- MB 24 | used_mem = self.size2Mb(mem.used) 25 | # 内存占用率 26 | mem_percent = mem.percent 27 | # text 28 | MEM_TEXT = "MEM: {}% {}/{}".format(mem_percent,used_mem,max_mem) 29 | return MEM_TEXT 30 | 31 | def get_disk_info(self): 32 | # 获取磁盘完整信息 33 | disk = psutil.disk_partitions(all=False) 34 | disk_info = [] 35 | for d in disk: 36 | # 获取分区状态 37 | name = d[0] 38 | try: 39 | info = { 40 | "name":name.split(":")[0], 41 | "disk_usage":str(psutil.disk_usage(name)[-1]), 42 | "total":self.size2Mb(psutil.disk_usage(name)[0]) 43 | } 44 | except PermissionError as e: 45 | name = name.split(":")[0], 46 | disk_info.append("磁盘:{} | {} 使用率: {}".format(name,"设备未就绪","设备未就绪")) 47 | else: 48 | disk_info.append("磁盘:{} | {} 使用率: {}%".format(info["name"],info["total"],info["disk_usage"])) 49 | 50 | DISK_TEXT = "DISK:\n" + "\n".join(disk_info) 51 | return DISK_TEXT 52 | 53 | def main(self): 54 | return self.get_cpu_info() + "\n" +\ 55 | self.get_mem_info() + "\n" +\ 56 | self.get_disk_info() 57 | 58 | sysinfo = SysInfo() 59 | 60 | if __name__ == "__main__": 61 | sysinfo = SysInfo() 62 | print(sysinfo.main()) -------------------------------------------------------------------------------- /code/Arsenal/basic/task_processor.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : task_processor.py 4 | @Time : 2021/11/26 10:25:03 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import time 12 | 13 | 14 | from Arsenal.basic.bot_tool import tool 15 | from Arsenal.basic.log_record import logger 16 | from Arsenal.basic.msg_temp import TASK_PROCESSOR_TEMP 17 | 18 | class TaskProcessor: 19 | def __init__(self): 20 | self.task_status_temp = ["completed", "ongoing", "waiting", "error"] 21 | self.all_flag = "all" 22 | 23 | def create_tasks(self,insert_data): 24 | """ 25 | 根据insert_data创建task记录 26 | :params insert_data: 任务信息, key顺序必须与数据库字段一样 27 | :return: True or False 28 | """ 29 | logger.debug(f" - {insert_data}") 30 | result = tool.db.insert_records(cqp_data=None, table="tasks", **{"insert_data": insert_data}) 31 | return result 32 | 33 | def get_tasks(self,nums=0,filter=None): 34 | """ 35 | 从tass表中根据优先级获取1~10个/全部waiting状态的任务 36 | :params nums: 获取的任务数量 37 | range:1~10 0 -> all 38 | :return: tasks_dict 39 | """ 40 | # result数量 41 | if 1 <= nums <= 10: 42 | limit = nums 43 | elif nums == 0: 44 | limit = 0 45 | else: 46 | limit = 10 47 | 48 | # 任务状态筛选 49 | if filter in self.task_status_temp: 50 | filter_dict = {"task_status": filter} 51 | elif filter == self.all_flag: 52 | filter_dict = {} 53 | else: 54 | filter_dict = {"task_status": "waiting"} 55 | 56 | result = tool.db.select_records(table="tasks",limit=limit,**filter_dict) 57 | result = sorted(result, key=lambda x: x["task_level"], reverse=True) 58 | return result 59 | 60 | """ 61 | TODO 2021年12月8日23:45:17写下 62 | 1. 后续版本再处理数据表执行任务 63 | 当前可参考Executor.cycle_task_detect 64 | 2. 移植到Executor内 65 | """ 66 | @logger.catch 67 | def exec_tasks(self,task): 68 | """ 69 | 执行任务 70 | :params task: task任务体及信息 71 | :return: None 72 | """ 73 | while True: 74 | # 执行前校验数据 75 | # ==== exec_time ==== 76 | # exec_time值是否存在 77 | if not task.get("exec_time",""): 78 | logger.info(TASK_PROCESSOR_TEMP["NULL_VALUE"].format("exec_time")) 79 | return False 80 | 81 | try: 82 | # exec_time格式化格式是否正确 83 | task["exec_time"].strftime('%Y-%m-%d %H:%M:%S') 84 | except Exception as e: 85 | logger.info(e) 86 | logger.info(TASK_PROCESSOR_TEMP["NOT_DATETIME_DATA"].format("exec_time")) 87 | return False 88 | 89 | # 判断是否为datetime.datetime对象 90 | # if not isinstance(task["exec_time"],datetime.datetime): 91 | # logger.info(TASK_PROCESSOR_TEMP["NOT_DATETIME_DATA"].format("exec_time")) 92 | # return False 93 | # ==== exec_time ==== 94 | 95 | # ==== exec_task ==== 96 | if not task["exec_task"]: 97 | logger.info(TASK_PROCESSOR_TEMP["NULL_VALUE"].format("exec_task")) 98 | return 99 | 100 | # ==== exec_task ==== 101 | try: 102 | result = eval(task["exec_task"]) 103 | except Exception as e: 104 | logger.warning(f" - {e}| - {task['exec_task']}") 105 | else: 106 | print(result) 107 | 108 | # break判断 109 | # 根据task_type判断是否跳出 110 | if task.get("task_type","once") == "once": 111 | break 112 | elif task.get("task_type","once") == "cycle": 113 | pass 114 | else: 115 | break 116 | 117 | time.sleep(10) 118 | 119 | @staticmethod 120 | def task_count(task_list): 121 | _count_result = {} 122 | for _ in task_list: 123 | if _["task_status"] not in _count_result.keys(): 124 | _count_result[_["task_status"]] = 1 125 | else: 126 | _count_result[_["task_status"]] += 1 127 | return _count_result 128 | 129 | 130 | 131 | taskprocessor = TaskProcessor() -------------------------------------------------------------------------------- /code/Arsenal/basic/thread_pool.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | """ 4 | 一个基于thread和queue的线程池, 5 | 任务为队列元素,动态创建线程并重复利用. 6 | 通过close和terminate关闭线程池. 7 | """ 8 | 9 | import queue 10 | import threading 11 | import contextlib 12 | import time 13 | 14 | # 创建空对象,用于停止线程 15 | StopEvent = object() 16 | 17 | 18 | def callback(status, result): 19 | """ 20 | 根据需要进行的回调函数,默认不执行。 21 | :param status: action函数的执行状态 22 | :param result: action函数的返回值 23 | :return: 24 | """ 25 | pass 26 | 27 | 28 | def action(thread_name, arg): 29 | """ 30 | 真实的任务定义在这个函数里 31 | :param thread_name: 执行该方法的线程名 32 | :param arg: 该函数需要的参数 33 | :return: 34 | """ 35 | # 模拟执行 36 | time.sleep(0.1) 37 | print("第%s个任务调用了线程 %s,并打印了这条信息!" % (arg+1, thread_name)) 38 | 39 | 40 | class ThreadPool: 41 | 42 | def __init__(self, max_num, max_task_num=None): 43 | """ 44 | 初始化线程池 45 | :param max_num: 线程池最大线程数量 46 | :param max_task_num: 任务队列长度 47 | """ 48 | # 如果提供了最大任务数的参数,则将队列的最大元素个数设置为这个值。 49 | max_task_num = 20 50 | if max_task_num: 51 | self.q = queue.Queue(max_task_num) 52 | # 默认队列可接受无限多个的任务 53 | else: 54 | self.q = queue.Queue() 55 | # print("创建好队列了") 56 | # 设置线程池最多可实例化的线程数 57 | self.max_num = max_num 58 | # 任务取消标识 59 | self.cancel = False 60 | # 任务中断标识 61 | self.terminal = False 62 | # 已实例化的线程列表 63 | self.generate_list = [] 64 | # 处于空闲状态的线程列表 65 | self.free_list = [] 66 | 67 | def put(self, func, args, callback=None): 68 | """ 69 | 往任务队列里放入一个任务 70 | :param func: 任务函数 71 | :param args: 任务函数所需参数 72 | :param callback: 任务执行失败或成功后执行的回调函数,回调函数有两个参数 73 | 1、任务函数执行状态;2、任务函数返回值(默认为None,即:不执行回调函数) 74 | :return: 如果线程池已经终止,则返回True否则None 75 | """ 76 | # 先判断标识,看看任务是否取消了 77 | if self.cancel: 78 | return 79 | # 如果没有空闲的线程,并且已创建的线程的数量小于预定义的最大线程数,则创建新线程。 80 | if len(self.free_list) == 0 and len(self.generate_list) < self.max_num: 81 | self.generate_thread() 82 | # 构造任务参数元组,分别是调用的函数,该函数的参数,回调函数。 83 | w = (func, args, callback,) 84 | # 将任务放入队列 85 | self.q.put(w) 86 | 87 | def generate_thread(self): 88 | """ 89 | 创建一个线程 90 | """ 91 | # 每个线程都执行call方法 92 | t = threading.Thread(target=self.call) 93 | # 用于定时任务跟随主线程结束 94 | t.setDaemon(True) 95 | t.start() 96 | 97 | def call(self): 98 | """ 99 | 循环去获取任务函数并执行任务函数。在正常情况下,每个线程都保存生存状态,直到获取线程终止的flag。 100 | """ 101 | # 获取当前线程的名字 102 | current_thread = threading.currentThread().getName() 103 | # 将当前线程的名字加入已实例化的线程列表中 104 | self.generate_list.append(current_thread) 105 | # 从任务队列中获取一个任务 106 | event = self.q.get() 107 | # 让获取的任务不是终止线程的标识对象时 108 | while event != StopEvent: 109 | # 解析任务中封装的三个参数 110 | func, arguments, callback = event 111 | # 抓取异常,防止线程因为异常退出 112 | try: 113 | # 正常执行任务函数 114 | result = func(*arguments) 115 | # result = func(current_thread, *arguments) 116 | success = True 117 | except Exception as e: 118 | # 当任务执行过程中弹出异常 119 | result = None 120 | success = False 121 | # 如果有指定的回调函数 122 | if callback is not None: 123 | # 执行回调函数,并抓取异常 124 | try: 125 | callback(success, result) 126 | except Exception as e: 127 | pass 128 | # 当某个线程正常执行完一个任务时,先执行worker_state方法 129 | with self.worker_state(self.free_list, current_thread): 130 | # 如果强制关闭线程的flag开启,则传入一个StopEvent元素 131 | if self.terminal: 132 | event = StopEvent 133 | # 否则获取一个正常的任务,并回调worker_state方法的yield语句 134 | else: 135 | # 从这里开始又是一个正常的任务循环 136 | event = self.q.get() 137 | else: 138 | # 一旦发现任务是个终止线程的标识元素,将线程从已创建线程列表中删除 139 | self.generate_list.remove(current_thread) 140 | 141 | def close(self): 142 | """ 143 | 执行完所有的任务后,让所有线程都停止的方法 144 | """ 145 | # 设置flag 146 | self.cancel = True 147 | # 计算已创建线程列表中线程的个数, 148 | # 然后往任务队列里推送相同数量的终止线程的标识元素 149 | full_size = len(self.generate_list) 150 | while full_size: 151 | self.q.put(StopEvent) 152 | full_size -= 1 153 | 154 | def terminate(self): 155 | """ 156 | 在任务执行过程中,终止线程,提前退出。 157 | """ 158 | self.terminal = True 159 | # 强制性的停止线程 160 | while self.generate_list: 161 | self.q.put(StopEvent) 162 | 163 | # 该装饰器用于上下文管理 164 | @contextlib.contextmanager 165 | def worker_state(self, state_list, worker_thread): 166 | """ 167 | 用于记录空闲的线程,或从空闲列表中取出线程处理任务 168 | """ 169 | # 将当前线程,添加到空闲线程列表中 170 | state_list.append(worker_thread) 171 | # 捕获异常 172 | try: 173 | # 在此等待 174 | yield 175 | finally: 176 | # 将线程从空闲列表中移除 177 | state_list.remove(worker_thread) 178 | 179 | """ 180 | 181 | # 调用方式 182 | if __name__ == '__main__': 183 | # 创建一个最多包含5个线程的线程池 184 | # pool = ThreadPool(5) 185 | 186 | # 创建100个任务,让线程池进行处理 187 | # for i in range(100): 188 | # pool.put(action, (i,), callback) 189 | 190 | # 等待一定时间,让线程执行任务 191 | # time.sleep(3) 192 | 193 | print("-" * 50) 194 | while True: 195 | print("\033[32;0m任务停止之前线程池中有%s个线程,空闲的线程有%s个!\033[0m" 196 | % (len(pool.generate_list), len(pool.free_list))) 197 | 198 | if len(pool.free_list) == pool.max_num and len(pool.generate_list) == 5: 199 | # 正常关闭线程池 200 | pool.close() 201 | print("任务执行完毕,正常退出!") 202 | break 203 | 204 | time.sleep(1) 205 | # 强制关闭线程池 206 | # pool.terminate() 207 | # print("强制停止任务!") 208 | 209 | """ -------------------------------------------------------------------------------- /code/Arsenal/basic/user_data.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : user_data.py 4 | @Time : 2022/01/19 11:04:33 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | from Arsenal.basic.bot_tool import tool 12 | 13 | class UserData: 14 | def __init__(self, eval_cqp_data): 15 | self.judge_data = self.get_judge_data(eval_cqp_data) 16 | self.mybot_data = { 17 | "arrange": eval_cqp_data, 18 | "user_info": {}, 19 | "sender": { 20 | "user_id": int(self.judge_data.get("uid", 0)), 21 | "group_id": int(self.judge_data.get("gid", 0)), 22 | "type": self.judge_data.get("message_type", 0), 23 | }, 24 | "at": True, 25 | # 回复消息 26 | "message": "", 27 | "plugin": {}, 28 | } 29 | 30 | def __enter__(self): 31 | # 非新用户 32 | _kwargs = {"uid": self.judge_data.get("uid", 0), "gid": self.judge_data.get("gid", 0)} 33 | select_result:list = tool.db.select_records(**_kwargs) 34 | # 新用户 35 | if not select_result: 36 | # 判断是否在里群(level>=50) 37 | group_result = tool.db.select_records(table="group_chats", **{"gid":self.judge_data.get("gid", 0)}) 38 | if group_result: 39 | group_level = int(group_result.get("group_level", tool.level["general_group_level"])) 40 | else: 41 | group_level = tool.level["general_group_level"] 42 | # group_level = int(tool.db.select_records(table="group_chats", **{"gid":self.judge_data["gid"]})["group_level"]) 43 | 44 | if group_level >= tool.level["vip_group_level"]: 45 | result:dict = tool.db.insert_records(self.mybot_data, **{"level": tool.level["vip_user_level"]}) 46 | else: 47 | result:dict = tool.db.insert_records(self.mybot_data) 48 | else: 49 | result:dict = select_result[0] 50 | 51 | self.mybot_data["user_info"] = result 52 | return self.mybot_data 53 | 54 | def __exit__(self,exc_type,exc_value,exc_trackback): 55 | pass 56 | 57 | def get_judge_data(self, eval_cqp_data): 58 | message = eval_cqp_data.get("message", "") 59 | uid = eval_cqp_data.get("user_id",0) 60 | gid = eval_cqp_data.get("group_id",0) 61 | message_type = eval_cqp_data.get("message_type", "") 62 | 63 | judge_data = {"uid": uid, "gid": gid, "message_type": message_type, "message": message} 64 | 65 | for k in list(judge_data.keys()): 66 | if not judge_data[k]: 67 | del judge_data[k] 68 | 69 | if not judge_data: 70 | return {} 71 | return judge_data 72 | -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_blhx_build_pool -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/blhx_tool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : blhx_tool.py 4 | @Time : 2022/02/09 15:53:42 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import json 13 | 14 | from Arsenal.basic.plugin_res_directory import pdr 15 | 16 | class BLHX_Tool: 17 | """碧蓝航线模拟建造插件 - 全局变量""" 18 | def __init__(self, resource_path, workspace): 19 | self.resource_path = resource_path 20 | self.workspace = workspace 21 | 22 | ##### 主类Blhx_Build_Pool ##### 23 | # 合成所需船坞头像目录 24 | self.root_dir = os.path.join(self.resource_path, "shipyardicon") 25 | 26 | # 所有舰娘数据 暂时只有主类使用 27 | self.ships_all_data_filename = os.path.join(self.resource_path,"ships_all_data.json") 28 | self.ships_all_data = json.load(open(self.ships_all_data_filename,encoding="utf8")) 29 | 30 | 31 | ##### Blhx_Magic_Data ##### 32 | # 用户舰娘图鉴数据目录 - 收藏率 33 | self.user_ship_data_dir = os.path.join(self.workspace,"user_ship_data") 34 | pdr.create_path(self.user_ship_data_dir) 35 | # TODO delete 用户数据 36 | # self.user_now_data = {} 37 | 38 | 39 | ##### Lucky_Ships_Pool ##### 40 | # 建造池数据 41 | self.ship_key_filename = "pool_key.json" 42 | self.key_filepath = os.path.join(self.resource_path,self.ship_key_filename) 43 | 44 | 45 | ##### Build_Ships_Card ##### 46 | # 生成图片路径 47 | self.build_card_result_path = os.path.join(self.workspace,"build_card_result") 48 | pdr.create_path(self.build_card_result_path) 49 | 50 | # 建造背景图片 51 | self.img_build_bg_path = os.path.join(self.resource_path, "static", "build_bg_final_2_RGBA.png") 52 | # new icon 53 | self.new_ico_path = os.path.join(self.resource_path, "static", "new_resize.png") 54 | # new icon 坐标 55 | self.new_coordinate_list = [ 56 | (135,50),(244,50),(350,50),(456,50),(562,50), 57 | (180,194),(289,194),(395,194),(501,194),(607,194), 58 | ] 59 | # img 坐标 60 | self.img_coordinate_list = [ 61 | (114,89),(223,89),(329,89),(435,89),(541,89), 62 | (159,233),(268,233),(374,233),(480,233),(586,233) 63 | ] 64 | 65 | 66 | # ====================自定义======================== 67 | # 关键词 68 | self.keyword = "一键十连" 69 | self.keyword_help = "help" 70 | # 最低魔方需求 71 | self.pool_limit_magic = 10 72 | # 默认建造池 73 | self.default_pool = "啥都能建池" 74 | # 默认关闭 - 皮肤/改造/誓约显示 75 | self.default_skin = str(0) 76 | self.support_skin_value = ["0", "1"] 77 | # 默认倍率 78 | self.default_multiple = 1 79 | 80 | ##### 支持魔方-倍率提升的池子名称 ##### 81 | self.support_multiple_pools = ["活动池"] 82 | 83 | # 初始化模板 84 | self.msg_temp() 85 | self.error_temp() 86 | 87 | def activity_pool_info(self): 88 | """活动池名称介绍""" 89 | return {"name": "「镜位螺旋」"} 90 | 91 | def msg_temp(self): 92 | self.text_temp = { 93 | "help": """<{}>使用方法:\n发送'一键十连'"""\ 94 | """\n添加参数: 发送'一键十连' -pool=活动池 -skin=1 -m=10"""\ 95 | """\n\n-help : 查看此条帮助信息"""\ 96 | """\n-pool : 指定建造池, 参考<当前可用池子>"""\ 97 | """\n-skin : 皮肤/誓约/改造的显示, 默认为0,关闭; 1为开启"""\ 98 | """\n-m : 额外消耗m个魔方(1~10), 提高所有抽卡概率m倍\n仅限<支持倍率提升池子>"""\ 99 | """\n\n当前可用池子:\n● {}"""\ 100 | """\n\n当前支持倍率提升池子:\n● {}"""\ 101 | """\n\n当前收藏率: {}"""\ 102 | """\n当前魔方: {}个"""\ 103 | """\n当前活动池为:「{}」活动"""\ 104 | """\n\n更多文档: https://www.yuque.com/mybot/blhx""", 105 | "success": """当前建造池: {}\n建造结果如下图:\n"""\ 106 | """[CQ:image,file=file:///{}]"""\ 107 | """消耗魔方: {}\n"""\ 108 | """当前魔方: {}\n"""\ 109 | """当前收藏率: {}""" 110 | } 111 | 112 | def error_temp(self): 113 | self.err_temp = { 114 | "pool_name_err": "指定的建造池<{}>不存在", 115 | "pool_name_err_msg": "指定的<{}>建造池不存在!\n可以使用'一键十连 -help'来查看详细信息", 116 | "pool_exists_err_msg": "指定的<{}>建造池索引文件不存在,请通知管理员!\n(: ′⌒`)", 117 | "magic_thing_err_msg": "十连建造至少需要10个魔方.\n当前魔方: {}个", 118 | "skin_err_msg": f"用户未显式指定skin或指定的参数错误,将采用默认参数 - {self.default_skin}", 119 | "multiple_err_msg": f"指定的参数超出支持范围,将采用默认参数 - {self.default_multiple}", 120 | "general_err": "<{} value> err - {}", 121 | "not_found_user_err": "未查询到用户相关数据 - {}" 122 | } -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/bot_blhx_build_pool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : bot_blhx_build_pool.py 4 | @Time : 2021/03/30 17:30:10 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 碧蓝航线十连建造 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | 13 | from Arsenal.basic.bot_tool import tool 14 | from Arsenal.basic.log_record import logger 15 | from Arsenal.basic.plugin_res_directory import pdr 16 | from Arsenal.basic.plugin_class import PluginClass 17 | from Arsenal.basic.msg_temp import PLUGIN_BLOCK, PLUGIN_IGNORE, MYBOT_ERR_CODE 18 | 19 | from Arsenal.bot_blhx_build_pool.blhx_tool import BLHX_Tool 20 | from Arsenal.bot_blhx_build_pool.build_pool import Lucky_Ships_Pool 21 | from Arsenal.bot_blhx_build_pool.build_card import Build_Ships_Card 22 | from Arsenal.bot_blhx_build_pool.data_handler import Blhx_Magic_Data 23 | 24 | 25 | class Blhx_Build_Pool(PluginClass): 26 | """碧蓝航线模拟十连建造 - 主类""" 27 | def __init__(self): 28 | super(PluginClass, self).__init__() 29 | # 必须 30 | self.plugin_name = type(self).__name__ 31 | 32 | # 可选-插件别名 - 展示给用户,无则使用plugin_name 33 | self.plugin_nickname = "碧蓝航线-模拟建造插件(十连)" 34 | 35 | # 插件类型 - 1-主动式插件,0-被动式插件,默认1 36 | self.plugin_type = 1 37 | 38 | # 插件权限 - 10~999为主动式插件,1~9为被动式插件,默认为10 39 | # self.plugin_level = [v for k,v in {1: 10, 0: 5}.items() if self.plugin_type == k][0] 40 | self.plugin_level = 10 41 | 42 | # 静态资源文件目录 43 | self.resource_path = os.path.join(pdr.resource, "Blhx_Build_Pool") 44 | # self.resource_path 无则不填 45 | 46 | # 插件工作目录 47 | self.workspace = pdr.get_plus_res(self.plugin_name) 48 | 49 | ####### 插件内部全局变量 50 | self.blhx_tool = BLHX_Tool(self.resource_path, self.workspace) 51 | 52 | self.LSP = Lucky_Ships_Pool(self.blhx_tool) 53 | self.BSC = Build_Ships_Card(self.blhx_tool) 54 | self.BMD = Blhx_Magic_Data(self.blhx_tool) 55 | 56 | def help_info(self): 57 | # 可用池 58 | poolList_Text = "\n● ".join(list(self.LSP.ship_key_data.keys())) 59 | # 支持倍率提升的池子 60 | support_multiple_pools_text = "\n● ".join(self.blhx_tool.support_multiple_pools) 61 | # 收藏率 62 | ships_user_data = self.BMD.r_blhx_data(extra_id=self.mybot_data["sender"]["user_id"]) 63 | bookmark_rate = str(round(len(ships_user_data)/len(self.blhx_tool.ships_all_data)*100,3)) + "%" 64 | 65 | message = self.blhx_tool.text_temp["help"].format( 66 | self.plugin_nickname, poolList_Text, support_multiple_pools_text, bookmark_rate, 67 | str(self.mybot_data["user_info"]["magic_thing"]), self.blhx_tool.activity_pool_info()["name"] 68 | ) 69 | return message 70 | 71 | def find_ship_data(self,name): 72 | """ 73 | 1.返回舰娘数据 74 | 2.根据舰娘名称找出对应船坞头像路径 75 | :params name: 舰娘名称 76 | :return: data 77 | data: { 78 | 'shipName': '栭', 79 | 'shipRawName': '谷风', 80 | 'shipType': '驱逐', 81 | 'starLevelText': '稀有', 82 | 'starLevel': 'R', 83 | 'shipCamp': '重樱', 84 | 'shipNum': 'NO.319', 85 | 'shipPath': 'D:\\Code\\BLHX\\workspace\\ship\\稀有\\栭' 86 | } 87 | """ 88 | data = {} 89 | for _ in self.blhx_tool.ships_all_data: 90 | if _["shipName"] == name: 91 | data = _ 92 | break 93 | 94 | if not data: 95 | return {} 96 | else: 97 | logger.debug(f"{data}") 98 | starLevelText = data["starLevelText"] 99 | if starLevelText in ["最高方案","海上传奇","决战方案"]: 100 | starLevelText = "超稀有" 101 | shipPath = os.path.join(self.blhx_tool.root_dir, starLevelText, data["shipName"]) 102 | data["shipPath"] = shipPath 103 | return data 104 | 105 | def get_card(self,extra={}): 106 | """ 107 | 生成并返回模拟建造的图片路径 108 | :params extra: 额外参数 109 | :return: path or None 110 | """ 111 | try: 112 | ship_list = self.LSP.get_lucky_ships( 113 | pool_name = extra["pool"], 114 | multiple = extra["m"]) 115 | except Exception as e: 116 | logger.info(f"ship_list error:{e}") 117 | ship_list = ["pool_data_items_error"] 118 | 119 | if ship_list == ["pool_data_items_error"]: 120 | return "" 121 | 122 | ship_data = [] 123 | for ship in ship_list: 124 | data = self.find_ship_data(name=ship) 125 | if data: 126 | ship_data.append(data) 127 | 128 | # 有部分data为空,异常处理--待定 129 | if len(ship_data) != 10: 130 | logger.info(f"部分舰娘数据缺失,请更新或重新检查. - {ship_list}") 131 | return "" 132 | 133 | # 舰娘new数据添加 134 | ship_data = self.BMD.check_ship(ship_list, ship_data) 135 | 136 | # 生成图片 137 | try: 138 | build_card_path = self.BSC.build_card(ship_data, extra=extra) 139 | except Exception as e: 140 | logger.warning(f"build_card err - {e}") 141 | logger.warning(f" - {ship_data}") 142 | build_card_path = "" 143 | else: 144 | # 添加新舰娘数据 145 | self.BMD.w_blhx_data(ship_data) 146 | finally: 147 | return build_card_path 148 | 149 | def check_param(self, extra_params): 150 | """额外参数值校验 151 | pool - 指定建造池 152 | skin - 改造/皮肤/誓约等非原始立绘的控制 153 | multiple - 活动池抽卡倍率提升 154 | return True or False 155 | """ 156 | ## 客户端 ## 157 | # 检查pool 158 | if extra_params.get("pool","") == "": 159 | extra_params["pool"] = self.blhx_tool.default_pool 160 | 161 | # 无效的建造池指定 162 | if extra_params["pool"] not in list(self.LSP.ship_key_data.keys()): 163 | self.mybot_data["message"] = self.blhx_tool.err_temp["pool_name_err_msg"].format(extra_params["pool"]) 164 | tool.auto_send_msg(self.mybot_data) 165 | logger.warning(self.blhx_tool.err_temp["pool_name_err"].format(extra_params["pool"])) 166 | return PLUGIN_BLOCK 167 | 168 | # 建造池索引文件不存在 169 | pool_data_path = os.path.join(self.blhx_tool.resource_path, self.LSP.ship_key_data[extra_params["pool"]]) 170 | if not os.path.exists(pool_data_path): 171 | self.mybot_data["message"] = self.blhx_tool.err_temp["pool_exists_err_msg"].format(extra_params["pool"]) 172 | tool.auto_send_msg(self.mybot_data) 173 | logger.warning(self.blhx_tool.err_temp["pool_exists_err_msg"].format(extra_params["pool"])) 174 | return PLUGIN_BLOCK 175 | 176 | # 检查魔方 177 | if int(self.mybot_data["user_info"]["magic_thing"]) < self.blhx_tool.pool_limit_magic: 178 | self.mybot_data["message"] = self.blhx_tool.err_temp["magic_thing_err_msg"].format( 179 | self.mybot_data['user_info']['magic_thing']) 180 | tool.auto_send_msg(self.mybot_data) 181 | logger.warning(self.blhx_tool.err_temp["magic_thing_err_msg"].format( 182 | self.mybot_data['user_info']['magic_thing'])) 183 | return PLUGIN_BLOCK 184 | 185 | # 检查skin 186 | if not extra_params.get("skin","") or \ 187 | extra_params.get("skin","") not in self.blhx_tool.support_skin_value: 188 | logger.warning(self.blhx_tool.err_temp["skin_err_msg"]) 189 | extra_params["skin"] = self.blhx_tool.default_skin 190 | 191 | # multiple 192 | if not extra_params.get("m",""): 193 | extra_params["m"] = self.blhx_tool.default_multiple 194 | 195 | try: 196 | extra_params["m"] = abs(int(extra_params["m"])) 197 | except: 198 | self.mybot_data["message"] = "参数m(multiple)错误,请重新输入" 199 | tool.auto_send_msg(self.mybot_data) 200 | logger.warning("参数m(multiple)错误,请重新输入") 201 | return PLUGIN_BLOCK 202 | 203 | if extra_params["m"] > 10 or extra_params["m"] <= 0: 204 | logger.warning(self.blhx_tool.err_temp["multiple_err_msg"]) 205 | extra_params["m"] = self.blhx_tool.default_multiple 206 | 207 | if extra_params["m"] != self.blhx_tool.default_multiple: 208 | # 池子不支持提升multiple 209 | if extra_params["pool"] not in self.blhx_tool.support_multiple_pools: 210 | support_multiple_text = ",".join(self.blhx_tool.support_multiple_pools) 211 | self.mybot_data["message"] = "当前指定的建造池: <{}>不支持使用-m参数来提升up舰娘出货倍率!\n仅{}支持".\ 212 | format(extra_params["pool"],support_multiple_text) 213 | tool.auto_send_msg(self.mybot_data) 214 | return PLUGIN_BLOCK 215 | return PLUGIN_IGNORE 216 | 217 | @logger.catch 218 | def parse(self, mybot_data): 219 | self.mybot_data = mybot_data 220 | self.blhx_tool.mybot_data = mybot_data 221 | 222 | if self.mybot_data["sender"].get('type','') == 'group': 223 | group_id = self.mybot_data["sender"]["group_id"] 224 | user_id = self.mybot_data["sender"]["user_id"] 225 | message = self.mybot_data["arrange"]['message'] 226 | split_words = message.split(" ") 227 | 228 | # 基本关键词检测 229 | if split_words[0] != self.blhx_tool.keyword: 230 | return PLUGIN_IGNORE 231 | 232 | # TODO delete 查询到用户数据 233 | # self.blhx_tool.user_now_data = self.mybot_data["user_info"] 234 | # if not self.blhx_tool.user_now_data: 235 | if not self.mybot_data["user_info"]: 236 | self.mybot_data["message"] = "未查询到用户相关数据" 237 | tool.auto_send_msg(self.mybot_data) 238 | logger.warning(self.blhx_tool.err_temp["not_found_user_err"].format(self.mybot_data)) 239 | # 结束消息周期 240 | return PLUGIN_BLOCK 241 | 242 | # 整合传参 243 | extra_params = {} 244 | for word in split_words[1:]: 245 | k = word.split("=")[0].replace("-","") 246 | if k == self.blhx_tool.keyword_help: 247 | extra_params = k 248 | break 249 | else: 250 | v = word.split("=")[1] 251 | extra_params[k] = v 252 | 253 | # help 254 | if extra_params == self.blhx_tool.keyword_help: 255 | self.mybot_data["message"] = self.help_info() 256 | tool.auto_send_msg(self.mybot_data) 257 | # 结束消息周期 258 | return PLUGIN_BLOCK 259 | 260 | # 检查参数 261 | logger.debug(MYBOT_ERR_CODE["Generic_Value_Info"].format("extra_params",extra_params)) 262 | if not self.check_param(extra_params): 263 | # 结束消息周期 264 | return PLUGIN_BLOCK 265 | logger.debug(MYBOT_ERR_CODE["Generic_Value_Info"].format("extra_params",extra_params)) 266 | 267 | 268 | build_card_path = self.get_card(extra=extra_params) 269 | # 服务端错误 270 | if not build_card_path: 271 | self.mybot_data["message"] = "非常抱歉o(╥~~╥)o!当前建造池数据文件出错,请@管理员进行修复~" 272 | tool.auto_send_msg(self.mybot_data) 273 | return PLUGIN_BLOCK 274 | else: 275 | # 计算消耗的魔方 276 | magic_cost = 10 277 | if extra_params["m"] != self.blhx_tool.default_multiple: 278 | magic_cost = 10 + extra_params["m"] 279 | 280 | # 更新魔方数量 281 | magic_old = self.mybot_data["user_info"]["magic_thing"] 282 | magic_now = int(magic_old) - int(magic_cost) 283 | self.mybot_data["user_info"]["magic_thing"] = magic_now 284 | tool.db.update_records(**{ 285 | "update_data": self.mybot_data["user_info"], 286 | "judge_data": {"uid": user_id, "gid": group_id} 287 | }) 288 | 289 | # 收藏率 290 | ships_user_data = self.BMD.r_blhx_data(extra_id=self.mybot_data["sender"]["user_id"]) 291 | bookmark_rate = str(round(len(ships_user_data)/len(self.blhx_tool.ships_all_data)*100,3)) + "%" 292 | self.mybot_data["message"] = self.blhx_tool.text_temp["success"].format( 293 | extra_params["pool"], build_card_path, 294 | int(magic_cost), magic_now, bookmark_rate 295 | ) 296 | tool.auto_send_msg(self.mybot_data) 297 | return PLUGIN_BLOCK 298 | 299 | # 默认跳过 300 | return PLUGIN_IGNORE 301 | 302 | 303 | Bot_Blhx_Build_Pool = Blhx_Build_Pool() -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/build_card.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : build_card.py 4 | @Time : 2022/02/09 11:02:29 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import time 13 | import random 14 | from PIL import Image,ImageDraw 15 | 16 | from Arsenal.basic.log_record import logger 17 | 18 | 19 | class Build_Ships_Card: 20 | """合成十连界面""" 21 | def __init__(self, blhx_tool): 22 | self.blhx_tool = blhx_tool 23 | 24 | def build_card(self, ship_data, extra): 25 | """ 26 | 十连建造图片合成 27 | :params ship_data: 舰娘数据 28 | :params extra: 额外参数 29 | :return: build_card_path,合成后的十连建造图片路径 30 | """ 31 | # 关闭 - 皮肤/改造/誓约显示 32 | if extra.get("skin","") == self.blhx_tool.default_skin: 33 | result_img_path = [] 34 | for i in ship_data: 35 | list_files = [] 36 | for j in os.listdir(i["shipPath"]): 37 | _ = j.rsplit(".",1)[0] 38 | if "." not in _: 39 | list_files.append(j) 40 | try: 41 | temp_path = os.path.join(i["shipPath"],random.choice(list_files)) 42 | except Exception as e: 43 | logger.warning(f"Err - {i} {os.listdir(i['shipPath'])}") 44 | temp_path = os.path.join(i["shipPath"],random.choice(os.listdir(i["shipPath"]))) 45 | 46 | result_img_path.append(temp_path) 47 | else: 48 | result_img_path = [os.path.join(i["shipPath"],random.choice(os.listdir(i["shipPath"]))) for i in ship_data] 49 | 50 | # 舰娘船坞头像图片 51 | img_ships_list = [] 52 | for img_path in result_img_path: 53 | img_ship = Image.open(img_path) 54 | img_ship = img_ship.resize((91,128)) 55 | img_ship = self.circle_corner(img_ship,radii=7) 56 | img_ships_list.append(img_ship) 57 | 58 | # 十连建造背景 59 | img_build_bg = Image.open(self.blhx_tool.img_build_bg_path) 60 | # new标识 61 | new = Image.open(self.blhx_tool.new_ico_path) 62 | # new坐标 63 | new_coordinate_list = self.blhx_tool.new_coordinate_list 64 | # 图片坐标 65 | img_coordinate_list = self.blhx_tool.img_coordinate_list # img_coordinate_list 66 | 67 | # 粘贴船坞头像 68 | for i in range(len(img_ships_list)): 69 | img_build_bg.paste(img_ships_list[i], img_coordinate_list[i], mask=img_ships_list[i].split()[-1]) 70 | 71 | # 粘贴new标识 72 | for i in range(len(new_coordinate_list)): 73 | if ship_data[i]["isNew"]: 74 | img_build_bg.paste(new, new_coordinate_list[i], mask=new.split()[-1]) 75 | 76 | build_card_path = os.path.join(self.blhx_tool.build_card_result_path,"{}.png".format(int(time.time()))) 77 | img_build_bg.save(build_card_path,qulity=100) 78 | return build_card_path 79 | 80 | def circle_corner(self, img, radii): 81 | """ 82 | 圆角处理 83 | :param img: 源图象。 84 | :param radii: 半径,如: 30 85 | :return: 返回一个圆角处理后的img 86 | """ 87 | # 画圆 (用于分离4个角) 88 | circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建一个黑色背景的画布 89 | # circle = Image.new('RGBA', (radii * 2, radii * 2)) # 创建一个黑色背景的画布 90 | draw = ImageDraw.Draw(circle) 91 | draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 画白色圆形 92 | # 原图 93 | img = img.convert("RGBA") 94 | w, h = img.size 95 | 96 | # 四角 97 | alpha = Image.new('L', img.size, 255) 98 | # 左上角 99 | alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) 100 | # 右上角 101 | alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) 102 | # 右下角 103 | alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii)) 104 | # 左下角 105 | alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) 106 | # alpha.show() 107 | 108 | img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见 109 | return img -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/build_pool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : build_pool.py 4 | @Time : 2022/02/09 10:57:24 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import random 13 | 14 | from Arsenal.basic.file_handler import loadFile, dumpFile 15 | from Arsenal.basic.log_record import logger 16 | 17 | 18 | class Lucky_Ships_Pool: 19 | """构建卡池及建造函数""" 20 | def __init__(self, blhx_tool): 21 | self.blhx_tool = blhx_tool 22 | # 池子名称与索引文件 23 | self.ship_key_data = self.read_json(self.blhx_tool.key_filepath) 24 | 25 | def get_random_num(self): 26 | """获取随机数""" 27 | return round(random.random(),4) 28 | 29 | def read_json(self,path): 30 | return loadFile.by_json(path) 31 | 32 | def reload_json_data(self,path): 33 | return dumpFile.by_json(path) 34 | 35 | def get_rare_value(self,pool_rare_set,rare_random): 36 | """ 37 | 根据pool_rare_set和rare_random确认归属稀有度 38 | :params pool_rare_set: 池子数据-稀有度划分 39 | :params rare_random: 随机数0~1 40 | :return: rare_value 稀有度 41 | """ 42 | # 默认为0~7~19~45~100 43 | # 各稀有度占比,从小到大排序 44 | pool_rare_set = dict(sorted(pool_rare_set.items(),key=lambda i:i[-1])) 45 | start_rare = 0 46 | for k,v in pool_rare_set.items(): 47 | # 各稀有度右侧边界值 = 上一次右侧边界值 + 本轮稀有度占比 48 | # logger.info(start_rare,start_rare + v) 49 | if start_rare < rare_random <= round((start_rare + v),3): 50 | rare_value = k 51 | return rare_value 52 | else: 53 | start_rare = round((start_rare + v),3) 54 | else: 55 | # 防止无返回值,返回最低稀有度 56 | return list(pool_rare_set.keys())[-1] 57 | 58 | def get_lucky_ships(self, pool_name:str, 59 | count:int=10, 60 | multiple:int=1, 61 | cheating_list:list=[])->list: 62 | """碧蓝航线模拟十连建造 63 | :params pool_name: 池子名称,如:轻型池/重型池/特型池 64 | :params count: 建造次数,最高10个 65 | :params multiple: 魔方-倍率参数 66 | 消耗个魔方提高舰娘判定概率倍 67 | :return: 舰娘信息list ['科隆','榊','狐提','太原','小天鹅','科尔克','布什','狻','倔强','牙买加'] 68 | """ 69 | # [作弊]请使用本地数据内包含的舰娘名称,否则后果自负 70 | if cheating_list: 71 | logger.debug(f" - {cheating_list}") 72 | return cheating_list 73 | 74 | # 负数或次数大于10则返回 75 | if count > 10 or count <= 0: 76 | logger.warning(self.blhx_tool.err_temp["general_err"].format("count", count)) 77 | return [] 78 | 79 | # 读取对应池子索引数据 80 | self.ship_key_data = self.read_json(self.blhx_tool.key_filepath) 81 | # 读取对应池子数据 82 | pool_data_path = os.path.join(self.blhx_tool.resource_path,self.ship_key_data.get(pool_name,"")) 83 | pool_data = self.read_json(pool_data_path) 84 | 85 | # 池子数据中各个节点 86 | if pool_data.get("ships_list","") == "" or \ 87 | pool_data.get("rare_set","") == "": 88 | logger.warning(self.blhx_tool.err_temp["general_err"].format("pool_data", pool_data)) 89 | return ["pool_data_items_error"] 90 | 91 | # 舰娘建造结果 92 | lucky_result = [] 93 | log_result = [] 94 | # 对应建造次数 95 | for i in range(count): 96 | log_info = {} 97 | pool_ships_list = pool_data["ships_list"] 98 | pool_extra_data = pool_data["extra"] 99 | pool_rare_set = pool_data["rare_set"] 100 | 101 | # 确认稀有度 102 | rare_random = self.get_random_num() 103 | rare_value = self.get_rare_value(pool_rare_set,rare_random) 104 | log_info["rare_random"] = rare_random 105 | log_info["rare_value"] = rare_value 106 | 107 | # 确认是否抽中up舰娘 108 | up_ship_rare = self.get_random_num() 109 | log_info["up_ship_rare"] = up_ship_rare 110 | 111 | up_ship_list = pool_extra_data.get(rare_value,"") 112 | # 有up舰娘 113 | if up_ship_list: 114 | # 取up概率去重并转为list 115 | up_values = list(set(list(up_ship_list.values()))) 116 | # 概率增幅multiple倍 117 | up_values = [_*multiple for _ in up_values] 118 | # 从低到高排序 119 | up_values.sort() 120 | log_info["up_values"] = up_values 121 | 122 | # 判断up_ship_rare是否有命中up 123 | up_ship_lucky_number = 0 124 | for _ in up_values: 125 | if up_ship_rare <= _: 126 | up_ship_lucky_number = round(_/multiple,3) 127 | break 128 | 129 | # 有命中up舰娘 130 | if up_ship_lucky_number != 0: 131 | same_lucky_number_ship = [k for k,v in up_ship_list.items() if v == up_ship_lucky_number] 132 | if not same_lucky_number_ship: 133 | # TODO 134 | lucky_ship = random.choice(pool_ships_list[rare_value]) 135 | else: 136 | lucky_ship = random.choice(same_lucky_number_ship) 137 | log_info["lucky_ship"] = lucky_ship 138 | lucky_result.append(lucky_ship) 139 | # 没有命中up舰娘 140 | else: 141 | # 将up舰娘从pool_ships_list中删除 142 | pool_ships_list_now = pool_ships_list[rare_value][::] 143 | for ship in pool_ships_list_now[::-1]: 144 | if ship in list(pool_extra_data[rare_value].keys()): 145 | pool_ships_list_now.remove(ship) 146 | 147 | # 随机选取一个对应稀有度的非up舰娘 148 | lucky_ship = random.choice(pool_ships_list_now) 149 | log_info["lucky_ship"] = lucky_ship 150 | lucky_result.append(lucky_ship) 151 | # 无up舰娘 152 | else: 153 | lucky_ship = random.choice(pool_ships_list[rare_value]) 154 | log_info["lucky_ship"] = lucky_ship 155 | lucky_result.append(lucky_ship) 156 | 157 | log_result.append(log_info) 158 | logger.debug(f"第{i+1}发建造结果: {log_info}") 159 | 160 | logger.info(f" - {lucky_result}") 161 | return lucky_result -------------------------------------------------------------------------------- /code/Arsenal/bot_blhx_build_pool/data_handler.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : data_handler.py 4 | @Time : 2022/02/11 11:09:18 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import copy 13 | from collections import Counter 14 | 15 | 16 | class Blhx_Magic_Data: 17 | """用户舰娘图鉴管理""" 18 | def __init__(self, blhx_tool): 19 | self.blhx_tool = blhx_tool 20 | 21 | def r_blhx_data(self,extra_id=None): 22 | """ 23 | 读取并返回用户舰娘图鉴数据 24 | :params extra_id: 额外指定的用户id 25 | :return 26 | """ 27 | if extra_id: 28 | user_id = str(extra_id) 29 | else: 30 | user_id = str(self.blhx_tool.mybot_data["sender"]["user_id"]) 31 | 32 | # 查询用户舰娘数据 33 | self.user_data_path = os.path.join(self.blhx_tool.user_ship_data_dir, user_id) 34 | with open(self.user_data_path,"a+",encoding="utf8") as f: 35 | f.seek(0) 36 | read_ship_list = f.readlines() 37 | for i in range(len(read_ship_list)): 38 | read_ship_list[i] = read_ship_list[i].replace("\n","") 39 | return read_ship_list 40 | 41 | def w_blhx_data(self,datas): 42 | # data -> [{"shipName":"可畏","isNew":True}...] 43 | # r_blhx_data已处理user_data_path 44 | with open(self.user_data_path,"a+",encoding="utf8") as f: 45 | for d in datas: 46 | if d["isNew"] == True: 47 | f.write("{}\n".format(d["shipName"])) 48 | 49 | def check_ship(self,ship_list,ship_data): 50 | """ 51 | 检查舰娘是否在 52 | :params ship_list: 抽取的十位舰娘 53 | ['科隆', '榊', '狐提', '太原', '小天鹅', '科尔克', '布什', '狻', '倔强', '布什'] 54 | :params ship_data: 抽取舰娘的数据 55 | :return: 添加isNew字段后的ship_data 56 | """ 57 | raw_data_after = dict(Counter(ship_list)) 58 | # 重复元素-列表 59 | duplicate_list = [k for k,v in raw_data_after.items() if v >1] 60 | # 重复元素:重复次数-字典 61 | duplicate_item_dict = {k:v for k,v in raw_data_after.items() if v >1} 62 | user_magic_data = self.r_blhx_data() 63 | 64 | new_data = [] 65 | for _ in ship_data: 66 | d = copy.deepcopy(_) 67 | # 用户已拥有该舰娘 68 | if d["shipName"] in user_magic_data: 69 | d["isNew"] = False 70 | # 处理多个相同舰娘,new标识展示问题 71 | elif d["shipName"] in duplicate_list: 72 | if duplicate_item_dict[d["shipName"]] < ship_list.count(d["shipName"]): 73 | d["isNew"] = False 74 | else: 75 | duplicate_item_dict[d["shipName"]] = duplicate_item_dict[d["shipName"]] - 1 76 | d["isNew"] = True 77 | # 不重复也不在当前用户数据中 78 | else: 79 | d["isNew"] = True 80 | new_data.append(d) 81 | 82 | # 舰娘名称写入用户数据 83 | # self.w_blhx_data(new_data) 84 | return new_data -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_color_img -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/bot_color_img.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import random 4 | 5 | # from basic.plus_res_directory import pdr 6 | 7 | 8 | # bot_day_illust每日插件会引用到该函数,需要对每日插件进行修改 9 | def get_img(word,limit=None,illust_level=None,num=None,retry_num=3): 10 | """ 11 | 根据关键字返回1~3张图片 12 | 13 | limit: 最低收藏数 14 | """ 15 | # api_url = "http://127.0.0.1:1526/api/v2/random" 16 | api_url = "http://106.55.244.64:1526/api/v2/random" 17 | 18 | if not num: 19 | num = random.choice(list(range(1,4))) 20 | 21 | data = { 22 | "num":num, 23 | # "num":random.choice(list(range(1,6))), 24 | "extra":word, 25 | "limit":limit, 26 | "illust_level":illust_level, 27 | } 28 | print(data) 29 | try: 30 | resp = requests.post(api_url,data=data,timeout=10) 31 | except Exception as e: 32 | if retry_num > 0: 33 | return get_img(word=word,limit=limit,illust_level=illust_level,num=num,retry_num=retry_num-1) 34 | else: 35 | return None 36 | else: 37 | # print(resp.text) 38 | result = json.loads(resp.text) 39 | return result 40 | 41 | 42 | # from bot_color_img import Bot_Color_Img 43 | # Bot_Color_Img.parse_extra(msg) 44 | class Color_Img: 45 | """ 46 | 整合涩图插件,提供4种涩图输出模式 47 | mode:从支持的四种模式中选择一种并返回(反代直链) 48 | extra_params 额外支持的键: 49 | illust_level -- 指定稀有度,R/SR/SSR/UR其中一种 50 | limit -- 最低收藏数 51 | 52 | """ 53 | def __init__(self): 54 | # self.plugin_name = type(self).__name__ 55 | # self.workspace = pdr.get_plus_res(self.plugin_name) 56 | # keyword[:2] keyword[-2:] 57 | self.keyword = "来点涩图" 58 | self.start_keyword = self.keyword[:2] 59 | self.end_keyword = self.keyword[-2:] 60 | 61 | self.basic_text = "" 62 | self.temp_eval = "self.{}('{}')" 63 | self.mode_info = { 64 | "r":"get_img", 65 | "gs":"gaussianblur_img", 66 | "qr":"qr_img", 67 | "p":"phantom_img" 68 | } 69 | self.help_info = """当前支持模式:\n""" +\ 70 | """ 1.r -- 反代直链(默认)\n 2.gs -- 高斯模糊\n 3.qr -- 二维码\n 4.p -- 幻影坦克\n形如:来点涩图 -qr\n\n""" +\ 71 | """当前支持额外的键值\n1.illust_level\n 指定返回作品的稀有度,值可选R/SR/SSR/UR其中一种\n2.limit\n 指定返回作品的最低收藏数""" +\ 72 | """\n\n形如:来点涩图 -gs -limit=3000""" 73 | # eval(self.temp_eval.format("parse_extra",self.keyword)) 74 | 75 | def parse_extra(self,msg): 76 | """ 77 | 从msg中解析出参数键值 78 | """ 79 | parse_result = {} 80 | # 指定tag 81 | if msg[:4] == self.keyword: 82 | parse_result["word"] = "" 83 | else: 84 | parse_result["word"] = msg.split(self.start_keyword)[-1].split(self.end_keyword)[0] 85 | 86 | # 获取并整合键值对 87 | for k_v in msg.split(" ")[1:]: 88 | split_list = k_v.split("=") 89 | k = split_list[0].replace("-","") 90 | if len(split_list) == 1: 91 | v = "" 92 | else: 93 | v = split_list[1] 94 | parse_result[k] = v 95 | 96 | # {'word': '原创', 'help': '', 'qr': '', 'illust_level': 'SSR', 'limit': '1000'} 97 | 98 | # 检查是否有help 99 | if "help" in list(parse_result.keys()): 100 | return {"help":self.help_info} 101 | 102 | # 检查mode 103 | temp_list = [] 104 | # 倒序遍历删除 & 添加 105 | for k in list(parse_result.keys())[::-1]: 106 | if k in list(self.mode_info.keys()): 107 | temp_list.append(k) 108 | del parse_result[k] 109 | continue 110 | 111 | if not temp_list: 112 | mode = "r" 113 | else: 114 | # 再次翻转,顺序恢复 115 | mode = temp_list[::-1][0] 116 | 117 | # 检查extra 118 | result = self.get_img(parse_result) 119 | print(parse_result,temp_list,mode) 120 | return { 121 | "parse_result":parse_result, 122 | "temp_list":temp_list, 123 | "mode":mode 124 | } 125 | 126 | def service_func(self,eval_cqp_data): 127 | """ 128 | """ 129 | # 判断群 130 | pass 131 | 132 | # def get_img(self,word,limit=None,illust_level=None,num=None,retry_num=3): 133 | def get_img(self,parse_result:dict,retry_num:int=3)->dict: 134 | """ 135 | 根据parse_result向PixiC API random接口发起请求 136 | 137 | limit: 最低收藏数 138 | """ 139 | # api_url = "http://127.0.0.1:1526/api/v2/random" 140 | api_url = "http://112.74.90.108:1526/api/v2/random" 141 | 142 | if not num: 143 | num = random.choice(list(range(1,4))) 144 | 145 | data = { 146 | "num":num, 147 | # "num":random.choice(list(range(1,6))), 148 | "extra":word, 149 | "limit":limit, 150 | "illust_level":illust_level, 151 | } 152 | print(data) 153 | try: 154 | resp = requests.post(api_url,data=data,timeout=10) 155 | except Exception as e: 156 | if retry_num > 0: 157 | # return get_img(word=word,limit=limit,illust_level=illust_level,num=num,retry_num=retry_num-1) 158 | return get_img(word=word,limit=limit,illust_level=illust_level,num=num,retry_num=retry_num-1) 159 | else: 160 | return None 161 | else: 162 | # print(resp.text) 163 | try: 164 | result = json.loads(resp.text) 165 | except Exception as e: 166 | return None 167 | else: 168 | return result 169 | 170 | # TODO 2020年10月3日17:22:20 171 | # 更新返回模板数据,有tag情况下返回当前tag数据库存量 172 | def parse_img_data(eval_cqp_data): 173 | msg = eval_cqp_data["message"] 174 | word = msg[2:-2] 175 | result = get_img(word) 176 | # print("result",result) 177 | reply_group = { 178 | "group_id": eval_cqp_data['group_id'], 179 | # "message":"[CQ:at,qq={}]\n".format(eval_cqp_data["user_id"]) + msg 180 | } 181 | # 返回None 182 | if not result: 183 | msg = "网络爆炸惹~请重试" 184 | reply_group["message"] = "[CQ:at,qq={}]\n".format(eval_cqp_data["user_id"]) + msg 185 | return reply_group 186 | 187 | # 无结果返回 188 | if type(result["result"]) != type([]): 189 | msg = "{}\ntag:{} 暂无数据哦~".format(result["result"]["message"],word) 190 | else: 191 | msg = "\n".join([i["reverse_url"] for i in result["result"]]) 192 | if word: 193 | msg += "\ntag:{} 共有{}张".format(word,result["count"]) 194 | # print(msg) 195 | 196 | reply_group["message"] = "[CQ:at,qq={}]\n".format(eval_cqp_data["user_id"]) + msg 197 | print("color_img",reply_group) 198 | return reply_group 199 | 200 | 201 | Bot_Color_Img = Color_Img() 202 | """ 203 | from bot_color_img import Bot_Color_Img 204 | msg = "来点原创涩图 -qr -gs -Gs -gS -illust_level=SSR -limit=1000" 205 | Bot_Color_Img.parse_extra(msg) 206 | """ -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/bot_gaussianblur_img/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_gaussianblur_img -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/bot_gaussianblur_img/bot_gaussianblur_img.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from PIL import Image, ImageFilter 4 | 5 | import requests 6 | import random 7 | 8 | class MyGaussianBlur(ImageFilter.Filter): 9 | # 用于给图片自定义高斯模糊滤镜 10 | name = "GaussianBlur" 11 | 12 | def __init__(self, radius=2, bounds=None): 13 | # 模糊半径默认为2 14 | self.radius = radius 15 | self.bounds = bounds 16 | 17 | def filter(self, image): 18 | """ 19 | 用于给图片添加高斯滤镜 20 | 根据bounds可以只给指定区域添加 21 | 22 | :param image: Image对象 23 | :return: 添加高斯滤镜后的Image对象 24 | """ 25 | if self.bounds: 26 | clips = image.crop(self.bounds).gaussian_blur(self.radius) 27 | image.paste(clips, self.bounds) 28 | return image 29 | else: 30 | return image.gaussian_blur(self.radius) 31 | 32 | 33 | class GasussianBlur_Img: 34 | """主类 高斯模糊""" 35 | 36 | def __init__(self): 37 | # 默认存储位置 38 | # self.path = r"C:\Users\Administrator\Desktop\CQA-tuling\python插件\coolq-trace_anime-master\res\blur" 39 | self.path = "" 40 | if not self.path:self.path = os.getcwd() 41 | self.default_radius = 10 42 | 43 | def ready_img(self,img_path=None,img_url=None,radius=None)->str: 44 | """ 45 | 在filter进行前的一些准备工作,比如下载/打开图片以获得Image对象 46 | 47 | :param img_path: 本地图片路径(两者都存在,则该项优先) 48 | C:\\Users\\Administrator\\Desktop\\HoqgblbnFgekkdf.jpg 49 | 50 | :param img_url: 网络图片路径 51 | https://i.pximg.net/img-master/img/2019/11/12/03/26/16/77775892_master1200.jpg 52 | 53 | :return: new_img_path 添加高斯模糊后的本地图片路径 54 | """ 55 | # 两个参数都不存在 56 | if not img_path and not img_url: 57 | return "" 58 | 59 | if not radius: 60 | radius = self.default_radius 61 | 62 | # 本地图片路径优先 63 | if img_path: 64 | filename_list = img_path.rsplit(".") 65 | new_img_path = "_blur.".join(filename_list) 66 | image = Image.open(img_path) 67 | image = image.filter(MyGaussianBlur(radius=radius)) 68 | image.save(new_img_path) 69 | return new_img_path 70 | 71 | # 网络图片,主要用于pixiv的图片 72 | if img_url: 73 | if img_url[-3:] in ["jpg","png"] and img_url[-4] == '.': 74 | filename = img_url.rsplit("/",1)[-1] 75 | # ['77775892_master1200', 'jpg'] 76 | filename_list = filename.rsplit(".") 77 | new_filename = "_blur.".join(filename_list) 78 | 79 | old_img_path = os.path.join(self.path,filename) 80 | new_img_path = os.path.join(self.path,new_filename) 81 | 82 | # 网络请求后面需统一使用basic内的模块 83 | headers = { 84 | "Host": "www.pixiv.net", 85 | "referer": "https://www.pixiv.net/", 86 | "origin": "https://accounts.pixiv.net", 87 | "accept-language": "zh-CN,zh;q=0.9", # 返回translation,中文翻译的标签组 88 | "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 89 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', 90 | } 91 | try: 92 | resp_content = requests.get(img_url,headers=headers,timeout=10).content 93 | except Exception as e: 94 | resp_content = "" 95 | 96 | if resp_content: 97 | with open(old_img_path,"wb") as f: 98 | f.write(resp_content) 99 | 100 | image = Image.open(old_img_path) 101 | image = image.filter(MyGaussianBlur(radius=radius)) 102 | image.save(new_img_path) 103 | os.remove(old_img_path) 104 | return new_img_path 105 | # 没有获取到content 106 | else: 107 | return "" 108 | 109 | def parse(self,eval_cqp_data:dict)->dict: 110 | pass 111 | 112 | # GBImg = GasussianBlur_Img() 113 | Bot_GasussianBlur_Img = GasussianBlur_Img() 114 | # g = GasussianBlur_Img() 115 | # img_url = "https://i.pximg.net/img-master/img/2019/11/12/03/26/16/77775892_master1200.jpg" 116 | # new_path = g.ready_img(img_url=img_url) 117 | # if new_path: 118 | # print(new_path) 119 | # else: 120 | # print("下载出错") 121 | # ===================== 122 | 123 | 124 | # simg = '1.jpg' 125 | # dimg = 'res_1.jpg' 126 | # image = Image.open(simg) 127 | # image = image.filter(MyGaussianBlur(radius=10)) 128 | # image.save(dimg) -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/bot_phantom_img/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_phantom_img -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/bot_phantom_img/bot_phantom_img.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from PIL import Image,ImageDraw,ImageFont 4 | 5 | from Arsenal.basic.plugin_res_directory import pdr 6 | 7 | # 白色背景——ImageA——opacity=100% 8 | # 黑色背景——ImageB——opacity=0% 9 | # A图亮度大于B图亮度,提高A图/降低B图 10 | class Phantom_Img: 11 | """ 12 | 生成一张表图+指定里图=幻影坦克 13 | """ 14 | def __init__(self): 15 | self.plugin_name = type(self).__name__ 16 | self.workspace = pdr.get_plus_res(self.plugin_name) 17 | self.ttf_path = os.path.join(self.workspace,"..","dy.ttf") 18 | # 表图text 19 | self.text = "点我看涩图" 20 | 21 | def get_font_size(self,img_size): 22 | """ 23 | 根据图片宽度与字体,得出合适的fontsize 24 | :params img_size: 图片大小,tuple 25 | :return: fontsize 26 | """ 27 | # starting font size 28 | fontsize = 1 29 | # portion of image width you want text width to be 30 | img_fraction = 0.50 31 | 32 | font = ImageFont.truetype(self.ttf_path, fontsize) 33 | while font.getsize(self.text)[0] < img_fraction * img_size[0]: 34 | # iterate until the text size is just larger than the criteria 35 | fontsize += 1 36 | font = ImageFont.truetype(self.ttf_path, fontsize) 37 | 38 | # optionally de-increment to be sure it is less than criteria 39 | fontsize -= 1 40 | return fontsize 41 | 42 | def main(self,img1_path): 43 | # 里图 44 | img1 = Image.open(img1_path) 45 | img1_size = img1.size 46 | 47 | # 表图: 根据里图 新建一个画布 居中写入文字 48 | img2 = Image.new('RGB',img1.size) 49 | draw = ImageDraw.Draw(img2) 50 | # 调整表图字体 51 | fontsize = self.get_font_size(img1_size) 52 | font = ImageFont.truetype(self.ttf_path, fontsize) 53 | text_size = font.getsize(self.text) 54 | text_coordinate = int((img1_size[0]-text_size[0])/2), int((img1_size[1]-text_size[1])/2) 55 | draw.text(text_coordinate, self.text, (242,237,238), font) 56 | # 转为灰度图 mode 57 | img1_g = img1.convert("L") 58 | img2_g = img2.convert("L") 59 | # 调整大小 60 | img2_r = img2_g.resize((int(img1_g.width), int(img1_g.height))) 61 | img1_g = img1_g.resize((int(img1_g.width), int(img1_g.height))) 62 | # 调整亮度 63 | img1 = self.light_degree(img1_g, 0) 64 | img2 = self.light_degree(img2_r, 1) 65 | 66 | line = self.opposed_line(img2, img1) 67 | divided = self.divide(img1, line) 68 | 69 | final_path = self.final(divided, line) 70 | return final_path 71 | 72 | def light_degree(self,img,i): 73 | """ 74 | 调整图片整体亮度 75 | :params img: img对象 76 | :params i: 判定亮度增加或减少 77 | :return: 调整亮度后的img 78 | """ 79 | if i > 0: 80 | img = img.point(lambda i: i * 1.1) 81 | else: 82 | img = img.point(lambda i: i * 0.3) 83 | return img 84 | 85 | def opposed_line(self, img2, img1): 86 | """ 87 | 同时进行反相与线性减淡操作 88 | :params img1: 里图 89 | :params img2: 表图 90 | """ 91 | imgb = img2.load() 92 | imga = img1.load() 93 | for x in range(0, img2.width, 1): 94 | for y in range(0, img2.height, 1): 95 | # 逐点读取像素值 96 | b = imgb[x, y] 97 | a = imga[x, y] 98 | # (255-)b表示该像素点反相,(255-b)+a即为线性减淡运算 99 | color = (255-b+a,) 100 | # 避免线性减淡过程中有灰度值超过255的情况 101 | if color > (220,): 102 | imgb[x, y] = (160 - b + a,) 103 | else: 104 | imgb[x, y] = color 105 | return img2 106 | 107 | def divide(self, img1, imgO): 108 | # 划分操作,得到差异显著的里图 109 | imga = img1.load() 110 | imgo = imgO.load() 111 | for x in range(0, img1.width, 1): 112 | for y in range(0, img1.height, 1): 113 | A = imga[x, y] 114 | O = imgo[x, y] 115 | if O == 0: 116 | color = (int(A*0.3),) 117 | elif A/O >= 1: 118 | color = (int(A*6.2),) 119 | else: 120 | color = (int(255*A/O),) 121 | imga[x, y] = color 122 | return img1 123 | 124 | def final(self, divided, line): 125 | """ 126 | 将反相与线性减淡过的表图的灰度值,添加到经过划分操作的里图A通道中 127 | """ 128 | LINE = line.load() 129 | divided_RGBA = divided.convert("RGBA") 130 | DIV_RGBA = divided_RGBA.load() 131 | line = line.convert("RGBA") 132 | for x in range(0, line.width, 1): 133 | for y in range(0, line.height, 1): 134 | DIV_RGBA[x, y] = (DIV_RGBA[x, y][0], DIV_RGBA[x, y][1], 135 | DIV_RGBA[x, y][2], LINE[x, y]) 136 | # divided_RGBA.show() 137 | filename = str(int(time.time())) 138 | final_path = os.path.join(self.workspace,"{}.png".format(filename)) 139 | divided_RGBA.save(final_path) 140 | return final_path 141 | 142 | Bot_Phantom_Img = Phantom_Img() 143 | 144 | if __name__ == "__main__": 145 | # from bot_phantom_img import Bot_Phantom_Img 146 | # img1_path = r'D:\Code\mybot\code\res\Phantom_Img\85071750_p0.jpg' 147 | # img1_path = r'D:\Code\mybot\code\res\Phantom_Img\82028936_p0.jpg' 148 | img1_path = r'C:\Users\lenovo\Desktop\校验集-收藏\76530353_p0.jpg' 149 | Bot_Phantom_Img = Phantom_Img() 150 | Bot_Phantom_Img.main(img1_path) 151 | 152 | 153 | """ 154 | from Arsenal.bot_phantom_img import Bot_Phantom_Img 155 | img1_path = r"xxx.png" 156 | Bot_Phantom_Img.main(img1_path) 157 | """ -------------------------------------------------------------------------------- /code/Arsenal/bot_color_img/color_img.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import random 4 | 5 | def get_img(word,limit=None,illust_level=None,num=None,retry_num=3): 6 | """ 7 | 根据关键字返回1~3张图片 8 | 9 | limit: 最低收藏数 10 | """ 11 | api_url = "http://127.0.0.1:1526/api/v2/random" 12 | 13 | if not num: 14 | num = random.choice(list(range(1,4))) 15 | 16 | data = { 17 | "num":num, 18 | # "num":random.choice(list(range(1,6))), 19 | "extra":word, 20 | "limit":limit, 21 | "illust_level":illust_level, 22 | } 23 | print(data) 24 | try: 25 | resp = requests.post(api_url,data=data,timeout=10) 26 | except Exception as e: 27 | if retry_num > 0: 28 | return get_img(word=word,limit=limit,illust_level=illust_level,num=num,retry_num=retry_num-1) 29 | else: 30 | return None 31 | else: 32 | # print(resp.text) 33 | result = json.loads(resp.text) 34 | return result 35 | 36 | # TODO 2020年10月3日17:22:20 37 | # 更新返回模板数据,有tag情况下返回当前tag数据库存量 38 | def parse_img_data(eval_cqp_data): 39 | msg = eval_cqp_data["message"] 40 | word = msg[2:-2] 41 | 42 | result = get_img(word) 43 | # print("result",result) 44 | if type(result["result"]) != type([]): 45 | # 无结果返回 46 | msg = "{}\ntag:{} 暂无数据哦~".format(result["result"]["message"],word) 47 | else: 48 | msg = "\n".join([i["reverse_url"] for i in result["result"]]) 49 | # msg = ",".join(['[CQ:image,file={}]'.format(i["reverse_url"]) for i in result["result"]]) 50 | if word: 51 | msg += "\ntag:{} 共有{}张".format(word,result["count"]) 52 | # print(msg) 53 | 54 | reply_group = { 55 | "group_id": eval_cqp_data['group_id'], 56 | "message":"[CQ:at,qq={}]\n".format(eval_cqp_data["user_id"]) + msg 57 | } 58 | print("color_img",reply_group) 59 | return reply_group -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot_img_search import Bot_Img_Search -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/bot_img_search.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : bot_img_search.py 4 | @Time : 2022/02/20 02:10:30 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import time 12 | from Arsenal.basic.bot_tool import tool 13 | from Arsenal.basic.log_record import logger 14 | from Arsenal.basic.plugin_class import PluginClass 15 | from Arsenal.basic.plugin_res_directory import pdr 16 | from Arsenal.basic.datetime_tool import datetime_now, datetime_offset 17 | from Arsenal.basic.msg_temp import MYBOT_ERR_CODE, TASK_PROCESSOR_TEMP,\ 18 | PLUGIN_BLOCK, PLUGIN_IGNORE 19 | 20 | from Arsenal.bot_img_search.sites import SauceNao, Ascii2d, Doujin 21 | # from Arsenal.bot_img_search.sites import SauceNao, Ascii2d, Yandex 22 | from Arsenal.bot_img_search.img_search_tool import Img_Search_Tool,\ 23 | saucenao_info, ascii2d_info, yandex_info 24 | 25 | 26 | def callback(): 27 | pass 28 | 29 | 30 | class Img_Search(PluginClass): 31 | def __init__(self): 32 | super(PluginClass, self).__init__() 33 | self.plugin_name = type(self).__name__ 34 | self.plugin_nickname = "二次元图片搜索" 35 | self.plugin_type = 1 36 | self.plugin_level = 10 37 | self.resource_path = "" 38 | # 插件工作目录 39 | self.workspace = pdr.get_plus_res(self.plugin_name) 40 | 41 | ########################### 42 | # 配置类 43 | self.img_search_tool = Img_Search_Tool(self.resource_path, self.workspace) 44 | # 搜图引擎设置 45 | self.engines = { 46 | "SauceNao": SauceNao, 47 | "Ascii2d": Ascii2d, 48 | "Doujin": Doujin, # 同人本 49 | # "Yandex": Yandex, # TODO 50 | # "Iqdb": Iqdb # TODO 51 | } 52 | self.default_engineName = "SauceNao" 53 | self.default_engine = self.engines[self.default_engineName] 54 | 55 | # 搜图队列 56 | # tool.pool.put(self.cycle_check_SearchQueue, (), callback) 57 | 58 | def help_info(self): 59 | message = self.blhx_tool.text_temp["help"].format() 60 | return message 61 | 62 | def switch_img_engine(self, engineName): 63 | """切换为对应的搜图引擎""" 64 | return self.engines.get(engineName, self.default_engineName) 65 | 66 | @logger.catch 67 | def cycle_check_SearchQueue(self): 68 | # def check(): 69 | while True: 70 | if tool.pool.terminal: 71 | logger.info(TASK_PROCESSOR_TEMP["TASK_BREAK_TERMINAL"]) 72 | break 73 | 74 | logger.debug("cycle_check_SearchQueue check start") 75 | 76 | for i in self.img_search_tool.search_queue: 77 | # 到期时间 78 | expire_time = datetime_offset(i["last"], self.img_search_tool.limit_seconds) 79 | # 上一次调用时间与到期时间对比 80 | if datetime_now() > expire_time: 81 | # logger.info(f'{future_time} {i["last"]}') 82 | self.img_search_tool.search_queue_delete(i) 83 | logger.info(f"[DELETE] 从搜图队列中删除: {i}") 84 | 85 | logger.debug(f"search_queue <{len(self.img_search_tool.search_queue)}> {self.img_search_tool.search_queue}") 86 | logger.debug(f"cycle_check_SearchQueue check success - sleep:{self.img_search_tool.limit_seconds}") 87 | time.sleep(self.img_search_tool.limit_seconds) 88 | 89 | @logger.catch 90 | def parse(self, mybot_data: dict) -> dict: 91 | self.mybot_data = mybot_data 92 | if self.mybot_data["sender"].get('type','') == 'group': 93 | group_id = self.mybot_data["sender"]["group_id"] 94 | user_id = self.mybot_data["sender"]["user_id"] 95 | message = self.mybot_data["arrange"]['message'] 96 | split_words = message.split(" ") 97 | 98 | data = { 99 | "user_id": user_id, 100 | "group_id": group_id, 101 | "engineName": self.default_engineName, 102 | "alway": False, 103 | } 104 | 105 | # enable 106 | 107 | # disable 108 | 109 | # img 110 | 111 | # others 112 | 113 | if split_words[0] not in self.img_search_tool.text_temp["keywords"] or \ 114 | data not in self.img_search_tool.search_queue: 115 | return PLUGIN_IGNORE 116 | 117 | self.img_search_tool.search_queue_check(data) 118 | logger.debug(self.img_search_tool.search_queue) 119 | return PLUGIN_BLOCK 120 | 121 | # 默认跳过 122 | return PLUGIN_IGNORE 123 | 124 | Bot_Img_Search = Img_Search() -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/img_search_tool.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : img_search_tool.py 4 | @Time : 2022/02/20 02:04:21 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | # from Arsenal.basic.plugin_res_directory import pdr 12 | from Arsenal.basic.bot_tool import tool 13 | from Arsenal.basic.log_record import logger 14 | from Arsenal.basic.datetime_tool import datetime_now 15 | 16 | 17 | # saucenao 索引数据库id 18 | DB_id = { 19 | "all": 999, 20 | 21 | "pixiv": 5, 22 | "danbooru": 9, 23 | "yande": 12, 24 | "gelbooru": 25, 25 | "konachan": 26, 26 | 27 | "doujin": 38, 28 | "mangadex": 37, 29 | 30 | "niconico": 8, 31 | "anime": 21, 32 | } 33 | 34 | 35 | # 各个搜图引擎的一些常量配置 36 | def saucenao_info_func(): 37 | """saucenao info""" 38 | return { 39 | "short_limit_count": 3, # 30秒内剩余次数 触发提醒次数 40 | "short_limit_min": 0, 41 | "long_limit_count": 30, # 24h内剩余次数 触发提醒次数 42 | "long_limit_min": 0, 43 | } 44 | 45 | def ascii2d_info_func(): 46 | """ascii2d info""" 47 | return { 48 | 49 | } 50 | 51 | def yandex_info_func(): 52 | """yandex info""" 53 | return { 54 | 55 | } 56 | saucenao_info = saucenao_info_func() 57 | ascii2d_info = ascii2d_info_func() 58 | yandex_info = yandex_info_func() 59 | 60 | 61 | class Img_Search_Tool: 62 | def __init__(self, resource_path, workspace): 63 | self.resource_path = resource_path 64 | self.workspace = workspace 65 | self.tool = tool 66 | 67 | # self.default_engine = "saucenao" 68 | # self.engines = { 69 | # "saucenao": "search_by_saucenao", 70 | # "ascii2d": "search_by_ascii2d", 71 | # "yandex": "search_by_yandex" 72 | # } 73 | 74 | # 搜图插件队列 75 | self.search_queue = [] 76 | # 搜图 最大消息等待时间 77 | self.limit_seconds = tool.config["Plugin"]["bot_img_search"]["timeout"] 78 | 79 | # 初始化模板 80 | self.msg_temp() 81 | self.error_temp() 82 | 83 | def saucenao_api_key(self): 84 | return self.tool.config["Plugin"]["saucenao"]["api_key"] 85 | 86 | # ======== search_queue ======= 87 | 88 | def search_queue_add(self, data): 89 | """搜图队列添加记录""" 90 | now_time = datetime_now() 91 | 92 | add_data = { 93 | "user_id": data["user_id"], 94 | "group_id": data["group_id"], 95 | "engineName": data["engineName"], 96 | "alway": data["alway"], 97 | "last": now_time 98 | } 99 | self.search_queue.append(add_data) 100 | logger.info(f"[ADD] 从搜图队列中增加: {add_data}") 101 | 102 | def search_queue_check(self, data): 103 | # 存在队列中 104 | # for i in self.search_queue[::-1]: 105 | for k,v in enumerate(self.search_queue[::]): 106 | if v["user_id"] == data["user_id"] and \ 107 | v["group_id"] == data["group_id"]: 108 | now_time = datetime_now() 109 | self.search_queue[k]["last"] = now_time 110 | logger.info(f"[UPDATE] 从搜图队列中更新: {v}") 111 | # v["last"] = now_time 112 | # future_time = datetime_offset(now_time, self.limit_seconds) 113 | # i["expire"] = future_time 114 | break 115 | # 加入对象 116 | else: 117 | self.search_queue_add(data) 118 | 119 | def search_queue_delete(self, data): 120 | for i in self.search_queue[::]: 121 | if i["user_id"] == data["user_id"] and \ 122 | i["group_id"] == data["group_id"]: 123 | self.search_queue.remove(i) 124 | # break 125 | 126 | # ======== search_queue ======= 127 | 128 | 129 | def msg_temp(self): 130 | """general msg""" 131 | self.text_temp = { 132 | "help": """""", 133 | "alway_search_info": """已进入连续搜图模式, 请发送一张或多张图片进行搜图,"""\ 134 | """发送'退出搜图'以退出搜图队列.\n提示: 请注意发送图片的频次,以免给他人造成困扰""", 135 | 136 | "keywords": ["搜图", "连续搜图", "搜本", "退出搜图"], 137 | "enable_keyword_doujin": ["搜本"], 138 | "enable_keyword_img": ["搜图", "连续搜图"], 139 | "disable_keyword_img": ["退出搜图"], 140 | } 141 | 142 | 143 | def error_temp(self): 144 | """err msg""" 145 | self.err_temp = { 146 | 147 | } 148 | -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/sites/__init__.py: -------------------------------------------------------------------------------- 1 | from .saucenao import SauceNao 2 | from .ascii2d import Ascii2d 3 | from .doujin import Doujin 4 | # from .yandex import Yandex -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/sites/ascii2d.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : ascii2d.py 4 | @Time : 2022/03/31 22:47:24 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import re 13 | from lxml import etree 14 | from requests_toolbelt import MultipartEncoder 15 | 16 | from Arsenal.basic.log_record import logger 17 | from Arsenal.basic.BNConnect import scraperRequest, general_headers 18 | from Arsenal.basic.msg_temp import IMG_SEARCH_TEMP_GENG 19 | 20 | from Arsenal.bot_img_search.utils import Ascii2dResp 21 | 22 | 23 | class Ascii2d: 24 | def __init__(self, 25 | bovw: bool = True, 26 | nums: int = 5, 27 | )->None: 28 | self.host = "https://ascii2d.net" 29 | self.color = "https://ascii2d.net/search/color/" 30 | self.bovw = "https://ascii2d.net/search/bovw/" 31 | self.search_url = "https://ascii2d.net/search/url/{}" 32 | self.search_file = "https://ascii2d.net/search/file" 33 | 34 | self.bovw = bovw 35 | self.nums = nums 36 | self.color_url = "" 37 | self.bovw_url = "" 38 | 39 | self.limit_byte = 5 * 1024 * 1024 40 | 41 | @staticmethod 42 | def _err_code(code:int): 43 | code_info = { 44 | "429": "Too many request - 太多请求,请稍后再试", 45 | "403": "Forbidden,or token unvalid - 服务器拒绝或api_key未设置", 46 | "413": "Request Entity Too Large - 图片大小过大", 47 | "430": "禁止访问,请重试.", 48 | "500": "服务端(ascii2d.net)错误", 49 | "520": "空白、未知、意外响应或图片大小过大", 50 | 51 | "998": "ascii2d访问超时, 请稍后再试", 52 | "999": "未知错误,请联系管理员", 53 | } 54 | if str(code) in code_info: 55 | return code_info[str(code)] 56 | else: 57 | return "Unknown err code" 58 | 59 | def search(self, 60 | url:str = None, 61 | file:str = None)->Ascii2dResp or str: 62 | """ 63 | 通过Ascii2D进行反向图像搜索 64 | 65 | Params 66 | ----------- 67 | :param url: network img url 68 | :param file: local img url 69 | 70 | Return Ascii2dResp or str 71 | attributes 72 | ----------- 73 | + .results = Parsed data 74 | + .results_bovw = bovw Parsed data 75 | 若未指定特征检索, 则results_bovw为[]. 76 | 否则则results_bovw为数据结构与results一致. 77 | + .results[0] = First index of Parsed data 78 | + .results[0].info = 图片信息 79 | + .results[0].pic_link = 图片链接 80 | + .results[0].pic_name = 图片名称 81 | + .results[0].author_link = 作者链接 82 | + .results[0].author = 作者名称 83 | + .results[0].thumb = 缩略图 84 | + .results[0].type = 类型 85 | 86 | str - err message to client 87 | """ 88 | logger.debug(IMG_SEARCH_TEMP_GENG["param_err"].format( 89 | f":{url} - :{file}" 90 | )) 91 | 92 | ascii2d_resp = self.exec(url, file) 93 | 94 | if not ascii2d_resp: 95 | return self._err_code(998) 96 | 97 | # err_code text 98 | if isinstance(ascii2d_resp, str): 99 | return ascii2d_resp 100 | 101 | 102 | ascii2d_resp.results_bovw = [] 103 | if self.bovw: 104 | ascii2d_bovw_resp = self.exec_bovw() 105 | ascii2d_resp.results_bovw = ascii2d_bovw_resp.results 106 | 107 | return ascii2d_resp 108 | 109 | def exec(self, 110 | url:str = None, 111 | file:str = None)->Ascii2dResp or str: 112 | logger.info(f"Ascii2d Color Mode Searching") 113 | # network url 114 | if url: 115 | _url = self.search_url.format(url) 116 | resp = scraperRequest(options={"url": _url}) 117 | # local file 118 | elif file: 119 | if os.path.getsize(file) >= self.limit_byte: 120 | logger.warning(f"{self._err_code(413)} - {file}") 121 | return self._err_code(413) 122 | 123 | m = MultipartEncoder( 124 | fields={"file": ("filename",open(file, "rb"),"type=multipart/form-data",)} 125 | ) 126 | headers = general_headers.copy() 127 | headers["Content-Type"] = m.content_type 128 | 129 | resp = scraperRequest( 130 | options={"url": self.search_file, "headers":headers}, 131 | method="POST", 132 | data=m 133 | ) 134 | else: 135 | return self._err_code(999) 136 | 137 | 138 | if not resp: 139 | logger.warning(f"No Resp - {resp}") 140 | return self._err_code(999) 141 | elif resp.status_code != 200: 142 | logger.error(f"{self._err_code(resp.status_code)}") 143 | logger.error(f"{resp.text[:300]}") 144 | return self._err_code(resp.status_code) 145 | else: 146 | self.color_url = resp.url 147 | self.bovw_url = self.color_url.replace("color", "bovw") 148 | obj = etree.HTML(resp.text).xpath("""//div[@class="row item-box"]""")[:self.nums] 149 | ascii2d_resp = Ascii2dResp(obj) 150 | ascii2d_resp = self.deal_with_resp(ascii2d_resp) 151 | return ascii2d_resp 152 | 153 | def exec_bovw(self)->Ascii2dResp or str: 154 | logger.info(f"Ascii2d Bovw Mode Searching") 155 | resp = scraperRequest(options={"url": self.bovw_url}) 156 | # resp = scraper.get(self.bovw_url, headers=general_headers, timeout=10) 157 | 158 | if not resp: 159 | return self._err_code(999) 160 | elif resp.status_code != 200: 161 | logger.error(f"{self._err_code(resp.status_code)}") 162 | logger.error(f"{resp.text[:300]}") 163 | return self._err_code(resp.status_code) 164 | else: 165 | obj = etree.HTML(resp.text).xpath("""//div[@class="row item-box"]""")[:self.nums] 166 | ascii2d_resp = Ascii2dResp(obj) 167 | ascii2d_resp = self.deal_with_resp(ascii2d_resp) 168 | return ascii2d_resp 169 | 170 | def deal_with_resp(self, ascii2d_resp:Ascii2dResp)->Ascii2dResp or str: 171 | logger.debug(f"Length-ascii2d_resp.results - {len(ascii2d_resp.results)}") 172 | [logger.debug(i.__dict__) for i in ascii2d_resp.results] 173 | 174 | # 去除空数据的box4 175 | new_results = [r for r in ascii2d_resp.results \ 176 | if not (r.pic_link == "unknown" and r.pic_name == "unknown" and \ 177 | r.author_link == "unknown" and r.author == "unknown" and r.type == "unknown")] 178 | 179 | # 更新筛选后的box 180 | if not new_results: 181 | ascii2d_resp.results = ascii2d_resp.results[1] 182 | else: 183 | ascii2d_resp.results = new_results 184 | 185 | # 提高相同分辨率,size更大的box的权重 186 | if len(ascii2d_resp.results) != 1: 187 | _info = ascii2d_resp.results[0].info 188 | _resolution = _info.split(" ")[0] 189 | _size = int(re.sub("[a-zA-Z]", "", _info.split(" ")[-1])) 190 | 191 | for i in range(len(ascii2d_resp.results)): 192 | i_list = ascii2d_resp.results[i].info.split(" ") 193 | if i_list[0] == _resolution and int(re.sub("[a-zA-Z]", "", i_list[-1])) > _size: 194 | ascii2d_resp.results[0], ascii2d_resp.results[i] = ascii2d_resp.results[i], ascii2d_resp.results[0] 195 | 196 | return ascii2d_resp -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/sites/doujin.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : doujin.py 4 | @Time : 2022/04/01 17:04:15 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : saucenao db-38 图片反向搜索 8 | ''' 9 | 10 | # here put the import lib 11 | from Arsenal.bot_img_search.sites import SauceNao 12 | 13 | 14 | class Doujin(SauceNao): 15 | def __init__(self, 16 | api_key: str = None, 17 | db: int = 38, 18 | output_type: int = 2, 19 | testmode: int = 0, 20 | numres: int = 5 21 | ) -> None: 22 | super().__init__(api_key, db, output_type, testmode, numres) -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/sites/saucenao.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : saucenao.py 4 | @Time : 2022/02/20 02:10:46 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | from requests_toolbelt import MultipartEncoder 13 | 14 | from Arsenal.basic.bot_tool import tool 15 | from Arsenal.basic.log_record import logger 16 | from Arsenal.basic.BNConnect import baseRequest 17 | from Arsenal.basic.msg_temp import IMG_SEARCH_TEMP_GENG 18 | 19 | from Arsenal.bot_img_search.utils import SauceNaoResp 20 | from Arsenal.bot_img_search.img_search_tool import DB_id 21 | 22 | 23 | class SauceNao: 24 | def __init__(self, 25 | api_key: str = None, 26 | db: int = 999, 27 | output_type: int = 2, 28 | testmode: int = 0, 29 | numres: int = 5, 30 | )->None: 31 | # saucenao doc: https://saucenao.com/user.php?page=search-api 32 | self.search_url = "https://saucenao.com/search.php" 33 | self.min_similarity = 60 34 | 35 | params = { 36 | "testmode": testmode, 37 | "numres": numres, 38 | "output_type": output_type, 39 | "db": db 40 | } 41 | self.api_key = tool.config["Plugin"]["saucenao"]["api_key"] if not api_key else api_key 42 | if self.api_key: 43 | params["api_key"] = self.api_key 44 | 45 | self.params = params 46 | 47 | self.limit_byte = 20 * 1024 * 1024 48 | 49 | @staticmethod 50 | def _err_code(code:int): 51 | code_info = { 52 | "429": "Too many request - 太多请求,请稍后再试", 53 | "403": "Forbidden,or token unvalid - 服务器拒绝或api_key未设置", 54 | "413": "Request Entity Too Large - 图片大小过大", 55 | "430": "Request Entity Too Large -- 图片大小过大", 56 | "500": "", 57 | "503": "Request Entity Too Large -- 图片大小过大", 58 | 59 | # 自定义 60 | "997": "短时(30秒)搜索次数为零,请等待30秒后重试或使用其他搜图引擎~", 61 | "998": "长时(24小时)搜索次数为零,请等待24小时后重试或使用其他搜图引擎~", 62 | "999": "未知错误,请联系管理员", 63 | } 64 | if str(code) in code_info: 65 | return code_info[str(code)] 66 | else: 67 | return "Unknown err code" 68 | 69 | def search(self, 70 | url:str = None, 71 | file:str = None)->SauceNaoResp or str: 72 | """ 73 | 通过SauceNao进行反向图像搜索 74 | 75 | Params 76 | ----------- 77 | :param url: network img url 78 | :param file: local img url 79 | 80 | Return SauceNaoResp or str 81 | attributes 82 | ----------- 83 | + .short_limit = 剩余搜索次数/30s 84 | + .long_limit = 剩余搜索次数/24h 85 | + .results = Parsed data 86 | + .results[0] = First index of Parsed data 87 | + .results[0].index_id = 数据库id 88 | + .results[0].similarity = 相似度 89 | + .results[0].thumbnail = 缩略图url 90 | + .results[0].title = 图片标题 / 漫画标题 91 | + .results[0].url = 图片链接 / 原始链接 92 | + .results[0].member_name = 作者名称 93 | + .results[0].pixiv_id = pixiv插画id 94 | + .results[0].member_id = pixiv作者uid 95 | + .results[0].doujinName = 同人本名称 96 | + .results[0].anilist_id = anilist番剧id 97 | 98 | str - err message to client 99 | """ 100 | params = self.params 101 | options = {"url": self.search_url} 102 | 103 | # network url 104 | if url: 105 | params["url"] = url 106 | logger.debug(f"SauceNao.params - {params}") 107 | resp = baseRequest(options=options, params=params) 108 | # local file 109 | elif file: 110 | if os.path.getsize(file) >= self.limit_byte: 111 | logger.warning(f"{self._err_code(413)} - {file}") 112 | return self._err_code(413) 113 | 114 | m = MultipartEncoder(fields={ 115 | "file": ("filename", open(file, "rb"), "type=multipart/form-data") 116 | } 117 | ) 118 | options["headers"] = {'Content-Type': m.content_type} 119 | logger.debug(f"SauceNao.params - {params}") 120 | resp = baseRequest(method="POST", options=options, data=m, params=params) 121 | else: 122 | logger.warning(IMG_SEARCH_TEMP_GENG["param_err"].format( 123 | f":{url} - :{file}" 124 | )) 125 | return self._err_code(999) 126 | 127 | 128 | if not resp: 129 | logger.warning(IMG_SEARCH_TEMP_GENG["param_err"].format( 130 | f":{url} - :{file}" 131 | )) 132 | return self._err_code(999) 133 | elif resp.status_code != 200: 134 | logger.error(f"{self._err_code(resp.status_code)}") 135 | logger.error(f"{resp.text[:300]}") 136 | return self._err_code(resp.status_code) 137 | else: 138 | saucenao_resp = SauceNaoResp(resp.json()) 139 | saucenao_resp = self.deal_with_resp(saucenao_resp) 140 | return saucenao_resp 141 | 142 | def deal_with_resp(self, saucenao_resp:SauceNaoResp)->SauceNaoResp or str: 143 | logger.debug(f"Length-saucenao_resp.results - {len(saucenao_resp.results)}") 144 | [logger.debug(i.__dict__) for i in saucenao_resp.results] 145 | 146 | # 短时次数 147 | if saucenao_resp.short_limit <= 0: 148 | return self._err_code(997) 149 | # 长时次数 150 | if saucenao_resp.long_limit <= 0: 151 | return self._err_code(998) 152 | 153 | # db:all - 第一个结果为非pixiv网站时,给予pixiv额外权重 154 | if self.params["db"] == DB_id["all"] and \ 155 | saucenao_resp.results[0].index_id != DB_id["pixiv"]: 156 | for _ in saucenao_resp.results: 157 | # pixiv 1.03x权重 158 | if _.index_id == DB_id["pixiv"]: 159 | _.similarity = float(_.similarity * 1.03) 160 | # doujin 0.97x权重 161 | if _.index_id == DB_id["doujin"]: 162 | _.similarity = float(_.similarity * 0.97) 163 | 164 | # db:doujin/mangadex - 搜本模式给予漫画类结果额外权重 165 | if self.params["db"] in [DB_id["doujin"], DB_id["mangadex"]]: 166 | for _ in saucenao_resp.results: 167 | if _.index_id in [DB_id["doujin"], DB_id["mangadex"]]: 168 | _.similarity = float(_.similarity * 1.03) 169 | 170 | # 尝试丢弃盗图者或已删除的记录 - 实验性功能 171 | first_result = saucenao_resp.results[0] 172 | if first_result.index_id == DB_id["pixiv"]: 173 | _pixivList = [_ for _ in saucenao_resp.results\ 174 | if _.index_id == DB_id["pixiv"] and _.similarity >= first_result.similarity-5.0] 175 | if len(_pixivList) == 1: 176 | saucenao_resp.results = [first_result] 177 | # return saucenao_resp 178 | else: 179 | second_result = saucenao_resp.results[0] 180 | # 画师不同, 前者pid大于后者 - 选择后者 181 | if first_result.member_name != second_result.member_name and \ 182 | first_result.pixiv_id > second_result.pixiv_id: 183 | saucenao_resp.results = [second_result] 184 | # return saucenao_resp 185 | 186 | # second_result.index_id == DB_id["pixiv"] and \ 187 | # saucenao_resp.results 188 | 189 | # 相似度排序 190 | saucenao_resp.results.sort(key=lambda i:i.similarity, reverse=True) 191 | return saucenao_resp -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/test/test_ascii2d.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from Arsenal.bot_img_search.bot_img_search import Bot_Img_Search 3 | 4 | engines = Bot_Img_Search.engines 5 | search = engines["Ascii2d"]().search 6 | 7 | # file 8 | def test_file(file=None): 9 | logger.info(file) 10 | result = search(file=file) 11 | return result 12 | 13 | # url 14 | def test_url(url=None): 15 | logger.info(url) 16 | result = search(url=url) 17 | return result 18 | 19 | # if __name__ == "__main__": 20 | # file = r"" 21 | # url = "" 22 | # test_file(file=file) 23 | # test_url(url=url) -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/test/test_saucenao.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from Arsenal.bot_img_search.bot_img_search import Bot_Img_Search 3 | 4 | engines = Bot_Img_Search.engines 5 | search = engines["SauceNao"]().search 6 | 7 | # file 8 | def test_file(file=None): 9 | logger.info(file) 10 | result = search(file=file) 11 | return result 12 | 13 | # url 14 | def test_url(url=None): 15 | logger.info(url) 16 | result = search(url=url) 17 | return result 18 | 19 | # if __name__ == "__main__": 20 | # file = r"" 21 | # url = "" 22 | # test_file(file=file) 23 | # test_url(url=url) -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .ascii2d import Ascii2dItem, Ascii2dResp 2 | from .saucenao import SauceNaoItem, SauceNaoResp -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/utils/ascii2d.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : ascii2d.py 4 | @Time : 2022/02/27 23:55:22 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | 12 | 13 | from loguru import logger 14 | 15 | 16 | class Ascii2dItem: 17 | URL = 'https://ascii2d.net' 18 | 19 | def __init__(self, item) -> None: 20 | # 图片分辨率 格式 大小信息 21 | self.info = "unknown" 22 | # 缩略图 23 | self.thumb = "" 24 | # hash 25 | self.hash = "unknown" 26 | 27 | # 来源链接 28 | self.pic_link = "unknown" 29 | # 标题 30 | self.pic_name = "unknown" 31 | # 作者链接 32 | self.author_link = "unknown" 33 | # 作者名称 34 | self.author = "unknown" 35 | # 类型 36 | self.type = "unknown" 37 | 38 | self._integrate(item) 39 | 40 | def _integrate(self, item): 41 | """data type: html""" 42 | try: 43 | self.info = item.xpath(""".//div[2]/small/text()""")[0] 44 | except: 45 | pass 46 | 47 | try: 48 | self.thumb = f"{Ascii2dItem.URL}{item.xpath('.//div[1]/img/@src')[0]}" 49 | except: 50 | pass 51 | 52 | try: 53 | self.hash = item.xpath(""".//div[2]/div[@class='hash']/text()""")[0] 54 | except: 55 | pass 56 | 57 | self.get_detailInfo(item) 58 | 59 | # 额外处理 60 | self.pic_name = self.pic_name.replace("\u3000"," ").replace("\n","") 61 | self.type = self.type.replace("\n","") 62 | 63 | # pic_link 64 | self.pic_link = self.pic_link.replace("\n","") 65 | if "/ch2/" in self.pic_link: 66 | self.pic_link = Ascii2dItem.URL + self.pic_link 67 | 68 | 69 | def get_detailInfo(self, item)->dict: 70 | detail_info = item.xpath(""".//div[@class='detail-box gray-link']""") 71 | logger.debug(detail_info) 72 | 73 | if detail_info: 74 | check_data = detail_info[0].xpath("""./strong[@class='info-header']/text()""") 75 | # ascii2d类型数据 76 | if not check_data: 77 | _ = detail_info[0].xpath(""".//h6""") 78 | if not _:return 79 | 80 | _test = _[0].xpath("""./small[@class='text-muted']/a""") 81 | _dmm = _[0].xpath("""./small[@class='text-muted']""") 82 | if _test: 83 | try: 84 | self.pic_name = _[0].xpath("""./text()""")[0] 85 | except: 86 | pass 87 | 88 | try: 89 | self.pic_link = _dmm[0].xpath("""./a/@href""")[0] 90 | except: 91 | pass 92 | 93 | try: 94 | self.type = _dmm[0].xpath("""./a/text()""")[0] 95 | except: 96 | pass 97 | else: 98 | try: 99 | self.pic_name = _[0].xpath("""./a[1]/text()""")[0] 100 | except: 101 | pass 102 | 103 | try: 104 | self.pic_link = _[0].xpath("""./a[1]/@href""")[0] 105 | except: 106 | pass 107 | 108 | try: 109 | self.author = _[0].xpath("""./a[2]/text()""")[0] 110 | except: 111 | pass 112 | 113 | try: 114 | self.author_link = _[0].xpath("""./a[2]/@href""")[0] 115 | except: 116 | pass 117 | 118 | try: 119 | self.type = _[0].xpath("""./small/text()""")[0] 120 | except: 121 | pass 122 | 123 | # 评论类型数据 124 | elif check_data[0] == "登録された詳細": 125 | comment_data = detail_info[0].xpath("""./div[@class='external']""")[0] 126 | data = comment_data.xpath(" ./a") 127 | logger.debug(f" - {comment_data} | - {data}") 128 | 129 | # 较少数据 130 | if not data: 131 | try: 132 | self.pic_link = comment_data.xpath("""./text()""")[0] 133 | except: 134 | pass 135 | 136 | # 较多数据 137 | else: 138 | # 评论+文字+链接 如:dmm dlsite 139 | if len(data) == 1: 140 | try: 141 | self.pic_name = comment_data.xpath("""./text()""")[0] 142 | except: 143 | pass 144 | 145 | try: 146 | self.pic_link = data[0].xpath("""./@href""")[0] 147 | except: 148 | pass 149 | 150 | try: 151 | self.type = data[0].xpath("""./text()""")[0] 152 | except: 153 | pass 154 | 155 | elif len(data) == 2: 156 | try: 157 | self.pic_name = data[0].xpath("""./text()""")[0] 158 | except: 159 | pass 160 | 161 | try: 162 | self.pic_link = data[0].xpath("""./@href""")[0] 163 | except: 164 | pass 165 | 166 | try: 167 | self.author = data[1].xpath("""./text()""")[0] 168 | except: 169 | pass 170 | 171 | try: 172 | self.author_link = data[1].xpath("""./@href""")[0] 173 | except: 174 | pass 175 | 176 | try: 177 | self.type = comment_data.xpath("""./small/text()""")[0] 178 | except: 179 | pass 180 | 181 | 182 | class Ascii2dResp: 183 | def __init__(self, resp_list:list) -> None: 184 | self.results = [] 185 | 186 | for resp in resp_list: 187 | self.results.append(Ascii2dItem(resp)) -------------------------------------------------------------------------------- /code/Arsenal/bot_img_search/utils/saucenao.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : saucenao.py 4 | @Time : 2022/02/28 00:32:09 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | from typing import List 12 | from Arsenal.basic.log_record import logger 13 | 14 | 15 | class SauceNaoItem: 16 | URL = 'https://saucenao.com' 17 | 18 | def __init__(self, item) -> None: 19 | # 数据库id 20 | self.index_id = int() 21 | # 相似度 22 | self.similarity = float() 23 | # 缩略图 24 | self.thumbnail = "" 25 | # 图片标题 / 漫画标题 26 | self.title = "unknown" 27 | # 图片链接 / 本子链接 28 | self.url = "" 29 | # 作者名称 30 | self.member_name = "unknown" 31 | 32 | # 插画pid 可能存在 33 | self.pixiv_id = 0 34 | # 作者uid 可能存在 35 | self.member_id = 0 36 | # 同人本名称 可能存在 37 | self.doujinName = "unknown" 38 | # 番剧 可能存在 - 用于在anilist中查询番剧信息 39 | self.anilist_id = int() 40 | 41 | # # 区分普通搜图与搜本 42 | # self.normal = "normal" 43 | # self.doujin = "doujin" 44 | 45 | self._integrate(item) 46 | 47 | def _integrate(self, item): 48 | """ 49 | data type: json 50 | """ 51 | self.index_id = item["header"]["index_id"] 52 | self.similarity = float(item["header"]["similarity"]) 53 | self.thumbnail = item["header"].get("thumbnail", "") 54 | self.title = self._get_title(item) 55 | self.url = self._get_url(item) 56 | self.member_name = self._get_author(item) 57 | self.pixiv_id = item["data"].get("pixiv_id", 0) 58 | self.member_id = item["data"].get("member_id", 0) 59 | self.doujinName = self._get_doujinName(item) 60 | self.anilist_id = item["data"].get("anilist_id", 0) 61 | 62 | @staticmethod 63 | def _get_title(item): 64 | data:dict = item["data"] 65 | title = data.get('title',"") or data.get('material',"") or \ 66 | data.get('source',"") or data.get('created_at',"") or "unknown" 67 | return title 68 | 69 | @staticmethod 70 | def _get_author(item): 71 | data:dict = item["data"] 72 | try: 73 | author = data.get('author',"") or data.get('author_name',"") or \ 74 | data.get('member_name',"") or data.get('pawoo_user_username',"") or \ 75 | data.get('company',"") or data.get('creator',[""])[0] or \ 76 | data.get('creator',"") or "unknown" 77 | except Exception as e: 78 | logger.warning(f"Exception - {e} - {item}") 79 | author = "unknown" 80 | return author 81 | 82 | @staticmethod 83 | def _get_url(item): 84 | data:dict = item["data"] 85 | if "ext_urls" in data: 86 | url = data['ext_urls'][0] 87 | elif "getchu_id" in data: 88 | url = f"http://www.getchu.com/soft.phtml?id={data['getchu_id']}" 89 | else: 90 | url = "" 91 | return url 92 | 93 | @staticmethod 94 | def _get_doujinName(item): 95 | data:dict = item["data"] 96 | doujinName = data.get("jp_name", "") or data.get("eng_name", "") or "unknown" 97 | return doujinName 98 | 99 | 100 | class SauceNaoResp: 101 | def __init__(self, resp:list) -> None: 102 | resp_header = resp["header"] 103 | resp_results = resp["results"] 104 | 105 | # 解析结果 106 | self.results: List[SauceNaoItem] = [SauceNaoItem(item) for item in resp_results] 107 | # 剩余搜索次数/30s 108 | self.short_limit: int = resp_header['short_remaining'] 109 | # 剩余搜索次数/24h 110 | self.long_limit: int = resp_header['long_remaining'] 111 | -------------------------------------------------------------------------------- /code/Arsenal/bot_random_text/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_random_text -------------------------------------------------------------------------------- /code/Arsenal/bot_random_text/bot_random_text.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import random 4 | from gtts import gTTS 5 | 6 | from Arsenal.basic.plugin_res_directory import pdr 7 | from Arsenal.basic.BNConnect import baseRequest 8 | 9 | """ 10 | from future.Arsenal.basic.BNConnect import baseRequest 11 | from bot_random_text import Bot_Random_Text 12 | data = {"group_id":112233,"user_id":1508015265,"message_type":"group","message":"骂他[CQ:at,qq=3012797743]"} 13 | Bot_Random_Text.service_func(data) 14 | """ 15 | class Random_Text: 16 | """随机文字接口(舔狗日记,彩虹屁,祖安词典) 17 | 非指向性时 --> 转语音 18 | 指向性时 --> 不转语音 19 | """ 20 | def __init__(self): 21 | self.plugin_name = type(self).__name__ 22 | self.workspace = pdr.get_plus_res(self.plugin_name) 23 | # self.workspace = "" 24 | 25 | self.default_headers = { 26 | "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 27 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', 28 | } 29 | self.temp_list = [ 30 | { 31 | "keyword": ["彩虹屁","舔我","舔他"], 32 | "api": "https://chp.shadiao.app/api.php", 33 | "except_word": "正在舔的路上...", 34 | "specified_word": "舔他" 35 | }, 36 | { 37 | "keyword": ["舔狗"], 38 | "api": "https://api.oick.cn/dog/api.php", 39 | "except_word": "舔狗炸了...", 40 | "specified_word": "" 41 | }, 42 | { 43 | "keyword": ["祖安词典","骂我","骂他"], 44 | "api": "https://zuanbot.com/api.php?level=min&lang=zh_cn", 45 | "except_word": "正在骂的路上...", 46 | "specified_word": "骂他" 47 | }, 48 | { 49 | "keyword": ["毒鸡汤","毒我","毒他"], 50 | "api": "https://api.oick.cn/dutang/api.php", 51 | "except_word": "正在煲汤的路上...", 52 | "specified_word": "毒他" 53 | } 54 | ] 55 | # 祖安词典/毒鸡汤白名单 56 | self.ugly_specified_words = ["骂他","毒他"] 57 | self.protection_list = [2076465138,3012797743] 58 | self.bot_list = [1359453526,3480538398] 59 | self.safe_text = { 60 | "protection_raw":"不可以骂尊贵的master们哦", 61 | "protection":"不可以骂尊贵的master们哦(^_-)~", 62 | "bot_raw":"想让我骂自己? 没门!", 63 | "bot":"想让我骂自己? 没门!", 64 | } 65 | 66 | def gtts_text2audio(self,word): 67 | """ 68 | 使用Google TTS实现文字转语音 69 | :params word: 文字 70 | :return: audio音频路径 71 | """ 72 | tts = gTTS( 73 | text=word, 74 | lang='zh-CN', 75 | ) 76 | # cq发送MP3需安装ffmpeg 77 | filename = "{}.mp3".format(int(time.time())) 78 | file_path = os.path.join(self.workspace,filename) 79 | tts.save(file_path) 80 | return file_path 81 | 82 | def match_temp(self,eval_cqp_data): 83 | """ 84 | 根据消息匹配出接口并执行 85 | :parmas eval_cqp_data: 86 | :return: 返回组装好的数据包 87 | """ 88 | msg = eval_cqp_data["message"] 89 | data = { 90 | "group_id":eval_cqp_data["group_id"], 91 | "message":"" 92 | } 93 | 94 | for temp in self.temp_list: 95 | # 循环遍历是否符合当前temp的关键词 96 | keyword = "" 97 | for _ in temp["keyword"]: 98 | if _ in msg: 99 | keyword = _ 100 | break 101 | else: 102 | continue 103 | 104 | if keyword: 105 | options = {"url":temp["api"], "headers":self.default_headers} 106 | resp = baseRequest(options) 107 | word = resp.text if resp else temp["except_word"] 108 | print("bot_random_text match_temp | word:{}".format(word)) 109 | # 指向性关键词触发->不进行文字转语音 110 | if temp["specified_word"] != "" and\ 111 | temp["specified_word"] in msg and\ 112 | "[CQ:at,qq=" in msg: 113 | user_id = msg.split("]")[0].split("[CQ:at,qq=")[-1] 114 | # 白名单-master 115 | if int(user_id) in self.protection_list and keyword in self.ugly_specified_words: 116 | data["message"] = "[CQ:at,qq={}]\n{}".format( 117 | eval_cqp_data["user_id"],self.safe_text["protection"]) 118 | return data 119 | # 白名单-bot 120 | elif int(user_id) in self.bot_list and keyword in self.ugly_specified_words: 121 | data["message"] = "[CQ:at,qq={}]\n{}\n\n{}".format( 122 | eval_cqp_data["user_id"],self.safe_text["bot"],word) 123 | return data 124 | else: 125 | data["message"] = "[CQ:at,qq={}]\n{}".format(user_id,word) 126 | return data 127 | 128 | # 文字转语音 129 | if resp: 130 | try: 131 | result = self.gtts_text2audio(word) 132 | except Exception as e: 133 | print("bot_random_text match_temp Error:{} | word:{}".format(e,word)) 134 | data["message"] = "[CQ:at,qq={}]\n{}".format(eval_cqp_data["user_id"],word) 135 | return data 136 | else: 137 | data["message"] = "[CQ:record,file=file:///{}]".format(result) 138 | return data 139 | # 无匹配 140 | return None 141 | 142 | def service_func(self,eval_cqp_data): 143 | if eval_cqp_data.get('message_type','') == 'group' and\ 144 | eval_cqp_data.get('message','') != '': 145 | match_text_data = self.match_temp(eval_cqp_data) 146 | return match_text_data 147 | 148 | 149 | Bot_Random_Text = Random_Text() -------------------------------------------------------------------------------- /code/Arsenal/bot_report_time/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_report_time -------------------------------------------------------------------------------- /code/Arsenal/bot_report_time/bot_report_time.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from PIL import Image, ImageDraw, ImageFont 4 | 5 | def add_test_img(eval_cqp_data): 6 | person_time = time.localtime() 7 | hour = str(person_time.tm_hour) 8 | minutes = str(person_time.tm_min) 9 | 10 | if len(hour) == 1: 11 | hour = "0" + hour 12 | 13 | if len(minutes) == 1: 14 | minutes = "0" + minutes 15 | 16 | text_list = [ 17 | {"point": (400,15),"char": hour}, 18 | {"point": (400,50),"char": "点"}, 19 | {"point": (400,85),"char": minutes}, 20 | {"point": (400,120),"char": "分"}, 21 | {"point": (400,155),"char": "了"}, 22 | {"point": (400,190),"char": "!!"}, 23 | ] 24 | 25 | # 加载模板图像 26 | img = Image.open("../res/test.png") 27 | # 创建绘制对象 28 | draw = ImageDraw.Draw(img) 29 | # 载入字体 30 | ttf_path = "../res/dy.ttf" 31 | font = ImageFont.truetype(ttf_path,30) 32 | # 写入文字 33 | for text in text_list: 34 | draw.text(text["point"],text["char"],"black",font) 35 | 36 | res_path = os.path.join(os.getcwd(),"res") 37 | report_img_filename = "result-{}{}.png".format(hour,minutes) 38 | report_img_path = os.path.join(res_path,report_img_filename) 39 | img.save(report_img_path) 40 | # img.save("./res/add_text.png") 41 | 42 | reply_group = { 43 | "group_id": eval_cqp_data['group_id'], 44 | "message":"[CQ:image,file=file:///{}]".format(report_img_path) 45 | } 46 | return reply_group 47 | -------------------------------------------------------------------------------- /code/Arsenal/bot_suolink/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_suolink -------------------------------------------------------------------------------- /code/Arsenal/bot_suolink/bot_suolink.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : bot_suolink.py 4 | @Time : 2021/12/15 17:09:20 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import requests 12 | 13 | from Arsenal.basic.msg_temp import PLUGIN_BLOCK 14 | 15 | # 网络请求函数修正后再统一导入使用 16 | # from BNConnect import baseRequest 17 | 18 | 19 | class so985: 20 | def __init__(self): 21 | self.plugin_name = type(self).__name__ 22 | self.plugin_nickname = "短链接插件" 23 | self.plugin_level = 8 24 | # print(f"{self.plugin_name} - Warning!!!") 25 | # http://api.985.so/api.php?format=json&url=http%3a%2f%2fwww.baidu.com&apikey=18816765357@94f53154fef918eb70d7b74f9a5dfc67 26 | # {"status":1,"url":"http://r6f.cn/Mjqh","err":""} 27 | self.suolink_api = "http://api.985.so/api.php" 28 | # self.host = "https://dlj.li/{}" 29 | 30 | def parse(self,mybot_data): 31 | # TEST DynamicImport 32 | message = mybot_data['message'] 33 | if message == "test1": 34 | return PLUGIN_BLOCK 35 | # 2022年1月26日23:27:10 36 | elif message == "#reload": 37 | from Arsenal.basic.restart_mybot import restart 38 | restart() 39 | else: 40 | return 41 | 42 | def get_slink(self,url): 43 | """ 44 | :params url: 需要缩短的url 45 | :return: 短链,错误信息 46 | 不报错则为空,报错则为err 47 | """ 48 | # 调用BNC模块 49 | try: 50 | # 这样不会直接编码 51 | # u = "https://dlj.li/api.php?url={}".format(url) 52 | params = { 53 | "url":url, 54 | "apikey":"18816765357@94f53154fef918eb70d7b74f9a5dfc67", 55 | } 56 | resp = requests.get(self.suolink_api,params=params) 57 | print(resp.text) 58 | except Exception as e: 59 | print("so985 ",e) 60 | return "短链生成失败-dlj" 61 | else: 62 | return resp.text 63 | 64 | Bot_suolink = so985() 65 | 66 | # if __name__ == '__main__': 67 | # preview_url = "https://trace.moe/122137/[Ohys-Raws] Dogeza de Tanondemita - 08 (AT-X 1280x720 x264 AAC).mp4?start=101.58&end=123&token=FM2mP_l8IPxUaWAQ--D70A" 68 | # so985().get_slink(preview_url) 69 | -------------------------------------------------------------------------------- /code/Arsenal/bot_whatanime/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_whatanime -------------------------------------------------------------------------------- /code/Arsenal/coding/Readme.md: -------------------------------------------------------------------------------- 1 | 这是为正在开发中或处于`demo`阶段的插件预留的目录 2 | 3 | > 正在努力建设中... -------------------------------------------------------------------------------- /code/Arsenal/coding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/code/Arsenal/coding/__init__.py -------------------------------------------------------------------------------- /code/Arsenal/coding/bot_day_illust/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_day_illust -------------------------------------------------------------------------------- /code/Arsenal/coding/bot_rj_info/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_rj_info -------------------------------------------------------------------------------- /code/Arsenal/coding/bot_rj_info/bot_rj_info.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : dlsite_info.py 4 | @Time : 2021/05/28 21:11:32 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Contact : 1508015265@qq.com 8 | @Desc : None 9 | ''' 10 | 11 | # here put the import lib 12 | import json 13 | from lxml import etree 14 | 15 | 16 | # from basic.plus_res_directory import pdr 17 | from Arsenal.basic.BNConnect import baseRequest 18 | from Arsenal.basic.log_record import logger 19 | 20 | 21 | class RJ_Info: 22 | """有关DLsite上的RJ号查询 23 | 1. 指定RJ号查询 24 | 2. 指定关键字搜索 25 | """ 26 | def __init__(self): 27 | self.headers = { 28 | "referer": "https://www.dlsite.com", 29 | "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" 30 | } 31 | # 关键字搜索 32 | self.search_dl_url = "https://www.dlsite.com/maniax/fsr/=/language/jp/sex_category[0]/male/keyword/{}/order[0]/trend/options[0]/JPN/options[1]/NM/per_page/30/lang_options[0]/%E6%97%A5%E6%9C%AC%E8%AA%9E/lang_options[1]/%E8%A8%80%E8%AA%9E%E4%B8%8D%E5%95%8F" 33 | self.search_result_expression = """.//ul[@id='search_result_img_box']//li/dl""" 34 | # 缩略图 35 | self.pic_expression = """.//a[@class='work_thumb_inner']/img/@src""" 36 | # 名称 37 | self.name_expression = """.//div[@class='multiline_truncate']/a/@title""" 38 | # DL地址 39 | self.url_expression = """.//a[@class='work_thumb_inner']/@href""" 40 | 41 | # 指定RJ号查询 42 | self.rj_ajax = "https://www.dlsite.com/maniax/product/info/ajax" 43 | self.temp_url = "https://www.dlsite.com/maniax/work/=/product_id/{}.html" 44 | 45 | def search_by_RJ(self,data): 46 | if data.get("product_id","") == "": 47 | logger.info("product_id is None") 48 | logger.info(data) 49 | return {} 50 | 51 | params = {"product_id":data["product_id"]} 52 | resp = baseRequest(options={"url":self.rj_ajax,"headers":self.headers},params=params) 53 | if resp: 54 | try: 55 | result = json.loads(resp.text) 56 | except json.decoder.JSONDecodeError as e: 57 | logger.info("JSONDecodeError: {}".format(e)) 58 | logger.info(resp.text) 59 | return {} 60 | else: 61 | product_info = {} 62 | # RJ号信息为空 63 | if result == []: 64 | logger.info(product_info) 65 | return product_info 66 | 67 | for k,v in result.items(): 68 | product_info["pic"] = "http:" + v["work_image"] 69 | product_info["product_name"] = v["work_name"] 70 | product_info["RJ"] = k 71 | product_info["dlsite_url"] = self.temp_url.format(k) 72 | logger.info(product_info) 73 | return product_info 74 | else: 75 | logger.info("resp is None") 76 | logger.info("{} {}".format(self.rj_ajax,data)) 77 | return {} 78 | 79 | def search_by_keyword(self,data): 80 | if data.get("keyword","") == "": 81 | logger.info("keyword is None") 82 | logger.info(data) 83 | return {} 84 | 85 | url = self.search_dl_url.format(data["keyword"]) 86 | resp = baseRequest(options={"url":url,"headers":self.headers,"timeout":10}) 87 | if resp: 88 | obj = etree.HTML(resp.text) 89 | try: 90 | result = obj.xpath(self.search_result_expression) 91 | except Exception as e: 92 | logger.info("Xpath Parser Error: {}".format(e)) 93 | logger.info(resp.text) 94 | return {} 95 | else: 96 | search_result_dict = {} 97 | if result == []: 98 | logger.info(search_result_dict) 99 | return search_result_dict 100 | 101 | # 只取前三个 102 | for _ in result[:3]: 103 | pic = _.xpath(self.pic_expression)[0] 104 | name = _.xpath(self.name_expression)[0] 105 | url = _.xpath(self.url_expression)[0] 106 | RJ = url.split("/")[-1].split(".")[0] 107 | 108 | search_result_dict[RJ] = {} 109 | search_result_dict[RJ]["pic"] = "http:" + pic 110 | search_result_dict[RJ]["name"] = name 111 | search_result_dict[RJ]["url"] = url 112 | search_result_dict[RJ]["RJ"] = RJ 113 | 114 | logger.info(search_result_dict) 115 | return search_result_dict 116 | else: 117 | logger.info("resp is None") 118 | logger.info("{} {}".format(url,data)) 119 | return {} 120 | 121 | def parse(self): 122 | return "Unexpected use..." 123 | 124 | Bot_RJ_Info = RJ_Info() 125 | """ 126 | from bot_rj_info import Bot_RJ_Info 127 | data = {"product_id":"RJ250814"} 128 | data = {"product_id":""} 129 | data = {"product_id":" "} 130 | data = {"product_id":"asa"} 131 | data = {"product_id":"123321"} 132 | Bot_RJ_Info.search_by_RJ(data) 133 | 134 | from bot_rj_info import Bot_RJ_Info 135 | data = {"keyword":"RJ250814"} 136 | data = {"keyword":"RJ"} 137 | data = {"keyword":""} 138 | data = {"keyword":" "} 139 | data = {"keyword":"双 子"} 140 | data = {"keyword":"双子"} 141 | data = {"keyword":"双子乙女"} 142 | Bot_RJ_Info.search_by_keyword(data) 143 | 144 | """ -------------------------------------------------------------------------------- /code/Arsenal/coding/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | 处理网络错误 3 | """ 4 | 5 | def error(data,m=0): 6 | if error == None: 7 | return 8 | print("error",data) 9 | if m == 0: 10 | message = "网络出错啦!" 11 | elif m == 1: 12 | message = "查询的pid错误!" 13 | else: 14 | pass 15 | 16 | # 群聊 17 | if data["message_type"] == "group": 18 | group_id = data["group_id"] 19 | at = "[CQ:at,qq=" + str(data['user_id']) + "]" 20 | 21 | res = { 22 | "group_id": group_id, 23 | "message": at + "\n" + message 24 | } 25 | else: 26 | user_id = data["user_id"] 27 | res = { 28 | "user_id": user_id, 29 | "message": message 30 | } 31 | return res 32 | -------------------------------------------------------------------------------- /code/Arsenal/coding/trash/bot_blhx_prediction/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_blhx_prediction -------------------------------------------------------------------------------- /code/Arsenal/coding/trash/bot_blhx_prediction/bot_blhx_prediction.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import random 4 | import requests 5 | from PIL import Image, ImageDraw, ImageFont 6 | 7 | from Arsenal.basic.plugin_res_directory import pdr 8 | 9 | 10 | class Blhx_Secretary: 11 | """碧蓝航线随机秘书抽取 12 | """ 13 | def __init__(self): 14 | """初始化工作""" 15 | self.plugin_name = type(self).__name__ 16 | self.api_url = "https://api.bilibili.com/x/activity/prediction" 17 | self.headers = { 18 | "user-agent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36" 19 | } 20 | self.point_list = list(range(10,110,10)) 21 | self.ttf_path = "../res/dy.ttf" 22 | self.temporary_file_path = pdr.get_plus_res(self.plugin_name) 23 | # self.temporary_file_path = os.path.join(os.getcwd(),"../res/blhx") 24 | isExists = os.path.exists(self.temporary_file_path) 25 | if not isExists:os.mkdir(self.temporary_file_path) 26 | 27 | # def parse(self,eval_cqp_data->dict):->dict 28 | # """解析函数""" 29 | # pass 30 | 31 | def download_pic(self,path,info): 32 | """下载图片""" 33 | resp_content = requests.get(info["pic"],headers=self.headers,timeout=10).content 34 | with open(path,"wb") as f: 35 | f.write(resp_content) 36 | 37 | def get_pic_favor(self,nickname): 38 | """ 39 | 简要注明函数功能 40 | 41 | :param word:输入的单词 42 | :return: 返回生成的单词字典 43 | """ 44 | params = { 45 | "sid":"11947", 46 | # 这里用昵称,写在图片上用qq号 47 | "nickname":nickname, 48 | "point":random.choice(self.point_list), 49 | } 50 | print(params) 51 | try: 52 | # 2.0改为使用基础模块,基础模块增加下载文件Func 53 | resp = requests.get(self.api_url,headers=self.headers,params=params,timeout=10) 54 | except Exception as e: 55 | resp = "" 56 | if not resp: 57 | return None 58 | 59 | try: 60 | result = json.loads(resp.text) 61 | except Exception as e: 62 | print("json解析错误") 63 | return None 64 | 65 | if result["data"] == None or result["code"] != 0: 66 | return None 67 | 68 | pic = "{}{}".format("http:",result["data"]["157"]["image"]) 69 | favor = str(result["data"]["160"]["desc"]) 70 | 71 | return {"pic":pic,"favor":favor} 72 | 73 | def add_text(self,path,new_path,point_text_list): 74 | # ttf_path = "./res/dy.ttf" 75 | # ttf_path = "../res/dy.ttf" 76 | img = Image.open(path) 77 | draw = ImageDraw.Draw(img) 78 | font = ImageFont.truetype(self.ttf_path,30) 79 | for text in point_text_list: 80 | draw.text(text["point"],text["char"],"black",font) 81 | 82 | img.save(new_path) 83 | return new_path 84 | 85 | def main(self,eval_cqp_data): 86 | """主函数""" 87 | # 获取CQ数据包 88 | 89 | nickname = eval_cqp_data.get("sender","None")["nickname"] 90 | info = self.get_pic_favor(nickname) 91 | if info: 92 | filename = info["pic"].rsplit("/",1)[-1] 93 | path = os.path.join(self.temporary_file_path,filename) 94 | new_path = os.path.join(self.temporary_file_path,filename) 95 | point_text_list = [ 96 | {"point": (440,970),"char": str(eval_cqp_data["user_id"])}, 97 | {"point": (624,953),"char": info['favor']}, 98 | ] 99 | 100 | self.download_pic(path,info) 101 | self.add_text(path,new_path,point_text_list) 102 | data = { 103 | "group_id": eval_cqp_data["group_id"], 104 | "message":"[CQ:at,qq={}]\n今天侍奉你的秘书舰为".format(eval_cqp_data["user_id"]) + 105 | "[CQ:image,file=file:///{}]".format(new_path), 106 | } 107 | else: 108 | data = { 109 | "group_id": eval_cqp_data["group_id"], 110 | "message":"[CQ:at,qq={}]\n接口错误,请联系master".format(eval_cqp_data["user_id"]) 111 | } 112 | 113 | return data,path,new_path 114 | # requests.get() 115 | # if new_path: 116 | # os.remove(path) 117 | # os.remove(new_path) 118 | 119 | Bot_Blhx_Secretary = Blhx_Secretary() 120 | 121 | # 测试数据 122 | # eval_cqp_data = {"group_id":123, "user_id":5101314, "sender":{"nickname":"桜花树下宇焉酱"}} 123 | # Bot_Blhx_Secretary.main(eval_cqp_data) 124 | # ============ 调用demo——添加文字 ============ 125 | # path = r"C:\Users\lenovo\Desktop\test.jpg" 126 | # new_path = r"C:\Users\lenovo\Desktop\test1.jpg" 127 | # favor = str(random.choice(Bot_Blhx_Secretary.point_list)) 128 | # point_text_list = [ 129 | # {"point": (420,970),"char": "1508015265"}, 130 | # {"point": (627,953),"char": favor}, 131 | # ] 132 | # Bot_Blhx_Secretary.add_text(path,new_path,point_text_list) 133 | # ============ 调用demo——添加文字 ============ 134 | 135 | """ 136 | 上层调用 137 | from blhx_prediction import Bot_Blhx_Secretary 138 | if eval_cqp_data.get('group_id','') in [1072957655,813614458,965302904,780849000]: 139 | # 随机概率 > 中奖阈值 140 | if prob >= limit_prob: 141 | blhx_data,blhx_path,blhx_new_path = Bot_Blhx_Secretary.main(eval_cqp_data) 142 | print(blhx_data) 143 | time.sleep(0.01) 144 | requests.get(url=qunliao, params=blhx_data) 145 | if blhx_new_path: 146 | os.remove(blhx_path) 147 | os.remove(blhx_new_path) 148 | """ -------------------------------------------------------------------------------- /code/Arsenal/coding/trash/bot_pokenman/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bot_pokenman -------------------------------------------------------------------------------- /code/Arsenal/coding/trash/bot_pokenman/bot_pokenman.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import random 4 | from lxml import etree 5 | # pip3 install PyExecJS 6 | # import execjs,datetime 7 | 8 | from Arsenal.basic.plugin_res_directory import pdr 9 | 10 | def return_value(name1,name2): 11 | try: 12 | h = etree.parse('poken.html') 13 | except: 14 | print('将poken.html放在同一目录/修改打开路径') 15 | exit() 16 | 17 | o = h.xpath('//div[@class="list"]//option') 18 | 19 | res = [] # 记录value 20 | res1 = {} # 记录隐藏text和value 21 | 22 | for i in o: 23 | value_dict = {} 24 | # 获得name对应的value 25 | if str(i.xpath('./text()')[0]) == name1: 26 | value_dict[name1] = int(i.xpath('./@value')[0]) # n1 v1 27 | if str(i.xpath('./text()')[0]) == name2: 28 | value_dict[name2] = int(i.xpath('./@value')[0]) # n2 v2 29 | if value_dict: 30 | res.append(value_dict) 31 | print("res",res) 32 | 33 | if len(res) < 2 and name1 != name2: 34 | print('两只宝可梦中存在暂不支持的宝可梦') 35 | exit() 36 | 37 | o1 = h.xpath('//div[@class="name1"]//option') 38 | for i in o1: 39 | if int(i.xpath('./@value')[0]) == res[0]: 40 | res1[str(i.xpath('./text()')[0])] = res[0] 41 | break 42 | 43 | o2 = h.xpath('//div[@class="name2"]//option') 44 | for i in o2: 45 | if int(i.xpath('./@value')[0]) == res[1]: 46 | res1[str(i.xpath('./text()')[0])] = res[1] 47 | break 48 | 49 | return res,res1 50 | 51 | 52 | class Pokenman: 53 | """宝可梦杂交""" 54 | def __init__(self): 55 | self.plugin_name = type(self).__name__ 56 | # self.workspace = pdr.get_plus_res(self.plugin_name) 57 | self.workspace = os.path.join(pdr.resource, "Pokenman") 58 | 59 | self.blend_path = os.path.join(self.workspace,"blend") 60 | self.raw_path = os.path.join(self.workspace,"raw") 61 | self.blend_img = "https://images.alexonsager.net/pokemon/fused/{}/{}.{}.png" 62 | self.raw_img = "https://images.alexonsager.net/pokemon/{}.png" 63 | 64 | self.html_path = os.path.join(self.workspace,"poken.html") 65 | self.html_options = self.pokenman_parse() 66 | 67 | def pokenman_parse(self,class_name="list"): 68 | """解析并获得宝可梦名单""" 69 | h = etree.parse(self.html_path) 70 | o = h.xpath('//div[@class="{}"]//option'.format(class_name)) 71 | return o 72 | 73 | def get_spirit_data(self,spirit_list=None): 74 | """ 75 | 获取两个宝可梦的数据 76 | :return: 77 | spirit_data = { 78 | 'father': {'name': '超梦', 'value': '150', 'path': '150.png'}, 79 | 'mother': {'name': '超梦', 'value': '150', 'path': '150.png'} 80 | } 81 | """ 82 | spirit_data = {"father":{},"mother":{}} 83 | # 未指定两个杂交的宝可梦名称 84 | if not spirit_list: 85 | for i in range(2): 86 | item = {} 87 | option = random.choice(self.html_options) 88 | item["name"] = option.xpath("./text()")[0] 89 | value = option.xpath("./@value")[0] 90 | item["value"] = value 91 | item["path"] = os.path.join(self.raw_path,"{}.png".format(value)) 92 | 93 | if spirit_data["father"] != {}: 94 | spirit_data["mother"] = item 95 | else: 96 | spirit_data["father"] = item 97 | # spirit_data.append(item) 98 | # 指定spirit_list 99 | else: 100 | for spirit in spirit_list: 101 | def find_spirit(spirit): 102 | item = {} 103 | for option in self.html_options: 104 | if option.xpath('./text()')[0] == spirit: 105 | item["name"] = option.xpath("./text()")[0] 106 | value = option.xpath("./@value")[0] 107 | item["value"] = value 108 | item["path"] = os.path.join(self.raw_path,"{}.png".format(value)) 109 | # item[spirit] = int(option.xpath('./@value')[0]) 110 | break 111 | return item 112 | item = find_spirit(spirit) 113 | if spirit_data["father"] != {}: 114 | spirit_data["mother"] = item 115 | else: 116 | spirit_data["father"] = item 117 | # spirit_data.append(item) 118 | 119 | return spirit_data 120 | 121 | def blend(self,name1=None,name2=None): 122 | """ 123 | 根据指定的2个宝可梦合成信息 124 | :params name1: 宝可梦1 125 | :params name2: 宝可梦2 126 | :return: 127 | spirit_data = { 128 | 'father': {'name': '超梦', 'value': '150', 'path': '150.png'}, 129 | 'mother': {'name': '超梦', 'value': '150', 'path': '150.png'} 130 | } 131 | blend_path = "150.150.png" 132 | """ 133 | spirit_list = None 134 | if name1 and name2: 135 | spirit_list = [name1,name2] 136 | 137 | spirit_data = self.get_spirit_data(spirit_list=spirit_list) 138 | # print("spirit_data_father",spirit_data["father"]["name"]) 139 | # print("spirit_data_mother",spirit_data["mother"]["name"]) 140 | 141 | blend_data = {} 142 | # 杂交后命名 father在前 143 | blend_name = "" 144 | for options in self.pokenman_parse(class_name="name1"): 145 | if options.xpath('./@value')[0] == spirit_data["father"]["value"]: 146 | blend_name += options.xpath('./text()')[0] 147 | break 148 | 149 | for options in self.pokenman_parse(class_name="name2"): 150 | if options.xpath('./@value')[0] == spirit_data["mother"]["value"]: 151 | blend_name += options.xpath('./text()')[0] 152 | break 153 | 154 | 155 | # 杂交后的img mother在前 156 | blend_filename = "{}.{}.png".format( 157 | spirit_data["mother"]["value"], 158 | spirit_data["father"]["value"] 159 | ) 160 | blend_data["name"] = blend_name 161 | blend_data["path"] = os.path.join(self.blend_path,blend_filename) 162 | 163 | 164 | return spirit_data,blend_data 165 | 166 | def service_func(self,eval_cqp_data): 167 | if eval_cqp_data.get('message_type','') == 'group': 168 | group_id = eval_cqp_data["group_id"] 169 | user_id = eval_cqp_data["user_id"] 170 | message = eval_cqp_data['message'] 171 | keyword = "宝可梦杂交" 172 | 173 | # 未触发关键词 174 | if message != keyword: 175 | return 176 | 177 | spirit_data,blend_data = self.blend() 178 | 179 | # father在前 180 | data_raw_spirit = { 181 | "group_id": group_id, 182 | "message": "[CQ:at,qq={}]\n本次杂交实验目标\n[CQ:image,file=file:///{}]\n\n[CQ:image,file=file:///{}]".format( 183 | user_id,spirit_data["father"]["path"], 184 | spirit_data["mother"]["path"]) 185 | } 186 | 187 | blend_result_text = "杂交成功!得到了新的宝可梦!" 188 | if spirit_data["father"]["value"] == spirit_data["mother"]["value"]: 189 | blend_result_text = "杂交失败...得到了'新'的宝可梦?" 190 | 191 | # 杂交公式 192 | blend_spirit_text = "杂交公式:\n{} + {} = {}".format( 193 | spirit_data["father"]["name"], 194 | spirit_data["mother"]["name"], 195 | blend_data["name"] 196 | ) 197 | print(blend_spirit_text) 198 | 199 | data_blend_spirit = { 200 | "group_id": group_id, 201 | "message": "[CQ:at,qq={}]\n{}\n{}\n[CQ:image,file=file:///{}]".format( 202 | user_id,blend_result_text,blend_spirit_text,blend_data["path"] 203 | ) 204 | } 205 | # print(data_raw_spirit,"\n",data_blend_spirit) 206 | return data_raw_spirit,data_blend_spirit 207 | 208 | # 前期素材准备 209 | def download_img(self): 210 | # 目前一共151位宝可梦 211 | # 杂交结果... 212 | import requests 213 | headers = { 214 | "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (\ 215 | KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" 216 | } 217 | # blend_img = "https://images.alexonsager.net/pokemon/fused/{}/{}.{}.png" 218 | # raw_img = "https://images.alexonsager.net/pokemon/{}.png" 219 | # blend_path = os.path.join(self.workspace,"blend") 220 | # raw_path = os.path.join(self.workspace,"raw") 221 | 222 | # 原始图像 223 | for i in range(1,152): 224 | now_raw_path = os.path.join(self.raw_path,"{}.png".format(i)) 225 | if not os.path.exists(now_raw_path): 226 | resp = requests.get(self.raw_img.format(i),headers=headers).content 227 | with open(now_raw_path,"wb") as f: 228 | f.write(resp) 229 | # print(now_raw_path) 230 | 231 | # 杂交图像 232 | for i in range(1,152): 233 | for j in range(1,152): 234 | now_blend_path = os.path.join(self.blend_path,"{}.{}.png".format(i,j)) 235 | if not os.path.exists(now_blend_path): 236 | resp = requests.get(self.blend_img.format(i,i,j),headers=headers).content 237 | with open(now_blend_path,"wb") as f: 238 | f.write(resp) 239 | print(now_blend_path) 240 | # break 241 | # break 242 | 243 | 244 | Bot_Pokenman = Pokenman() 245 | # Bot_Pokenman.blend() 246 | # Bot_Pokenman.blend("超梦","梦幻") 247 | # Bot_Pokenman.blend("梦幻","超梦") 248 | # Bot_Pokenman.blend("超梦","超梦") 249 | # Bot_Pokenman.service_func({"group_id":1122334,"user_id":1508015265,"message_type":"group","message":"宝可梦杂交"}) 250 | # Bot_Pokenman.download_img() 251 | 252 | 253 | 254 | # if __name__ == '__main__': 255 | # o = test_list() 256 | # n1 = input('第一位宝可梦:') 257 | # n2 = input('第二位宝可梦:') 258 | 259 | # if n1 == '' or n2 == '' or n1.isdecimal() == True or n2.isdecimal() == True: 260 | # print('宝可梦名字不能为空/数字') 261 | # exit() 262 | # res,res1 = return_value(n1,n2) 263 | # result = ''.join(r for r in res1) 264 | # # img_url = "https://images.alexonsager.net/pokemon/fused/58/58.21.png" 265 | # img_url = "https://images.alexonsager.net/pokemon/fused/{}/{}.{}.png".\ 266 | # format(res[-1].values,res[-1],res[0]) 267 | # print(result,res,res1) 268 | # print(img_url) 269 | 270 | 271 | # for n1 in o: 272 | # for n2 in o: 273 | # try: 274 | # res = return_value(n1,n2) 275 | # res = ''.join(r for r in res) # new_name 276 | # # print (res) # new_name 277 | # with open('result.txt','a') as f: 278 | # f.write(res) 279 | # f.write('\n') 280 | # except: 281 | # print(n1,n2) 282 | 283 | # j = open('poken.js','r') 284 | # c = execjs.compile(j.read()) 285 | # j.close() 286 | # res = c.call('p',r1,r2,v1,v2) 287 | 288 | # print(res) -------------------------------------------------------------------------------- /code/Arsenal/coding/yandex_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/code/Arsenal/coding/yandex_demo.jpg -------------------------------------------------------------------------------- /code/Arsenal/coding/yandex_upload_demo.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import requests 4 | from lxml import etree 5 | 6 | # filePath = "C:\\path\\whateverThisIs.png" 7 | # searchUrl = 'https://yandex.com/images/' 8 | # multipart = {'encoded_image': (filePath, open( 9 | # filePath, 'rb')), 'image_content': ''} 10 | # # allow_redirects 禁止重定向 11 | # response = requests.post(searchUrl, files=multipart, allow_redirects=False) 12 | 13 | 14 | """ 15 | cbir_id=1574873%2F2RTpPjqOPRMiJu83pPYMWQ1357&uinfo=sw-1920-sh-1080-ww-1920-wh-937-pd-1-wp-16x9_1920x1080&rpt=imageview&lr=109371&family=yes 16 | 17 | 18 | https://yandex.com/images/search?family=yes&rpt=imageview&url=https://im0-tub-com.yandex.net/i?id=39ac23213f869e30e5974ad896e7b6a8&n=24&cbir_id=1574873%2F2RTpPjqOPRMiJu83pPYMWQ1357 19 | 20 | 21 | 测试:https://im0-tub-com.yandex.net/i?id=39ac23213f869e30e5974ad896e7b6a8&n=24 22 | 23 | 24 | https://yandex.ru/images/search?cbir_id=1750078%2FQ7Je-yG6ApwH7NkCB73Caw9438&uinfo=sw-1920-sh-1080-ww-1920-wh-937-pd-1-wp-16x9_1920x1080&rpt=imageview&lr=109371&family=yes 25 | """ 26 | 27 | # 参考 https://www.it1352.com/2330402.html 28 | filePath = r"D:\Code\mybot\code\Arsenal\coding\yandex_demo.jpg" 29 | searchUrl = 'https://yandex.com/images/search' 30 | files = { 31 | 'upfile': ('blob', open(filePath, 'rb'), 'image/jpeg') 32 | } 33 | params = { 34 | 'rpt': 'imageview', 35 | 'format': 'json', 36 | 'request': '{"blocks":[{"block":"b-page_type_search-by-image__link"}]}' 37 | } 38 | headers = { 39 | "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 40 | "referer":"https://yandex.com/images/", 41 | "content-type":"multipart/form-data; boundary=----WebKitFormBoundaryAhBIdySP3K1BCxLB" 42 | } 43 | 44 | 45 | resp = requests.post(searchUrl, headers=headers, params=params, files=files) 46 | # print(json.loads(resp.text)) 47 | 48 | query_string = json.loads(resp.content)['blocks'][0]['params']['url'] 49 | result_url = f"{searchUrl}?{query_string}" 50 | print(result_url) 51 | 52 | resp = requests.get(result_url, headers=headers) 53 | obj = etree.HTML(resp.text) 54 | 55 | # ======== 包含该图片的文章 ======== 56 | 57 | 58 | 59 | 60 | # ======== 相似图片 ======== 61 | # 取第一个 62 | xpath_selector = """.//div[@class='CbirSimilar-Thumbs CbirSimilar-Thumbs_justified']/div[1]/div[1]/a/div/@style""" 63 | # 取第一个和第二个 64 | # [position()=1 or position()=2] 65 | xpath_selector = """.//div[@class='CbirSimilar-Thumbs CbirSimilar-Thumbs_justified']/div[1]/div[position()=1 or position()=2]/a/div/@style""" 66 | # 取前两个 67 | # [position()<3] 68 | xpath_selector = """.//div[@class='CbirSimilar-Thumbs CbirSimilar-Thumbs_justified']/div[1]/div[position()<3]/a/div/@style""" 69 | # https://blog.csdn.net/qq_39454048/article/details/90598344 70 | text = obj.xpath(xpath_selector)[0] 71 | compile_expression = """.*?:url\("(.*?)"\);.*?""" 72 | img_url = f"http:{re.findall(compile_expression,text)[0]}" -------------------------------------------------------------------------------- /code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/code/__init__.py -------------------------------------------------------------------------------- /code/church.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : church.py 4 | @Time : 2021/05/08 18:21:37 5 | @Author : Coder-Sakura 6 | @Version : 1.1 7 | @Desc : Mybot消息-生命周期事件处理 8 | ''' 9 | 10 | # here put the import lib 11 | # import copy 12 | import json 13 | from flask import Flask, request, jsonify 14 | 15 | from executor import Executor 16 | from level_manager import Monitor 17 | from Arsenal.basic.user_data import UserData 18 | from Arsenal.basic.log_record import logger 19 | 20 | 21 | class Church: 22 | """Mybot消息-生命周期事件处理""" 23 | 24 | def hand(self,eval_cqp_data): 25 | with UserData(eval_cqp_data) as mybot_data: 26 | if not monitor_control.filter_msg(mybot_data): 27 | # mybot_data = copy.deepcopy(tool.mybot_data) 28 | executor_control.exec(mybot_data) 29 | 30 | 31 | 32 | # ================= START ================ 33 | 34 | app = Flask(__name__) 35 | church_control = Church() 36 | executor_control = Executor() 37 | monitor_control = Monitor() 38 | 39 | 40 | @app.route('/',methods=['POST']) 41 | @logger.catch 42 | def event(): 43 | cqp_push_data = request.get_data() 44 | eval_cqp_data = json.loads(cqp_push_data.decode('utf-8')) 45 | church_control.hand(eval_cqp_data) 46 | return "" 47 | 48 | # ================= END ================ 49 | 50 | 51 | if __name__ == '__main__': 52 | app.config['JSON_AS_ASCII'] = False 53 | app.run( port='5000') -------------------------------------------------------------------------------- /code/config.yaml: -------------------------------------------------------------------------------- 1 | # Bot Config 2 | Bot : 3 | # HTTP通信 4 | http : 5 | coolq_http_api_ip : "127.0.0.1" 6 | coolq_http_api_port : "5700" 7 | 8 | # Mysql连接信息 9 | mysql : 10 | db_host : "127.0.0.1" 11 | db_port : 3306 12 | db_user : "bot_admin" 13 | db_passwd : "Huawei12#$" 14 | db_database : "mybot" 15 | db_charset : "utf8mb4" 16 | 17 | # 管理员信息配置 18 | admin : 19 | uid : 3012797743 20 | 21 | 22 | # 用户管控 不建议改动 23 | Level : 24 | # 用户权限 25 | user : 26 | general : 10 27 | vip : 50 28 | admin : 999 29 | # 群组权限 30 | group : 31 | general : 10 32 | vip : 50 33 | # 普通用户调用频率限制 34 | user_limit: 35 | enable: True # or False 36 | seconds: 10 # user_limit_cycle 37 | 38 | # 默认定时任务 39 | Task : 40 | # 默认监测任务线程 41 | CYCLE_TASK_DETECT: 42 | enable: False 43 | # 随机发言插件 - 实验性 44 | BOT_RANDOM_SPEECH: 45 | enable: False 46 | # 定时图片发送 - 实验性 47 | CYCLE_IMAGE_SENDER: 48 | enable: False 49 | 50 | 51 | # Plugin Config 52 | Plugin : 53 | saucenao : 54 | api_key : "" 55 | # saucenao触发ascii2d二次搜索的最低相似度 56 | limit_similarity : 60.0 57 | # saucenao返回同人本时是否获取同人本信息 58 | add_doujin_info: True 59 | 60 | bot_img_search : 61 | # 搜图插件 - 最大消息等待时间,秒 62 | timeout : 60 63 | 64 | 65 | # Debug Config 66 | Debug : True -------------------------------------------------------------------------------- /code/dynamic_import.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : dynamic_import.py 4 | @Time : 2021/06/21 16:25:18 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import os 12 | import glob 13 | import importlib 14 | 15 | from Arsenal.basic.bot_tool import tool 16 | from Arsenal.basic.log_record import logger 17 | from Arsenal.basic.datetime_tool import datetime_now 18 | from Arsenal.basic.msg_temp import PLUGIN_BLOCK, PLUGIN_IGNORE 19 | 20 | class Dynamic_Load: 21 | """消息 - 插件解析器""" 22 | 23 | def __init__(self): 24 | # 插件路径表达式 25 | # self.pathname = "Arsenal/bot**.py" 26 | self.pathname = "Arsenal/bot**/bot**.py" 27 | # self.module_dicts = self.import_modules(self.pathname) 28 | 29 | self.error_code_list = { 30 | "NPF": "Not Plugin Found", 31 | "NHM": "Not Hit Module" 32 | } 33 | 34 | def import_modules(self, 35 | pathname:str, 36 | recursive:bool=False, 37 | class_start_with:str="Bot", 38 | reimport:bool=False 39 | )->list: 40 | """导入路径表达式下的模块 41 | 1. 导入指定路径(pathname)下的模块,返回满足指定条件的类信息 42 | 2. 被导入的插件类必须满足以下几个条件: 43 | 类是用户自定义的 44 | 类名以Bot开头 45 | 类必须包含有plugin_name属性(type(self).__name__) 46 | 类必须有一个解析函数parse 47 | 48 | :param pathname: 49 | 要导入的模块目录的相对路径,只要符合glob路径表达式写法即可 50 | :param recursive: 51 | True则迭代匹配pathname下子目录 52 | :param start_str: 53 | 条件2:返回符合以start_str开头的类 54 | :param reimport: 55 | True为重新导入插件目录下满足条件的类,False则忽略 56 | :return: 57 | 模块信息字典 58 | 59 | :exp: 重新导入 60 | import_modules(pathname,reimport=True) 61 | """ 62 | module_paths = glob.glob(pathname,recursive=recursive) 63 | logger.debug(" - Start") 64 | logger.debug(f" - 识别到{len(module_paths)}个符合规则的模块.") 65 | [logger.debug(f" - {path}") for path in module_paths] 66 | logger.debug(" - End") 67 | 68 | module_dicts = {} 69 | for path in module_paths: 70 | # Arsenal.bot_dynamic_demo1 71 | module_name = path.replace(os.sep, '.')[:-3] 72 | # 73 | module = importlib.import_module(module_name) 74 | logger.debug(f" - Try To Import {module}") 75 | 76 | # 重载 77 | if reimport: 78 | logger.warning(f" - <{module}> Reimporting!!!") 79 | module = importlib.reload(module) 80 | 81 | info = {} 82 | # element -> str 83 | for element in dir(module): 84 | if not element.startswith('__') and \ 85 | element.startswith(class_start_with) and \ 86 | hasattr(eval("module.{}".format(element)),"plugin_name") and \ 87 | hasattr(eval("module.{}".format(element)),"parse"): 88 | 89 | info[element] = eval('module.{}'.format(element)) 90 | try: 91 | info["plugin_level"] = int(eval("module.{}.plugin_level".format(element))) 92 | except Exception as e: 93 | logger.warning(f" <{eval('element')}> Not Found . Use Default PluginLevel") 94 | # 默认插件权限 95 | info["plugin_level"] = 10 96 | 97 | # 添加插件类型 98 | if info["plugin_level"] >= 10: 99 | # 主动式插件 100 | info["plugin_type"] = 1 101 | elif 1 <= info["plugin_level"] <= 9: 102 | # 被动式插件 103 | info["plugin_type"] = 0 104 | else: 105 | info["plugin_type"] = 1 106 | 107 | info["plugin_name"] = element 108 | try: 109 | info["plugin_nickname"] = eval("module.{}.plugin_nickname".format(element)) 110 | except Exception as e: 111 | info["plugin_nickname"] = "" 112 | 113 | logger.success(f" Import Plugin Success - {element}") 114 | module_dicts[element] = info 115 | # else: 116 | # logger.debug(f"Module: <{eval('module')}> Cancel Import") 117 | 118 | logger.info(f" Import Plugin Count: {len(module_dicts)}") 119 | module_dicts = dict(sorted(module_dicts.items(), key=lambda module_dicts:module_dicts[1]["plugin_type"])) 120 | return module_dicts 121 | 122 | def plugin_selector(self,mybot_data): 123 | """ 124 | 消息解析器 125 | :params mybot_data: mybot消息体 126 | :return : 127 | """ 128 | if not self.module_dicts: 129 | return self.error_code_list["NPF"] 130 | 131 | # logger.info(self.module_dicts) 132 | 133 | count = 1 134 | for module_name,module_addr in self.module_dicts.items(): 135 | logger.debug(f"[{count}/{len(self.module_dicts)}][{module_name}] =======") 136 | result = None 137 | if hasattr(module_addr[module_name],"parse"): 138 | try: 139 | # 判断群组权限是否大于等于插件权限 140 | # 判断用户权限是否大于等于插件权限 141 | pass 142 | 143 | result = module_addr[module_name].parse(mybot_data) 144 | except Exception as e: 145 | logger.debug(f" - {e}") 146 | # 异常则默认跳过 147 | result = PLUGIN_IGNORE 148 | else: 149 | logger.warning("module:{} not func:parse.Skip".format(module_name)) 150 | 151 | # 未命中解析规则或空值 152 | if result == PLUGIN_IGNORE: 153 | logger.debug(f"Miss Hit Module: {module_name} | - PLUGIN_IGNORE") 154 | count += 1 155 | continue 156 | # 解析成功后跳出 157 | elif result == PLUGIN_BLOCK: 158 | # TODO 调用机器人成功,更新last_call_date/user_call_count 159 | # 调用成功才更新last_call_date 160 | mybot_data["user_info"]["last_call_date"] = datetime_now().strftime('%Y-%m-%d %H:%M:%S') 161 | # TODO 搜图第一句之类的提示语会消耗次数,待后续调整 162 | mybot_data["user_info"]["user_call_count"] += 1 163 | tool.db.update_records(**{ 164 | "update_data": mybot_data["user_info"], 165 | "judge_data": {"uid": mybot_data["sender"]["user_id"], "gid": mybot_data["sender"]["group_id"]} 166 | }) 167 | logger.success(f"Hit Module: {module_name} | - PLUGIN_BLOCK") 168 | count += 1 169 | break 170 | # 意料之外的值 171 | else: 172 | logger.debug(f"Warning Module: {module_name} | - {result}") 173 | count += 1 174 | continue 175 | else: 176 | logger.warning(f"Not Hit Any Modules | - {mybot_data}") 177 | return self.error_code_list["NHM"] 178 | 179 | # TEST for dynamic import 180 | def main(self): 181 | self.module_dicts = self.import_modules(self.pathname) 182 | while True: 183 | command = input("\nInput the Command or q to Quit: ") 184 | if command == "q": 185 | exit() 186 | elif command == ".reimport": 187 | self.module_dicts = self.import_modules(self.pathname,reimport=True) 188 | logger.success(self.module_dicts) 189 | elif command == "modules": 190 | logger.success(self.module_dicts) 191 | else: 192 | result = self.plugin_selector(command) 193 | logger.success(result) 194 | 195 | modules_dynamicLoad = Dynamic_Load() 196 | 197 | # if __name__ == "__main__": 198 | # modules_dynamicLoad.main() -------------------------------------------------------------------------------- /code/executor.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : executor.py 4 | @Time : 2022/01/13 15:44:25 5 | @Author : Coder-Sakura 6 | @Version : 1.1 7 | @Desc : None 8 | ''' 9 | 10 | # here put the import lib 11 | import time 12 | import json 13 | 14 | from dynamic_import import modules_dynamicLoad 15 | from Arsenal.basic.bot_tool import tool 16 | from Arsenal.basic.thread_pool import ThreadPool, callback 17 | from Arsenal.basic.task_processor import taskprocessor 18 | from Arsenal.basic.log_record import logger 19 | from Arsenal.basic.msg_temp import MYBOT_ERR_CODE, EXECUTOR_TASK_STATUS_INFO, TASK_PROCESSOR_TEMP 20 | 21 | class Executor: 22 | """事件处理:插件轮询/热重启""" 23 | def __init__(self): 24 | # 为tool创建线程池pool 25 | self.init_thread_pool(8) 26 | 27 | # 启动定时任务检测线程 - 实验性 28 | # tool.pool.put(self.cycle_task_detect, (), callback) 29 | 30 | # 消息 - 插件解析器初始化 31 | try: 32 | modules_dynamicLoad.module_dicts = modules_dynamicLoad.import_modules(modules_dynamicLoad.pathname) 33 | tool.modules_dynamicLoad = modules_dynamicLoad 34 | except Exception as e: 35 | logger.warning(MYBOT_ERR_CODE["Generic_Exception_Info"].format(e)) 36 | logger.warning("模块导入插件出错,请检查模块或对应插件") 37 | 38 | # 数据库预处理 39 | DBCheck() 40 | 41 | 42 | # thread_pool func start 43 | def init_thread_pool(self, max_num=8)->bool: 44 | """ 45 | 初始化线程池,暂不考虑reload情况 46 | < -- not reliable! -- > 47 | """ 48 | try: 49 | if hasattr(tool,"pool"): 50 | tool.pool.terminal = True 51 | logger.info("Reload Thread Pool In Short Time.") 52 | time.sleep(0.1) 53 | # time.sleep(3) 54 | logger.info("Init Thread Pool In Short Time.") 55 | except Exception as e: 56 | logger.debug(MYBOT_ERR_CODE.format(e)) 57 | logger.warning("Init Thread Pool Fail.") 58 | return False 59 | finally: 60 | # 重置中断标志 61 | tool.pool = ThreadPool(max_num=max_num) 62 | tool.pool.terminal = False 63 | logger.info("Init Thread Pool Success.") 64 | return True 65 | 66 | @logger.catch 67 | def cycle_task_detect(self, cycle=10): 68 | """ 69 | 定时(秒)监测任务并将任务放入线程池执行 70 | :params cycle: 监测周期,默认10秒检查一次 71 | """ 72 | # 通过terminal来强制中断 73 | while True: 74 | logger.debug(TASK_PROCESSOR_TEMP["TASK_START"]) 75 | if tool.pool.terminal: 76 | logger.info(TASK_PROCESSOR_TEMP["TASK_BREAK_TERMINAL"]) 77 | break 78 | 79 | tasks_list = taskprocessor.get_tasks() 80 | if not tasks_list: 81 | logger.debug(TASK_PROCESSOR_TEMP["TASK_NO_RECORD"]) 82 | else: 83 | tasks_count_info = taskprocessor.task_count(tasks_list) 84 | [logger.info(EXECUTOR_TASK_STATUS_INFO["info"].format(i,j)) for i,j in tasks_count_info.items()] 85 | 86 | for task in tasks_list: 87 | tool.pool.put(taskprocessor.exec_tasks, (task,), callback) 88 | 89 | logger.debug(TASK_PROCESSOR_TEMP["TASK_END"].format(cycle)) 90 | time.sleep(cycle) 91 | 92 | # thread_pool func end 93 | 94 | def reload_modules(self, reimport=True)->dict: 95 | """ 96 | 加载插件目录插件 97 | :params reimport: True -> 重新导入 98 | :params pathname: todo 99 | :params recursive: todo 100 | :params class_start_with: todo 101 | """ 102 | return tool.modules_dynamicLoad.import_modules(reimport=reimport) 103 | 104 | def exec(self, mybot_data): 105 | """ 106 | 插件解析器解析消息,以匹配出结果 107 | """ 108 | logger.debug(f" - {mybot_data}") 109 | msg = mybot_data["arrange"].get("message", "") 110 | if msg: 111 | try: 112 | tool.modules_dynamicLoad.plugin_selector(mybot_data) 113 | except Exception as e: 114 | logger.warning(MYBOT_ERR_CODE["Generic_Exception_Info"].format(e)) 115 | else: 116 | # status = tool.auto_report_err(msg) 117 | logger.warning(f" not found - {mybot_data}") 118 | 119 | class DBCheck: 120 | """数据库信息预处理""" 121 | def __init__(self): 122 | self.group_check() 123 | self.plugin_check() 124 | 125 | def group_check(self): 126 | """群组信息预处理""" 127 | params = {} 128 | group_info = tool.send_cq_client(params=params, api="cq_get_group_list") 129 | 130 | if not group_info or not group_info["data"]: 131 | logger.warning(f" - {group_info}") 132 | else: 133 | for _ in group_info["data"]: 134 | if not tool.db.select_records(table="group_chats", **{"gid": _["group_id"]}): 135 | insert_data = { 136 | "gid": int(_["group_id"]), 137 | "group_level": int(tool.level["general_group_level"]), 138 | "is_qqBlocked": 0 139 | } 140 | 141 | tool.db.insert_records( 142 | table="group_chats", 143 | **{"insert_data": insert_data} 144 | ) 145 | 146 | result = tool.db.select_records(table="group_chats") 147 | logger.info(f"数据库群组: {len(result)}个") 148 | logger.debug(f"group_chats - {result}") 149 | 150 | def plugin_check(self): 151 | """插件信息预处理""" 152 | copy_module_dicts = tool.modules_dynamicLoad.module_dicts 153 | logger.debug(f" - {copy_module_dicts}") 154 | 155 | if not copy_module_dicts: 156 | logger.warning(f" - {copy_module_dicts}") 157 | return 158 | 159 | for module_name,module_addr in copy_module_dicts.items(): 160 | if not tool.db.select_records(table="plugin_info", **{"plugin_name": module_name}): 161 | insert_data = { 162 | "plugin_name": module_name, 163 | "plugin_nickname": module_addr["plugin_nickname"], 164 | "plugin_type": module_addr["plugin_type"], 165 | "plugin_level": module_addr["plugin_level"], 166 | # 默认运行中 - 0 167 | "plugin_status": 0, 168 | # 默认无限制规则 - {} 169 | "plugin_limit_info": json.dumps({}) 170 | } 171 | 172 | tool.db.insert_records( 173 | table="plugin_info", 174 | **{"insert_data": insert_data} 175 | ) 176 | 177 | result = tool.db.select_records(table="plugin_info") 178 | logger.debug(f"plugin_info - {result}") 179 | 180 | logger.info(f"数据库插件: {len(result)}个") 181 | logger.info(f"本地插件: {len(copy_module_dicts)}个") 182 | -------------------------------------------------------------------------------- /code/level_manager.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : level_manager.py 4 | @Time : 2021/11/30 15:18:46 5 | @Author : Coder-Sakura 6 | @Version : 1.0 7 | @Desc : 权限管理 8 | ''' 9 | 10 | # here put the import lib 11 | import random 12 | 13 | from Arsenal.basic.log_record import logger 14 | from Arsenal.basic.bot_tool import tool 15 | from Arsenal.basic.datetime_tool import datetime_now, datetime_offset, str2datetime 16 | from Arsenal.basic.msg_temp import USER_MSG_TEMP,USER_LIMIT_TEMP,DB_TEMP 17 | 18 | 19 | class Monitor: 20 | """用户/群组 的权限 查看/更新""" 21 | def __init__(self): 22 | self.judge_data = {} 23 | self.db_update_judge_data = {} 24 | self.wait_seconds = 1 25 | 26 | def filter_msg(self, mybot_data): 27 | """ 28 | 是否过滤消息 29 | :paramas mybot_data: mybot内部消息结构 30 | :return : 是 - True / 否 - False 31 | """ 32 | if not mybot_data: 33 | return True 34 | 35 | uid = mybot_data["sender"]["user_id"] 36 | gid = mybot_data["sender"]["group_id"] 37 | message = mybot_data["arrange"]["message"] 38 | self.db_update_judge_data = {"uid": uid, "gid": gid} 39 | 40 | # with UserData(**self.judge_data) as mybot_data: 41 | # tool.mybot_data = mybot_data 42 | 43 | # 屏蔽 / 黑名单 44 | if mybot_data["user_info"]["is_qqBlocked"] == DB_TEMP["is_qqBlocked"]: 45 | if mybot_data["sender"]["type"] == "group": 46 | logger.info(USER_MSG_TEMP["qqBlocker_group_msg"].format(gid,uid,message)) 47 | elif mybot_data["sender"]["type"] == "private": 48 | logger.info(USER_MSG_TEMP["qqBlocker_user_msg"].format(uid,message)) 49 | return True 50 | 51 | # 普通用户调用频率限制 52 | if tool.user_limit_flag: 53 | _ = self.user_limit(mybot_data) 54 | if not _: 55 | if mybot_data["sender"]["type"] == "group": 56 | logger.info(USER_MSG_TEMP["general_group_msg"].format(gid,uid,message)) 57 | elif mybot_data["sender"]["type"] == "private": 58 | logger.info(USER_MSG_TEMP["general_user_msg"].format(uid,message)) 59 | else: 60 | logger.info(USER_MSG_TEMP["general_unknown_msg"].format(gid,uid,message)) 61 | return False 62 | elif _: 63 | # 向用户发送提示 64 | self._limit_prompt_info(mybot_data) 65 | return True 66 | 67 | # 防止无返回值 默认不过滤False 68 | if mybot_data["sender"]["type"] == "group": 69 | logger.info(USER_MSG_TEMP["general_group_msg"].format(gid,uid,message)) 70 | elif mybot_data["sender"]["type"] == "private": 71 | logger.info(USER_MSG_TEMP["general_user_msg"].format(uid,message)) 72 | else: 73 | logger.info(USER_MSG_TEMP["general_unknown_msg"].format(gid,uid,message)) 74 | return False 75 | 76 | def user_limit(self, mybot_data): 77 | """ 78 | 普通用户调用频率限制,判断是否对其限制 79 | :params mybot_data: mybot消息体 80 | :return: 是 - True or 否 - False 81 | """ 82 | now_time = datetime_now() 83 | 84 | # 高级用户 85 | if int(mybot_data["user_info"]["user_level"]) > int(tool.level["general_user_level"]): 86 | return False 87 | 88 | # 第一次插入数据时,cycle_expiration_time为str类型 89 | # 后续为datetime.datetime,需要进行转换 90 | cycle_expiration_time = mybot_data["user_info"]["cycle_expiration_time"] 91 | if type(cycle_expiration_time) != type(now_time): 92 | cycle_expiration_time = str2datetime(cycle_expiration_time) 93 | 94 | # 更新cycle_expiration_time 95 | if now_time > cycle_expiration_time: 96 | future_time = datetime_offset(now_time, tool.config["Level"]["user_limit"]["seconds"]) 97 | mybot_data["user_info"]["user_call_count"] = 0 98 | mybot_data["user_info"]["cycle_expiration_time"] = future_time 99 | tool.db.update_records(**{ 100 | "update_data": mybot_data["user_info"], 101 | "judge_data": self.db_update_judge_data 102 | }) 103 | return False 104 | 105 | # 调用超出限制 106 | if int(mybot_data["user_info"]["user_call_count"]) >= int(mybot_data["user_info"]["user_limit_count"]): 107 | self.wait_seconds = (mybot_data["user_info"]["cycle_expiration_time"] - now_time).seconds 108 | return True 109 | 110 | def _limit_prompt_info(self, mybot_data): 111 | """发送提示,请稍后再试""" 112 | mybot_data["message"] = USER_LIMIT_TEMP[random.choice(list(USER_LIMIT_TEMP))].format(self.wait_seconds) 113 | status = tool.auto_send_msg(mybot_data) 114 | if not status: 115 | logger.warning(f"发送<用户调用限制>提示信息失败") -------------------------------------------------------------------------------- /code/version: -------------------------------------------------------------------------------- 1 | 0.18.0 -------------------------------------------------------------------------------- /code/更新日志.md: -------------------------------------------------------------------------------- 1 | 2 | 1. 升级搜图功能:添加ascii2d功能 3 | 4 | 2. jiki词典 5 | 6 | 3. 彩虹屁API及祖安模式 7 | 8 | 4. 升级来点涩图功能: 9 | 1. 增加与mysql数据库交互以达到更多来点涩图及标签分类 10 | 2. 增加/减少 id,下载或增加对应的pixiv作品到本地 11 | 12 | ========日志========== 13 | 2020.4.11 14 | 机器人开始运行 15 | 16 | 2020.4.15左右增加功能"来点涩图" 17 | 根据本地图片名字,返回对应的反代链接 18 | 19 | 2020.5.4 20 | 修复功能"来点涩图"Bug一个 21 | 主要反代两位数页码的图片出现错误,最后导致404 -------------------------------------------------------------------------------- /doc/sql/mybot.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS mybot DEFAULT CHARSET utf8mb4; 2 | -- user表 3 | CREATE TABLE users( 4 | id int AUTO_INCREMENT PRIMARY KEY, 5 | uid INT(20) NOT NULL, 6 | gid INT(20) NOT NULL, 7 | user_level INT(5) NOT NULL, 8 | user_limit_cycle INT(3) NOT NULL, 9 | user_limit_count INT(3) NOT NULL, 10 | user_call_count INT(3) NOT NULL, 11 | magic_thing INT(8) NOT NULL, 12 | is_qqBlocked TINYINT(1) NOT NULL, 13 | create_date DATETIME NOT NULL, 14 | last_call_date DATETIME NOT NULL, 15 | cycle_expiration_time DATETIME NOT NULL 16 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 17 | 18 | -- group_chats表 19 | CREATE TABLE group_chats( 20 | id INT AUTO_INCREMENT PRIMARY KEY, 21 | gid INT(20) NOT NULL, 22 | group_level INT(5) NOT NULL, 23 | is_qqBlocked TINYINT(1) NOT NULL 24 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 25 | 26 | -- messages表 27 | CREATE TABLE messages( 28 | id INT AUTO_INCREMENT PRIMARY KEY, 29 | message_type VARCHAR(20) NOT NULL, 30 | uid INT(20) NOT NULL, 31 | gid INT(20) NOT NULL, 32 | content VARCHAR(1024) NOT NULL, 33 | md5 VARCHAR(40) NOT NULL, 34 | message_datetime DATETIME NOT NULL 35 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 36 | 37 | -- plugin_info表 38 | CREATE TABLE plugin_info( 39 | id INT AUTO_INCREMENT PRIMARY KEY, 40 | plugin_name VARCHAR(40) NOT NULL, 41 | plugin_nickname VARCHAR(40) NOT NULL, 42 | plugin_type INT(3) NOT NULL, 43 | plugin_level INT(5) NOT NULL, 44 | plugin_status INT(3) NOT NULL, 45 | plugin_limit_info JSON NOT NULL 46 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 47 | 48 | -- tasks表 49 | CREATE TABLE tasks( 50 | id INT AUTO_INCREMENT PRIMARY KEY, 51 | creator_id VARCHAR(20) NOT NULL, 52 | gid VARCHAR(20) NOT NULL, 53 | task_type VARCHAR(30) NOT NULL, 54 | by_plugin VARCHAR(30) NOT NULL, 55 | create_time DATETIME NOT NULL, 56 | task_status VARCHAR(20) NOT NULL, 57 | task_level INT(5) NOT NULL, 58 | exec_task VARCHAR(1024) NOT NULL, 59 | exec_time DATETIME NOT NULL, 60 | report_user_id VARCHAR(20) NOT NULL, 61 | report_group_id VARCHAR(20) NOT NULL 62 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 63 | 64 | 65 | SELECT @@sql_mode; -------------------------------------------------------------------------------- /test/Readme.md: -------------------------------------------------------------------------------- 1 | ### 测试环境或测试脚本存放 -------------------------------------------------------------------------------- /test/test_bot_day_illust.py: -------------------------------------------------------------------------------- 1 | # day_illust_process 2 | # 方便复制 3 | from bot_day_illust import Bot_Day_Illust 4 | 5 | eval_cqp_data = {"group_id": 1072957655,"user_id":4,"message_type":"group"} 6 | # eval_cqp_data = {"group_id": 1072957655,"user_id":1695210915,"message_type":"group"} 7 | Bot_Day_Illust.day_illust_process(eval_cqp_data) 8 | 9 | # day_illust_process 10 | # ========== get_rare_value ========== 11 | from bot_day_illust import Bot_Day_Illust 12 | Bot_Day_Illust.get_rare_value(0.8,limit_prob=0.7) 13 | # "" 14 | Bot_Day_Illust.get_rare_value(0.115,limit_prob=0.7) 15 | # SR 16 | Bot_Day_Illust.get_rare_value(1.115,limit_prob=0.7) 17 | # UR 18 | Bot_Day_Illust.get_rare_value(1.7) 19 | # UR 20 | Bot_Day_Illust.get_rare_value(0.7) 21 | # R 22 | Bot_Day_Illust.get_rare_value(0.2,rare_set={"lost":0.05,"get":0.1,"normal":0.85}) 23 | Bot_Day_Illust.get_rare_value(1.2,rare_set={"lost":0.05,"get":0.1,"normal":0.85}) 24 | 25 | """ 26 | limit prob = 0 27 | pool_rare_set_sum = 1 28 | random = 0.1/1.2 29 | 30 | limit prob = 0.7 31 | pool_rare_set_sum = 0.3 32 | random = 0.1/0.8/1.2 33 | """ 34 | # ========== get_rare_value ========== 35 | 36 | # ========== get_additional_prob ========== 37 | from bot_day_illust import Bot_Day_Illust 38 | Bot_Day_Illust.get_additional_prob(int("10")) 39 | Bot_Day_Illust.get_additional_prob(int(10)) 40 | # ========== get_additional_prob ========== 41 | 42 | # ========== safe2pid_error ========== 43 | from bot_day_illust import Bot_Day_Illust 44 | Bot_Day_Illust.safe2pid_error(illust_level="R") 45 | Bot_Day_Illust.safe2pid_error(illust_level="SR") 46 | Bot_Day_Illust.safe2pid_error(illust_level="SSR") 47 | Bot_Day_Illust.safe2pid_error(illust_level="UR") 48 | # ========== safe2pid_error ========== 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/test_db_pool.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WriteCode-ChangeWorld/mybot/56853fa560472f02734b94616ce34c134c6f0457/test/test_db_pool.py --------------------------------------------------------------------------------