├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── ACTION.md ├── CLIENT.md ├── CONFIG.md ├── DECORATORS.md ├── INSTALL.md ├── MACRO.md ├── Makefile ├── OTHER.md ├── PLUGIN.md ├── QUICKSTART.md ├── SAMPLE.md ├── SCAFFOLD.md ├── STATEMENTS.md ├── TIPS.md ├── WEBHOOK.md ├── _sidebar.md ├── conf.py ├── index.html ├── index.rst └── make.bat ├── iotbot ├── __init__.py ├── __main__.py ├── action.py ├── cli.py ├── client.py ├── config.py ├── decorators.py ├── exceptions.py ├── logger.py ├── macro.py ├── model.py ├── plugin.py ├── refine.py ├── sugar.py ├── template.py ├── typing.py ├── utils.py ├── version.py └── webhook.py ├── publish.sh ├── sample ├── README.md ├── bot.py └── plugins │ ├── README.md │ ├── bot_163_comment.py │ ├── bot_auto_repeat.py │ ├── bot_auto_revoke.py │ ├── bot_cmd.py │ ├── bot_event.py │ ├── bot_morning.py │ ├── bot_phlogo.py │ ├── bot_pic.py │ ├── bot_qrcode.py │ ├── bot_replay.py │ ├── bot_send_local_image.py │ ├── bot_setu.py │ ├── bot_setu_v2.py │ ├── bot_stop_revoke.py │ ├── bot_sysinfo.py │ ├── bot_test_middleware.py │ ├── bot_test_queue.py │ ├── bot_test_refine_funcs.py │ ├── bot_to_card.py │ ├── bot_verse.py │ └── bot_whatis.py ├── setup.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | test.py 132 | test_plugins 133 | .iotbot.json 134 | 135 | .REMOVED_PLUGINS 136 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 0.2.3 - 2020-05-15 4 | 5 | 1. 更多 action 6 | 7 | 2. 每个 action 除默认参数外,还可设置: 8 | - `api_path` `default=/v1/LuaApiCaller` 9 | - `iot_timeout` `default=self.timeout=10` IOTBOT 端处理允许等待的时间 10 | - `bot_qq` `default=self.qq` 机器人 QQ 号 11 | 12 | ### 1.0.0 - 2020-05-28 13 | 14 | #### 大改动 15 | 16 | 1. 插件化 17 | 2. 效率更高,不漏消息 18 | 3. 更多快捷方法 19 | 4. 更多自定义参数 20 | 21 | ### 1.1.0 - 2020-06-19 22 | 23 | 1. 无需重启即可更新插件,正常调用`refresh_plugins`方法即可 24 | 2. 改了下刷新插件后的显示信息 25 | 3. 增加刷新 key 二次登陆 Action 26 | 4. 改善了生成模板 27 | 28 | ### 2.0.0 29 | 30 | 1. 支持队列发送 31 | 2. 支持中间件,可用于传递配置 32 | 3. 优化数据解析,提供解析更详细的函数 33 | 4. ... 34 | 35 | ### 2.1.0 36 | 37 | 1. 增加 webhook 功能,方便做远程服务 38 | 2. 废弃环境变量的配置方式,使用`.iotbot.json`进行配置 39 | 3. sugar 发送图片函数增加文字参数 40 | 41 | ### 2.2.0 42 | 43 | 1. 优化插件管理 44 | 45 | ### 2.2.1 46 | 47 | 1. 优化中间件的处理 48 | 2. 好友白名单改为好友黑名单 49 | 3. 配置文件增加群、好友黑名单配置项 50 | 4. Action 增加部分方法 51 | 52 | ### 2.3.1 53 | 54 | 1. 使用第三方库替代原来手动配置的 logger,日志不那么粗糙了 55 | 2. 移除 Action 中的设置日志参数 56 | 57 | ### 2.3.2 58 | 59 | 1. windows 上编码错误 60 | 61 | ### 2.3.4 62 | 63 | 1. 新增设置/取消管理员 Action 64 | 2. Action 对象的 host,port...等属性改为公开属性 65 | 3. 发送请求使用 session 66 | 4. GroupAdminsysnotifyEventMsgQQ 群系统消息通知消息完善 67 | ... 68 | 69 | ### 2.4.0 70 | 71 | 1. 移除 Action 每分钟限制发送频率的功能 72 | 2. 优化发送队列 73 | 74 | ### 2.4.1 75 | 76 | 1. action 检查 Ret 值时首先判断该字段是否存在 77 | 78 | ### 2.4.2 79 | 80 | 1. 封装获取包括群主在内的管理员方法 81 | 82 | ### 2.5.0 83 | 84 | 1. 优化插件管理,使用文件存储停用的插件信息 85 | 2. 添加 `is_botself`装饰器,只接收机器人自身消息 86 | 3. 去除冗余代码 87 | 88 | ### 2.5.1 89 | 90 | 1. 封装转发视频给群(repost_video_to_group)/好友(repost_video_to_friend)两个 action 91 | 2. 增加解析视频消息的 refine 函数 92 | 93 | ### 2.5.2 94 | 95 | 1. 修正误将 GetWebConn 作为心跳的错误 96 | 97 | ### 2.5.3 98 | 99 | 1. refine 函数不修改原上下文对象 100 | 101 | ### 2.6.0 102 | 103 | 1. 添加定时任务功能 104 | 105 | ## 2.6.1 106 | 107 | 1. 增加对发送错误码的描述 108 | 109 | ## 2.6.2 110 | 111 | 1. 优化 sugar 函数,增加对临时会话的支持 112 | 2. 好友消息增加字段 TempUin(临时会话的入口群聊 id) 113 | 3. refine 图片消息后对象的 GroupPic 列表成员由原始字典变为单独的图片对象 114 | 115 | ## 2.6.4 116 | 117 | 1. 增加辅助构建宏模块 118 | 2. 对旧版 atUser 参数做兼容 119 | 3. 模块 refine_message 更名为 refine 120 | 121 | ### 2.7.0 122 | 123 | 1. 可添加连接成功或断开连接后执行的钩子函数 124 | 125 | ### 2.7.1 126 | 127 | 1. [commit](https://github.com/xiyaowong/python--iotbot/commit/78820ac0134d24ae337a64e5c25d2738815c209e) 128 | 129 | ### 2.7.2 130 | 131 | 1. 增加接收函数装饰器(startswith) 132 | 2. 修改接收函数装饰器`equal_content`逻辑 133 | 134 | ### 2.7.3 135 | 136 | 1. 装饰器 equal_content 少一步对 MsgType 的判断 137 | 138 | ### 2.7.4 139 | 140 | 1. 配置方面更新 141 | 2. 小部分兼容 6.0.0 新功能 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wongxy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot端更新太快,该项目已不适用,请移步[botoy](https://github.com/xiyaowong/botoy) 2 | # python-iotbot 3 | 4 | [![pypi](https://img.shields.io/pypi/v/python-iotbot?style=flat-square 'pypi')](https://pypi.org/project/python-iotbot/) 5 | [![python-version](https://img.shields.io/pypi/pyversions/python-iotbot?style=flat-square)](https://pypi.org/project/python-iotbot/) 6 | 7 | ## Install 8 | 9 | ```shell 10 | pip install python-iotbot -i https://pypi.org/simple --upgrade 11 | ``` 12 | 13 | ## Quick Start 14 | 15 | ```python 16 | from iotbot import IOTBOT, GroupMsg 17 | 18 | bot = IOTBOT(your_bot_qq) 19 | 20 | 21 | @bot.on_group_msg 22 | def group(ctx: GroupMsg): 23 | print(f""" 24 | {ctx.FromNickName}在{ctx.MsgTime}的时候,发了一个类型是{ctx.MsgType}的消息,内容为: 25 | {ctx.Content}""") 26 | print(ctx.CurrentQQ) 27 | 28 | 29 | bot.run() 30 | ``` 31 | 32 | [documentation](https://xiyaowong.github.io/python--iotbot 'documentation') 33 | 34 | ## [CHANGELOG](./CHANGELOG.md) 35 | 36 | ## LICENSE 37 | 38 | MIT 39 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiyaowong/python--iotbot/9963c880d8bbd61a52ccba3b22a08d7294741baa/docs/.nojekyll -------------------------------------------------------------------------------- /docs/ACTION.md: -------------------------------------------------------------------------------- 1 | # 动作 2 | 3 | ``` 4 | 这个类封装了大部分webapi, 并添加了部分常用方法 5 | ``` 6 | 7 | ## 初始化 8 | 9 | ```python 10 | from iotbot import Action 11 | 12 | action = Action(123456) 13 | ''' 14 | 必选参数: 15 | qq_or_bot: qq 号或者机器人实例(`IOTBOT`), 如果传入机器人实例,如果开启多Q,将选取第一个 QQ 16 | 17 | 可选参数: 18 | 与发送队列有关的参数: 19 | queue: 队列开关 20 | queue_delay: 队列延时 21 | ------ 22 | timeout: 发送请求等待响应的时间 23 | api_path: 24 | port: 25 | host: 26 | ''' 27 | ``` 28 | 29 | **每个方法有完善的代码提示,参数命名也对应原 api 数据命名,所以不说明了。对于参数的疑问请先查询 iotqq 提供 webapi 文档 30 | 如果还有疑问,请查看源码** 31 | 32 | ## Action.baseSender 33 | 34 | 所有方法调用这个基础方法 35 | 36 | 开放出来是因为有时候出了新功能,可以用这个自行构建请求。 37 | 参数: 38 | 39 | - method: 请求方法, 如 POST、GET 40 | - funcname: 请求类型, 即 iotbot webapi 路径中的参数 41 | - data: post 的数据, 不需要就不传 42 | - timeout: 发送请求等待响应的时间(requests) 43 | - api_path: 默认为/v1/LuaApiCaller 44 | - iot_timeout: IOT 端处理请求等待的时间, 即 iotbot webapi 路径中的参数, 具体意思自行去机器人框架了解 45 | - bot_qq: 机器人 QQ 46 | 47 | ## 方法一览 48 | 49 | **表格显示不出可以直接在 github 上看** 50 | 51 | | 名称 | 作用 | 52 | | ------------------------ | ------------------------------------------------- | 53 | | send_friend_text_msg | 发送好友文本消息 | 54 | | get_user_list | 获取好友列表 | 55 | | send_friend_voice_msg | 发送好友语音消息 | 56 | | send_friend_pic_msg | 发送好友图片消息 | 57 | | send_group_text_msg | 发送群文字消息 | 58 | | send_group_voice_msg | 发送群语音 | 59 | | send_group_pic_msg | 发送群图片 | 60 | | send_private_text_msg | 发送私聊文字消息 | 61 | | send_private_voice_msg | 发送私聊语音 | 62 | | send_private_pic_msg | 发送私聊图片 | 63 | | send_group_json_msg | 发送群 Json 类型信息 | 64 | | send_group_xml_msg | 发送群 Xml 类型信息 | 65 | | revoke_msg | 撤回消息 | 66 | | search_group | 搜索群组 | 67 | | get_user_info | 获取用户信息 | 68 | | get_cookies | 获取 cookies | 69 | | get_group_list | 获取群聊列表 | 70 | | get_group_user_list | 获取群成员列表 | 71 | | get_group_admin_list | 获取群管理列表 | 72 | | get_group_all_admin_list | 获取包括群主在内的所有管理员列表 | 73 | | set_unique_title | 设置群成员头衔 | 74 | | modify_group_card | 修改群名片 | 75 | | refresh_keys | 刷新 key 二次登陆, 成功返回 True, 失败返回 False | 76 | | add_friend | 添加好友 | 77 | | deal_friend | 处理好友请求 | 78 | | all_shut_up_on | 开启全员禁言 | 79 | | all_shut_up_off | 关闭全员禁言 | 80 | | you_shut_up | 群成员禁言 | 81 | | like | 通用 call 点赞 | 82 | | like_2 | 点赞 | 83 | | logout | 退出 qq | 84 | | get_login_qrcode | 获取登录二维码的 base64 编码 | 85 | | get_friend_file | 获取好友文件下载链接 | 86 | | get_group_file | 获取群文件下载链接 | 87 | | set_group_announce | 设置群公告 | 88 | | set_group_admin | 设置群管理员 | 89 | | cancel_group_admin | 取消群管理员 | 90 | | repost_video_to_group | 转发视频到群聊 | 91 | | repost_video_to_friend | 转发视频给好友 | 92 | 93 | ## 发送队列 94 | 95 | 发送过快会导致发送失败或消息被 tx 屏蔽, 所以某些情况很有必要开启,特别是发图 96 | 97 | 初始化`Action`时设置参数`queue`为`True`即可以队列的方式执行发送任务,开启后对应有两个参数可以设置 98 | 99 | `queue_delay` 队列每一次发送之间的延时,一般保持默认即可,这是群内大佬的经验数值 100 | 101 | **注意**: 102 | 103 | 1. 开启队列后方法都没有返回值,所以只适合执行发送任务。 104 | 105 | (v2.4.0 增加)在使用开启队列的 action 运行各种方法时,可指定关键字参数`callback`, 106 | callback 要求为一个函数,函数有且只能有一个参数,之后会自动将队列中任务的返回值传说 callback 执行 107 | 108 | 没有对不同的群和 api 进行分开发送,即所有操作都会排入队列中, 这样也符合真人行为(个人觉得) 109 | 110 | 2. Action 必须定义为**全局变量**,不能放在接收函数内,这个其实不应该说明的,因为放在函数内会导致的问题显而易见 111 | 3. 参考[bot_test_queue](https://github.com/XiyaoWong/python-iotbot/blob/master/sample/plugins/bot_test_queue.py) 112 | 113 | ## sugar 114 | 115 | 在 action 的基础上深度封装了常用操作, 适用于**简单**的场景,不支持队列发送 116 | 使用的前提是使用默认的端口和 ip 配置或者已经设置好文件`.iotbot.json`,这个是什么在配置章节介绍 117 | 118 | ```python 119 | from iotbot.sugar import Text, Picture, Voice, Send 120 | 121 | Text('Hello') # 对该消息的来源(群或好友),发送内容为Hello的文字消息 122 | Picture(pic_url='') # 同上,这里是发送图片消息 123 | Voice(...) 124 | ... 125 | ``` 126 | 127 | 调用这几个方法,会自动选择上下文对进行不同的回复,包括临时会话 128 | 129 | 具体参数看代码提示, 如果提示不全请看源码注释! 130 | 131 | 这几个函数**只能**在群消息和好友消息接收函数中使用 132 | 133 | ## Tips 134 | 135 | 因为 Action 和 IOTBOT 136 | 实例完全解耦,所以你可以在自己的脚本中使用,比如用这个替代 137 | shell 替代定时脚本 138 | -------------------------------------------------------------------------------- /docs/CLIENT.md: -------------------------------------------------------------------------------- 1 | # 客户端(IOTBOT) 2 | 3 | ## 初始化 4 | 5 | ```python 6 | from iotbot import IOTBOT 7 | 8 | bot = IOTBOT(123456) 9 | ''' 10 | 必选参数: 11 | qq: 机器人QQ号, 多Q传入列表即可 12 | 13 | 可选参数: 14 | ues_plugins: 是否开启插件功能 15 | plugin_dir: 插件目录(同级目录) 16 | group_blacklist: 群黑名单, 此名单中的群聊消息不会被处理,默认为空 17 | friend_whitelist: 好友黑名单,此名单中的群聊消息不会被处理,默认为空 18 | log: 是否开启log, ×这块没写好,日志功能很糟糕×现已用三方库替代,稍微好一点点了 19 | log_file: 是否输出日志文件 20 | port: bot端运行端口 21 | host: bot端运行ip,需要包含schema 22 | ''' 23 | ``` 24 | 25 | ## 注册接收函数 26 | 27 | 在收到服务端消息后,首先将原始消息进行包装,然后将包装后的消息(后称上下文 ctx)作为参数传入每个消息接受函数(receiver)自动调用 28 | 29 | ### 通过方法添加接收函数 30 | 31 | - `bot.add_group_msg_receiver(func)` 添加群消息接收函数 32 | - `bot.add_friend_msg_receiver(func)` 添加好友消息接收函数 33 | - `bot.add_event_receiver(func)` 添加事件消息接收函数 34 | 35 | func 是对应的接收函数, 有自己编写,参数唯一,均为对应消息类型的**上下文对象** 36 | 37 | ### 通过装饰器添加接收函数 38 | 39 | - @bot.on_group_msg # 群消息 40 | - @bot.on_friend_msg # 好友消息 41 | - @bot.on_event # 事件消息 42 | 43 | 装饰器是调用上面的方法 44 | 45 | **接收函数数量和命名都不受限制**你可以针对不同的功能使用不同的接收函数 46 | 47 | ### 通过插件添加接收函数 48 | 49 | 在插件部分说明 50 | 51 | ## 连接或断开连接的钩子函数 52 | 53 | 有时候会有在连接成功后进行一些配置初始化或断开连接需要执行某些动作的需求,所以提供了两个装饰器可用于设置这两个事件的钩子函数 54 | 55 | 1. `bot.when_connected` 连接成功后执行 56 | 2. `bot.when_disconnected` 断开连接后执行 57 | 58 | ```python 59 | @bot.when_connected 60 | def connected(): 61 | print('该函数只在程序启动后第一次连接成功后执行, 不能有参数') 62 | 63 | @bot.when_disconnected 64 | def disconnected(): 65 | print('该函数只在第一次断开连接后才执行, 不能有参数') 66 | ``` 67 | 68 | 因为有自动重连机制,默认情况下这两个函数都只会执行一次,如果需要每一次连接成功(重连成功)或断开都执行的话,可设置参数`every_time`为`True` 69 | 70 | ```python 71 | @bot.when_connected(every_time=True) 72 | def connected(): 73 | print('该函数只要连接成功后就会执行一次, 不能有参数') 74 | # disconnected 同理 75 | ``` 76 | 77 | ## 消息上下文对象 78 | 79 | 接收函数必须有且只有一个参数,这个参数是消息上下文(后面用 ctx 代替),是将服务端传过来的原始信息处理后的对象。 80 | 原始数据是一个字典, 经过包装得到的 ctx 各项属性是原始数据中常用的字段,什么是常用字段?就是每一个该类消息都具有的字段, 81 | 比如群消息中分有图片、文本、语音等,它们都有 FromGroupId、FromUserId 等字段,而它们的不同是在 Content 字段中 82 | 当然如果你想使用最原始的数据,使用`ctx.message`属性即可 83 | 84 | 在编写接收函数时,建议导入相关类(FriendMsg, GroupMsg, EventMsg),使用注解语法,这样可以获得足够的代码提示, 也能避免出错 85 | 86 | ### 临时会话(私聊)消息 87 | 88 | 这是处理起来最麻烦的类型,所以框架没有提供处理,因为会特别乱 89 | 私聊消息属于好友消息一类,所以要在好友消息接收函数中处理, 90 | 如果 ctx.TempUin 属性不为 None,说明是私聊消息,可以通过 ctx.MsgType 和 ctx.TempUin 进行判断 91 | ctx.TempUin 是发起该临时会话的入口群聊的 id, 其他的数据请自行处理 92 | 93 | ## 消息中间件 94 | 95 | 可以对每一个(三个)消息上下文注册且只能一个中间件函数,中间件函数签名与接收函数一致。 96 | 97 | 在中间件中,你可以对消息上下文进行修改,只建议添加属性,不破坏原始属性 98 | 99 | ~~中间件的返回值如果与 ctx 类型一致,则将该返回值作为接收函数的参数~~ 100 | 101 | **只有当中间件的返回值类型与消息上下文对象类型一致时,消息才会向下传递给接收函数,否则所有消息都会被忽略!** 102 | 103 | 中间件的主要适用于给 ctx 添加额外属性,用于在接收函数中通过参数 ctx 直接访问,这对编写插件会有帮助 104 | 105 | 比如给 ctx 添加 master 属性(主人 qq),这样从而可以从插件中访问,而不用在每个需要用到的地方都显式的注明主人 QQ 号、 106 | 也可以给 ctx 添加开启队列的 Action,这样所有插件共用同一个队列等等 107 | 108 | ## 提取准确信息的 refine 函数 109 | 110 | 默认传递的上下文对象只包含该消息的固定字段,也就是不管哪种,都会包含的东东。 111 | 上面说过图片、文字、语音甚至红包消息等消息的不同在 Content 字段中,该字段永远是 str 类型,对于文本消息,该字段就是消息原文, 112 | 对其他消息来说,该字段是一个 json 格式的映射,如果需要处理这些数据,就需要自己通过解码 json 获得, 113 | 提取这些数据,如果是好友和群消息,不会特别麻烦,但如果处理含有特别多不同字段的事件类型的消息那就.... 114 | 很明显这是一个常用操作,为了避免重复劳动,所以提供了一系列 `refine_?` 函数用来进一步解析数据。 115 | 116 | refine 117 | 函数位于库的`refine_message`模块中,**现在是 refine 模块,refine_message 还可用,但之后可能取消删除** 118 | 119 | ```python 120 | from iotbot.refine import * 121 | # 按需导入 122 | ``` 123 | 124 | 其中每一个函数对应一种消息类型或场景,如果消息类型与该函数所期望处理的类型一致, 125 | 则会返回一个新的上下文对象,新对象包含了更详尽的属性。 126 | 如果消息类型不匹配,则返回 None,所以 refine 函数也能起判断(类型筛选)的作用 127 | 128 | 通过函数名称自行选择所需的函数 129 | 130 | 请看示例: [bot_test_refine_funcs.py](https://github.com/XiyaoWong/python-iotbot/blob/master/sample/plugins/bot_test_refine_funcs.py) 131 | 132 | ### 一览 133 | 134 | | 名称 | 作用 | 135 | | ------------------------------------- | ----------------------------------- | 136 | | refine_group_revoke_event_msg | 群成员撤回消息事件 | 137 | | refine_group_exit_event_msg | 群成员退出群聊事件 | 138 | | refine_group_join_event_msg | 某人进群事件 | 139 | | refine_friend_revoke_event_msg | 好友撤回消息事件 | 140 | | refine_friend_delete_event_msg | 删除好友事件 | 141 | | refine_group_adminsysnotify_event_msg | QQ 群系统消息通知(加群申请在这里面) | 142 | | refine_group_shut_event_msg | 群禁言事件 | 143 | | refine_group_admin_event_msg | 管理员变更事件 | 144 | | refine_voice_group_msg | 群语音消息 | 145 | | refine_video_group_msg | 群视频消息 | 146 | | refine_pic_group_msg | 群图片/表情包消息 | 147 | | refine_RedBag_group_msg | 群红包消息 | 148 | | refine_voice_friend_msg | 好友语音消息 | 149 | | refine_video_friend_msg | 好友视频消息 | 150 | | refine_pic_friend_msg | 好友图片/表情包消息 | 151 | | refine_RedBag_friend_msg | 好友红包消息 | 152 | 153 | 图片消息,与语音消息不同的是,因为可以同是发送几张图片,也就是富文本消息,其中的 GroupPic 是一个列表, 154 | 列表中是图片对象,图片对象又对应其数据 155 | 156 | ## 客户端属性或方法 157 | 158 | - `IOTBOT.receivers` 属性,三种消息接收函数数量 159 | 160 | 还有一系列与插件有关的方法,后面插件部分再说明 161 | 162 | --- 163 | 164 | **注意不要中途尝试修改属性** 165 | -------------------------------------------------------------------------------- /docs/CONFIG.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | 如果你的 iotbot 配置跟默认的不符,设置`host`和`port`会很必要, 4 | 否则每次定义`IOTBOT`、`Action`时你都需要指定参数 5 | 6 | 在项目根目录下创建 `.iotbot.json`文件用于配置,目前支持的配置如下几项: 7 | 8 | ```json 9 | { 10 | "host": "127.0.0.1", 11 | "port": 8888, 12 | "group_blacklist": [], 13 | "friend_blacklist": [], 14 | "webhook": true, 15 | "webhook_post_url": "http://localhost:5000", 16 | "webhook_timeout": 10 17 | } 18 | ``` 19 | 20 | 说明: 21 | 22 | 1. `host`: iotbot 端 ip 23 | 2. `port`: iotbot 端端口 24 | 3. `group_blacklist`: 群黑名单 25 | 4. `friend_blacklist`: 好友黑名单 26 | 5. `webhook`: webhook 功能开关, 如果打开,配置项`webhook_post_url`则为必填 27 | 6. `webhook_post_url`: webhook 推送的地址,需要包含 http 28 | 7. `webhook_timeout`: webhook 推送到 url 请求的响应时间 29 | 30 | 配置项均为可选,可以按需配置 31 | 32 | 配置的优先级大于定义时传入的参数 33 | -------------------------------------------------------------------------------- /docs/DECORATORS.md: -------------------------------------------------------------------------------- 1 | # 接收函数装饰器 2 | 3 | 这里提供了几个装饰器可快速管理接收函数,可用于快速构建简单的插件 4 | 5 | ## 作用一览 6 | 7 | ### not_botself 8 | 9 | ```python 10 | def not_botself(): 11 | """忽略机器人自身的消息""" 12 | ``` 13 | 14 | ### is_botself 15 | 16 | ```python 17 | def is_botself(): 18 | """只要机器人自身的消息""" 19 | ``` 20 | 21 | ### in_content 22 | 23 | ```python 24 | def in_content(string: str): 25 | """ 26 | 接受消息content字段含有指定消息时, 不支持事件类型消息 27 | :param string: 支持正则 28 | """ 29 | ``` 30 | 31 | ### equal_content 32 | 33 | ```python 34 | def equal_content(string: str): 35 | """ 36 | content字段与指定消息相等时, 不支持事件类型消息 37 | """ 38 | ``` 39 | 40 | ### not_these_users 41 | 42 | ```python 43 | def not_these_users(users: list): 44 | """仅接受这些人的消息 45 | :param users: qq 号列表 46 | """ 47 | ``` 48 | 49 | ### only_these_users 50 | 51 | ```python 52 | def only_these_users(users: list): 53 | """仅接受这些人的消息 54 | :param users: qq 号列表 55 | """ 56 | ``` 57 | 58 | ### only_this_msg_type 59 | 60 | ```python 61 | def only_this_msg_type(msg_type: str): 62 | """仅接受该类型消息 63 | :param msg_type: TextMsg, PicMsg, AtMsg, ReplyMsg, VoiceMsg之一 64 | """ 65 | ``` 66 | 67 | ### not_these_groups 68 | 69 | ```python 70 | def not_these_groups(groups: list): 71 | """不接受这些群组的消息 72 | :param groups: 群号列表 73 | """ 74 | ``` 75 | 76 | ### only_these_groups 77 | 78 | ```python 79 | def only_these_groups(groups: list): 80 | """只接受这些群组的消息 81 | :param groups: 群号列表 82 | """ 83 | ``` 84 | 85 | ```python 86 | def startswith(string:str, trim=True): 87 | """content以指定前缀开头时 88 | :param string: 前缀字符串 89 | :param trim: 是否将原始Content部分替换为裁剪掉前缀的内容 90 | """ 91 | ``` 92 | 93 | 这里没写全,还是那句话,用的时候请看代码提示或查看源码 94 | 95 | ## 几个说明 96 | 97 | 1. in_content 与 equal_content 98 | 有一点不同,in_content 用的是最原始的 Content 字段数据,比如图片消息是 json 格式的字符串; 99 | 而 equal_content 使用的是将 json 格式数据解码后的 Content, 100 | `startswith`装饰器和`equal_content`一样 101 | 2. **对装饰器的具体行为有疑惑的请看源码** 102 | 3. **框架对艾特消息都没有处理,所以编写与艾特有关功能时请仔细考虑装饰器是否适用** 103 | 104 | ## 使用示例 105 | 106 | `app.py` 107 | 108 | ```python 109 | import iotbot.decorators as deco 110 | 111 | ... # 省略常规定义部分 112 | 113 | @bot.on_group_msg 114 | @deco.only_these_users([111, 222]) 115 | @deco.in_content('Hello') 116 | def group1(ctx: GroupMsg): # 只有发送人qq是111或222,且包含Hello关键字时,才会被执行 117 | action.send_group_text_msg( 118 | ctx.FromGroupId, 119 | '测试指令 Hello' 120 | ) 121 | ``` 122 | 123 | `bot_test.py` 124 | 125 | ```python 126 | from iotbot import Action, GroupMsg 127 | import iotbot.decorators as deco 128 | 129 | 130 | @deco.equal_content('测试') 131 | @deco.only_these_groups([111]) 132 | def receive_group_msg(ctx: GroupMsg): # 仅当发送内容为'测试'且群号是111时,财被执行 133 | Action(ctx.CurrentQQ).send_group_text_msg( 134 | ctx.FromGroupId, 135 | '测试ok' 136 | ) 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | ## 环境 4 | 5 | python 版本: 3.6+ 6 | 7 | ## 在线安装 8 | 9 | ```shell 10 | pip install python-iotbot -i https://pypi.org/simple --upgrade 11 | ``` 12 | 13 | ```shell 14 | pip install git+https://github.com/XiyaoWong/python-iotbot.git@master 15 | ``` 16 | 17 | 更新会上传 pypi,可以放心使用 pip 安装,前提是使用官方源确保最新版 18 | 19 | ## 离线安装 20 | 21 | ```shell 22 | git clone https://github.com/XiyaoWong/python-iotbot 23 | cd python-iotbot 24 | python setup.py install 25 | ``` 26 | 27 | --- 28 | 29 | 请常留意项目更新 30 | -------------------------------------------------------------------------------- /docs/MACRO.md: -------------------------------------------------------------------------------- 1 | ## 构建宏 2 | 3 | 机器人端消息支持填入宏来获得不同的效果功能,如果你不知道这是什么,就请先去机器人 wiki 中了解。这个模块提供了几个辅助函数用于快速构建正确的宏。 4 | 5 | ```python 6 | from iotbot import macro 7 | ``` 8 | 9 | 请跳转到源码查看使用方法 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/OTHER.md: -------------------------------------------------------------------------------- 1 | # 其他 2 | 3 | 1. 主程序中注册的 receiver 函数优先级比插件中的高,也就是优先分发消息,但实际上他们时间的时间差可以忽略 4 | 2. 每个接收函数运行起来是独立的,彼此毫无关联 5 | 6 | 3. `utils`模块提供了一些辅助数据 7 | 8 | ```python 9 | from iotbot import utils 10 | ``` 11 | 12 | `utils.MsgTypes`: 消息类型名称集合 13 | `utils.EventNames`: 事件类型名称集合 14 | `utils.Emoticons`: 部分表情代码 15 | 16 | 怎么用请看源码便知 17 | -------------------------------------------------------------------------------- /docs/PLUGIN.md: -------------------------------------------------------------------------------- 1 | # 插件化 2 | 3 | ## 启用 4 | 5 | 要开启插件功能,只需在定义机器人时设置对应参数。 6 | 7 | ```python 8 | from iotbot import IOTBOT, GroupMsg 9 | 10 | bot = IOTBOT(your_bot_qq, use_plugins=True) 11 | # 参数`plugin_dir`用来指定插件所在文件夹, 默认为`plugins`, 12 | # 不是路径,必须是在同目录下的一个文件夹 13 | # 强烈建议默认,第一方面少坑,第二如果不使用默认,在很多方法下,需要手动传参,很麻烦 14 | ``` 15 | 16 | ## 插件要求 17 | 18 | 1. 文件名需以`bot_`开头。这样做是因为考虑到会有一些模块需要导入,这样稍微快一点 19 | 2. 消息接收函数命名 20 | 21 | - receive_group_msg # 群消息 22 | - receive_friend_msg # 好友消息 23 | - receive_events # 事件消息 24 | 25 | 命名错误不会影响程序运行,只是不会被使用。 26 | 27 | 3. 参数有且只有一个,即和前面主程序中写法一样 28 | 29 | ## 刷新插件 30 | 31 | 插件由一个插件管理器对象管理 32 | 33 | 假设 `bot = IOTBOT(123)` 34 | 35 | 可以通过以下方法管理插件: 36 | 37 | 1. 方法`bot.reload_plugins()` 重载旧插件,加载新插件 38 | 2. 方法`bot.refresh_plugins()` 刷新插件目录所有插件 39 | 3. 方法`bot.load_plugins()` 加载新插件,已加载插件不会重载 40 | 41 | 4. 属性`bot.plugins` 获取插件名称列表,用于下面方法 42 | 5. 方法`bot.reload_plugin(plugin_name)` 根据插件名重载对应插件 43 | 6. 方法`bot.remove_plugin()` 根据插件名停用对应插件 44 | 45 | 7. 属性`bot.removed_plugins` 获取已停用插件名称列表 46 | 8. 方法`bot.recover_plugin()` 与 remove 对应,根据插件名恢复使用对应插件 47 | 48 | 9. 属性`bot.plugin_status` 插件状态表格 49 | 50 | ~~因为停用的插件列表是保存在内存中的,重启程序后就没了。推荐自己用其他方式实现~~ 51 | 现在会将停用的插件信息保存在当前目录下的`.REMOVED_PLUGINS`文件中,这个文件不要手动修改 52 | -------------------------------------------------------------------------------- /docs/QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 框架的基本思路就是通过 websocket(socketio)接收机器人服务端传来的各类消息和事件,通过调用 webapi 4 | 进行主动操作(发消息,获取数据等)。框架分为 client(客户端)和 action(动作)两部分,client 负责接收,在收到服务端传来的数据后, 5 | 将该数据进行一系列的包装使其变得更数据更直观,更易用,减少踩坑;action 负责发送(其他操作这里统称发送),这部分主要是对 6 | webapi 的封装,细化了部分 api 的使用,方法命名与其意思一致,参数名和作用与 webapi 一致,还有一些封装后面再说明. 7 | 8 | 例: 9 | 10 | ```python 11 | from iotbot import IOTBOT, Action, GroupMsg 12 | 13 | bot = IOTBOT(123456) # 传入QQ号实例化机器人 14 | action = Action(bot) # 实例化发送动作,用于调用webapi, 这是传入bot实例, 也可以传入QQ号 15 | # Action和IOTBOT完全解耦 16 | # IOTBOT和Action都有若干个可选参数,以上实例是采用默认配置 17 | # 常用的参数有host和port,后续再说明 18 | 19 | # 通过装饰器, 注册group函数为一个群消息接收函数 20 | @bot.on_group_msg 21 | def group(ctx: GroupMsg): 22 | # 函数命名随意, 要求是参数有且只有一个,即将原始数据包装过后的新的对象 23 | # 使用注解语法可以方便的获取各项数据 24 | # 现在你可以在该函数编写逻辑了,每次有新的群消息,该函数被自动调用 25 | if ctx.Content == 'test': # 如果发送的内容是 test 则向该群发送文本 ok 26 | action.send_group_text_msg( 27 | ctx.FromGroupId, 28 | 'ok' 29 | ) 30 | 31 | 32 | if __name__ == "__main__": 33 | bot.run() # 启动 34 | ``` 35 | 36 | 这是一个最小实例,你可以在每个接收函数中编写任意多的逻辑,依然不用担心效率问题 37 | -------------------------------------------------------------------------------- /docs/SAMPLE.md: -------------------------------------------------------------------------------- 1 | # 使用示例 2 | 3 | [sample 地址](https://github.com/XiyaoWong/python-iotbot/tree/master/sample 'sample地址') 4 | 5 | 自动回复+1、 刷新插件、召唤群友、nmsl、 舔我、(cmd)执行命令、欢迎新群员、退群提醒、发送图片 6 | -------------------------------------------------------------------------------- /docs/SCAFFOLD.md: -------------------------------------------------------------------------------- 1 | # 脚手架 2 | 3 | ## 查看帮助 4 | ```shell 5 | iotbot -h 6 | ``` 7 | 8 | ## 生成主体文件 9 | ```shell 10 | iotbot -n app -q 123456 11 | ``` 12 | 生成文件命名为app.py, 机器人qq为123456 13 | 14 | ## 生成插件模板 15 | ```shell 16 | iotbot -p test 17 | ``` 18 | 生成文件命名为bot_test.py 19 | 20 | -------------------------------------------------------------------------------- /docs/STATEMENTS.md: -------------------------------------------------------------------------------- 1 | # 项目说明 2 | 3 | 这是[IOTBOT/OPQ](https://github.com/OPQBOT/OPQ/ "OPQ")(现为 OPQ,因为某些原因本框架不方便改名)机器人框架的 Python 开发 SDK,封装了大部分web api接口。同时提供插件功能。 4 | 5 | **此框架均为同步实现,不支持异步**, 虽然是同步框架,但由于使用线程池分配任务,所以你不用担心它的效率(至少大部分情况是) 6 | 7 | 如有错误或需改善的地方,欢迎 issue 或 pr。 感谢贡献,但由于某些原因并不能保证你的pr一定被合并,也请理解. 8 | -------------------------------------------------------------------------------- /docs/TIPS.md: -------------------------------------------------------------------------------- 1 | # TIPS 2 | 3 | 简单测试了下,基本是不会漏消息的,效率还是够用:) 4 | 5 | - 最新的机器人已经取消了 atUser 字段,现在只支持使用宏来艾特,但此框架做了旧版 api 兼容,如果在具有 atUser 参数的 action 方法中传入了 atUser 参数, 6 | 则自动构建宏添加到文本(Content)前面,因为不管宏的位置在哪,发送出来的消息中艾特部分一直都是在文本前面,所以推荐使用该参数。宏通过 macro 模块构建,支持单个 qq 号或 QQ 号列表. 7 | 如果不需要自动添加宏,请不要传如该参数(保持为 0 即可) 8 | -------------------------------------------------------------------------------- /docs/WEBHOOK.md: -------------------------------------------------------------------------------- 1 | # webhook 2 | 3 | webhook 功能是以内置插件的方式实现,实现比较简陋,如果不满足需求可以自行写插件 4 | 5 | 开启 webhook 功能需要设置配置项`webhook`为`true`, 此外另一个必选项为`webhook_post_url`用来推送信息, 为`POST`请求,推送的消息内容和格式与 iotbot websocket 发送过来的一致。 6 | 配置项`webhook_timeout`是指发送 post 请求到指定的地址,对方服务器响应的超时时间 7 | 8 | 发送 post 请求后允许服务端返回的响应包含几个参数用来对上下文进行快速回复,只接 受 json 格式的数据,可选字段有: 9 | 10 | ```json 11 | { 12 | "msg": "Hello", 13 | "at": 0, 14 | "pic_url": "图片链接", 15 | "pic_base64": "图片base64编码", 16 | "voice_url": "音频链接", 17 | "voice_base64": "音频base64编码" 18 | } 19 | ``` 20 | 21 | 说明: 22 | 以上字段都不是必须,只要传了其中的字段,会自动选择发送类型 23 | 图片和语音相关的都有两个字段,\_url 优先于 \_base64, 24 | 25 | 直接上示例: 26 | 27 | 1. 发送文字消息**不艾特** 28 | 29 | ```json 30 | { 31 | "msg": "test" 32 | } 33 | ``` 34 | 35 | 2. 发送文字消息**艾特** 36 | 37 | ```json 38 | { 39 | "msg": "test", 40 | "at": 1 41 | } 42 | ``` 43 | 44 | 3. 发送图片消息 45 | 46 | ```json 47 | { 48 | "pic_url": "..." 49 | } 50 | ``` 51 | 52 | 4. 发送图文消息 53 | 54 | ```json 55 | { 56 | "msg": "test", 57 | "pic_url": "..." 58 | } 59 | ``` 60 | 61 | 5. 发送语音消息 62 | 63 | ```json 64 | { 65 | "voice_url": "..." 66 | } 67 | ``` 68 | 69 | 为了安全考虑,只支持几个简单的操作 70 | ... 71 | 语音和文字和图片没有关系,只要存在即发送 72 | 73 | 图文消息优先于纯文字消息 74 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [项目说明](STATEMENTS.md) 2 | - [快速使用](QUICKSTART.md) 3 | - [安装](INSTALL.md) 4 | - [客户端](CLIENT.md) 5 | - [动作](ACTION.md) 6 | - [配置](CONFIG.md) 7 | - [插件](PLUGIN.md) 8 | - [接收函数装饰器](DECORATORS.md) 9 | - [构建宏](MACRO.md) 10 | - [Web Hook](WEBHOOK.md) 11 | - [脚手架](SCAFFOLD.md) 12 | - [其他](OTHER.md) 13 | - [示例](SAMPLE.md) 14 | - [TIPS](TIPS.md) 15 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('../iotbot')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'python-iotbot' 21 | copyright = '2020, wongxy' 22 | author = 'wongxy' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '2.4.0' 26 | 27 | master_doc = 'index' 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'recommonmark', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | 41 | source_suffix = { 42 | '.rst': 'restructuredtext', 43 | '.md': 'markdown', 44 | } 45 | 46 | 47 | # The language for content autogenerated by Sphinx. Refer to documentation 48 | # for a list of supported languages. 49 | # 50 | # This is also used if you do content translation via gettext catalogs. 51 | # Usually you set "language" from the command line for these cases. 52 | language = 'zh_CN' 53 | 54 | # List of patterns, relative to source directory, that match files and 55 | # directories to ignore when looking for source files. 56 | # This pattern also affects html_static_path and html_extra_path. 57 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 58 | 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. See the documentation for 63 | # a list of builtin themes. 64 | # 65 | html_theme = 'sphinx_rtd_theme' 66 | 67 | # Add any paths that contain custom static files (such as style sheets) here, 68 | # relative to this directory. They are copied after the builtin static files, 69 | # so a file named "default.css" will overwrite the builtin "default.css". 70 | html_static_path = ['_static'] 71 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | python-iotbot's Document 6 | 7 | 8 | 12 | 16 | 17 | 18 |
19 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-iotbot documentation master file, created by 2 | sphinx-quickstart on Sun Jun 21 07:22:35 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-iotbot's documentation! 7 | ========================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | STATEMENTS 14 | INSTALL 15 | QUICKSTART 16 | CLIENT 17 | ACTION 18 | CONFIG 19 | PLUGIN 20 | DECORATORS 21 | MACRO 22 | WEBHOOK 23 | SCHEDULER 24 | SCAFFOLD 25 | OTHER 26 | SAMPLE 27 | TIPS 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /iotbot/__init__.py: -------------------------------------------------------------------------------- 1 | """Author: wongxy 2 | 3 | github.com/xiyaowong/python-iotbot 4 | 5 | >>> from iotbot import IOTBOT, GroupMsg, FriendMsg, EventMsg 6 | 7 | >>> bot = IOTBOT(your_bot_qq) 8 | 9 | >>> @bot.on_group_msg 10 | >>> def group(ctx: GroupMsg): 11 | >>> pass 12 | 13 | >>> @bot.on_friend_msg 14 | >>> def friend(ctx: FriendMsg): 15 | >>> pass 16 | 17 | >>> @bot.on_event 18 | >>> def event(message: EventMsg): 19 | >>> pass 20 | 21 | >>> if __name__ == '__main__': 22 | bot.run() 23 | """ 24 | 25 | try: 26 | import ujson as json 27 | except ImportError: 28 | import json 29 | 30 | from .action import Action 31 | from .client import IOTBOT 32 | from .model import EventMsg, FriendMsg, GroupMsg 33 | 34 | __author__ = 'wongxy' 35 | -------------------------------------------------------------------------------- /iotbot/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import cli 2 | 3 | if __name__ == "__main__": 4 | cli() 5 | -------------------------------------------------------------------------------- /iotbot/action.py: -------------------------------------------------------------------------------- 1 | """一些常用的方法 2 | 3 | Tips: 如果开启队列,请将`action`定义为全局变量!,最重要的一点,开启队列方法都没有返回值, 4 | 所以对于获取信息的api,千万不能用这个模式 5 | 6 | 对于发送语音,图片的方法,建议将timeout设置很短,因为暂时发现这类请求因为需要文件上传操作, 7 | 响应时间会较长,而且目前来看,如果文件较大导致上传时间太长,IOTBOT端会报错, IOTBOT响应的结果一定是错误的, 8 | 不过发送去的操作是能正常完成的。 9 | """ 10 | import functools 11 | import queue 12 | import re 13 | import threading 14 | import time 15 | import traceback 16 | from typing import Callable, Generator, List, Union 17 | 18 | import requests 19 | from requests.exceptions import Timeout 20 | 21 | from . import json, macro 22 | from .config import Config 23 | from .client import IOTBOT 24 | from .logger import logger 25 | 26 | 27 | class _Task: 28 | def __init__(self, target: Callable, args: tuple = None, callback: Callable = None): 29 | args = args or tuple() 30 | self.target = functools.partial(target, *args) 31 | functools.update_wrapper(self.target, target) 32 | self.callback = callback 33 | 34 | 35 | class _SendThread(threading.Thread): 36 | def __init__(self, delay=1.1): 37 | super().__init__() 38 | self.tasks = queue.Queue(maxsize=-1) 39 | self.running = False 40 | self.delay = delay 41 | self.last_send_time = time.time() 42 | 43 | def run(self): 44 | self.running = True 45 | while True: 46 | try: 47 | # 因为重载(importlib.relaod)之后,线程仍会在后台运行 48 | # 暂时使用超时跳出线程 49 | # 线程停了之后,被重载后,是不是会被gc??? 0.o 50 | task: _Task = self.tasks.get(timeout=30 * 60) # 30min 51 | except queue.Empty: 52 | self.running = False 53 | break 54 | else: 55 | should_wait = self.delay - (time.time() - self.last_send_time) 56 | if should_wait > 0: 57 | time.sleep(should_wait) 58 | try: 59 | ret = task.target() 60 | if task.callback is not None: 61 | task.callback(ret) 62 | except Exception: 63 | logger.exception('Action发送线程出错') 64 | finally: 65 | self.last_send_time = time.time() 66 | 67 | def start(self): 68 | # 强改内部方法以允许重复执行start方法, 暂时不知道这样做有什么后果 69 | if not self.running: 70 | self._started.is_set = lambda: False 71 | else: 72 | self._started.is_set = lambda: True 73 | super().start() 74 | 75 | def put_task(self, task: _Task): 76 | assert isinstance(task, _Task) 77 | self.tasks.put(task) 78 | if not self.running: 79 | self.start() 80 | 81 | 82 | class Action: # pylint:disable=R0904 83 | """ 84 | :param qq_or_bot: qq号或者机器人实例(`IOTBOT`) 85 | :param queue: 是否开启队列,开启后任务将按顺序发送并延时指定时间,此参数与`queue_delay`对应 86 | 启用后,发送方法`没有返回值` 87 | :param queue_delay: 与`队列`对应, 开启队列时发送每条消息间的延时, 保持默认即可 88 | :param port: 端口 89 | :param host: ip 90 | """ 91 | 92 | def __init__( 93 | self, 94 | qq_or_bot: Union[int, IOTBOT] = None, 95 | queue: bool = False, 96 | queue_delay: Union[int, float] = 1.1, 97 | port: int = None, 98 | host: str = None, 99 | ): 100 | self.config = Config(host, port) 101 | if isinstance(qq_or_bot, IOTBOT): 102 | self.bind_bot(qq_or_bot) 103 | else: 104 | self.qq = int(qq_or_bot) 105 | 106 | self.s = requests.Session() 107 | 108 | # 任务队列 109 | self._use_queue = queue 110 | self._send_thread = _SendThread(queue_delay) 111 | self._send_thread.setDaemon(True) 112 | 113 | def bind_bot(self, bot: IOTBOT): 114 | """绑定机器人""" 115 | self.qq = bot.qq[0] 116 | self.config = bot.config 117 | 118 | def send_friend_text_msg( 119 | self, toUser: int, content: str, timeout=5, **kwargs 120 | ) -> dict: 121 | """发送好友文本消息""" 122 | data = { 123 | "toUser": toUser, 124 | "sendToType": 1, 125 | "sendMsgType": "TextMsg", 126 | "content": content, 127 | "groupid": 0, 128 | "replayInfo": None, 129 | } 130 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 131 | 132 | def get_user_list(self, timeout=5, **kwargs) -> dict: 133 | """获取好友列表""" 134 | return self.baseSender( 135 | 'post', 'GetQQUserList', {"StartIndex": 0}, timeout=timeout, **kwargs 136 | ) 137 | 138 | def send_friend_voice_msg( 139 | self, toUser, voiceUrl='', voiceBase64Buf='', timeout=5, **kwargs 140 | ) -> dict: 141 | """发送好友语音消息""" 142 | data = { 143 | "toUser": toUser, 144 | "sendToType": 1, 145 | "sendMsgType": "VoiceMsg", 146 | "content": "", 147 | "groupid": 0, 148 | "voiceUrl": voiceUrl, 149 | "voiceBase64Buf": voiceBase64Buf, 150 | } 151 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 152 | 153 | def send_friend_pic_msg( 154 | self, 155 | toUser, 156 | content='', 157 | picUrl='', 158 | picBase64Buf='', 159 | fileMd5: List[str] = None, 160 | flashPic=False, 161 | timeout=5, 162 | **kwargs, 163 | ) -> dict: 164 | """发送好友图片消息""" 165 | data = { 166 | "toUser": toUser, 167 | "sendToType": 1, 168 | "sendMsgType": "PicMsg", 169 | "content": content, 170 | "groupid": 0, 171 | "picUrl": picUrl, 172 | "picBase64Buf": picBase64Buf, 173 | "fileMd5": fileMd5, 174 | "flashPic": flashPic, 175 | } 176 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 177 | 178 | def send_group_text_msg( 179 | self, 180 | toUser: int, 181 | content='', 182 | atUser: Union[int, List[int]] = 0, 183 | timeout=5, 184 | **kwargs, 185 | ) -> dict: 186 | """发送群文字消息""" 187 | if atUser != 0: 188 | content = macro.atUser(atUser) + content 189 | data = { 190 | "toUser": toUser, 191 | "sendToType": 2, 192 | "sendMsgType": "TextMsg", 193 | "content": content, 194 | "groupid": 0, 195 | } 196 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 197 | 198 | def send_group_voice_msg( 199 | self, toUser, voiceUrl='', voiceBase64Buf='', timeout=5, **kwargs 200 | ) -> dict: 201 | """发送群语音""" 202 | data = { 203 | "toUser": toUser, 204 | "sendToType": 2, 205 | "sendMsgType": "VoiceMsg", 206 | "content": '', 207 | "groupid": 0, 208 | "voiceUrl": voiceUrl, 209 | "voiceBase64Buf": voiceBase64Buf, 210 | } 211 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 212 | 213 | def send_group_pic_msg( 214 | self, 215 | toUser: int, 216 | picUrl='', 217 | flashPic=False, 218 | atUser: Union[int, List[int]] = 0, 219 | content='', 220 | picBase64Buf='', 221 | fileMd5: List[str] = None, # 多图 222 | timeout=5, 223 | **kwargs, 224 | ) -> dict: 225 | """发送群图片 226 | Tips: 227 | [秀图id] 各id对应效果 228 | 40000 秀图 40001 幻影 40002 抖动 40003 生日 229 | 40004 爱你 40005 征友 40006 无(只显示大图无特效) 230 | 231 | [PICFLAG] 改变图文消息顺序 232 | """ 233 | if atUser != 0: 234 | content = macro.atUser(atUser) + content 235 | data = { 236 | "toUser": toUser, 237 | "sendToType": 2, 238 | "sendMsgType": "PicMsg", 239 | "content": content, 240 | "groupid": 0, 241 | "picUrl": picUrl, 242 | "picBase64Buf": picBase64Buf, 243 | "fileMd5": fileMd5, 244 | "flashPic": flashPic, 245 | } 246 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 247 | 248 | def send_private_text_msg( 249 | self, toUser: int, content: str, groupid: int, timeout=5, **kwargs 250 | ) -> dict: 251 | """发送私聊文字消息""" 252 | data = { 253 | "toUser": toUser, 254 | "sendToType": 3, 255 | "sendMsgType": "TextMsg", 256 | "content": content, 257 | "groupid": groupid, 258 | } 259 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 260 | 261 | def send_private_voice_msg( 262 | self, toUser: int, groupid, voiceUrl='', voiceBase64Buf='', timeout=5, **kwargs 263 | ) -> dict: 264 | """发送私聊语音""" 265 | data = { 266 | "toUser": toUser, 267 | "sendToType": 3, 268 | "sendMsgType": "VoiceMsg", 269 | "content": "", 270 | "groupid": groupid, 271 | "voiceUrl": voiceUrl, 272 | "voiceBase64Buf": voiceBase64Buf, 273 | } 274 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 275 | 276 | def send_private_pic_msg( 277 | self, 278 | toUser, 279 | groupid, 280 | picUrl='', 281 | picBase64Buf='', 282 | content='', 283 | fileMd5: List[str] = None, 284 | timeout=10, 285 | **kwargs, 286 | ) -> dict: 287 | """发送私聊图片""" 288 | data = { 289 | "toUser": toUser, 290 | "sendToType": 3, 291 | "sendMsgType": "PicMsg", 292 | "content": content, 293 | "groupid": groupid, 294 | "picUrl": picUrl, 295 | "picBase64Buf": picBase64Buf, 296 | "fileMd5": fileMd5, 297 | } 298 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 299 | 300 | def send_group_json_msg(self, toUser: int, content='', timeout=5, **kwargs) -> dict: 301 | """发送群Json类型信息 302 | :param content: 可以为json文本,或者字典类型 303 | """ 304 | if isinstance(content, dict): 305 | content = json.dumps(content) 306 | data = { 307 | "toUser": toUser, 308 | "sendToType": 2, 309 | "sendMsgType": "JsonMsg", 310 | "content": content, 311 | "groupid": 0, 312 | } 313 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 314 | 315 | def send_group_xml_msg(self, toUser: int, content='', timeout=5, **kwargs) -> dict: 316 | """发送群Xml类型信息""" 317 | data = { 318 | "toUser": toUser, 319 | "sendToType": 2, 320 | "sendMsgType": "XmlMsg", 321 | "content": content, 322 | "groupid": 0, 323 | } 324 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 325 | 326 | def revoke_msg( 327 | self, groupid: int, msgseq: int, msgrandom: int, type_=1, timeout=5, **kwargs 328 | ) -> dict: 329 | """撤回消息 330 | :param type_: 1: RevokeMsg | 2: PbMessageSvc.PbMsgWithDraw 331 | """ 332 | funcname = 'RevokeMsg' if type_ == 1 else 'PbMessageSvc.PbMsgWithDraw' 333 | data = {"GroupID": groupid, "MsgSeq": msgseq, "MsgRandom": msgrandom} 334 | return self.baseSender('POST', funcname, data, timeout, **kwargs) 335 | 336 | def search_group(self, content, page=0, timeout=5, **kwargs) -> dict: 337 | """搜索群组""" 338 | return self.baseSender( 339 | 'POST', 'SearchGroup', {"Content": content, "Page": page}, timeout, **kwargs 340 | ) 341 | 342 | def get_user_info(self, userID: int, timeout=5, **kwargs) -> dict: 343 | '''获取用户信息''' 344 | return self.baseSender( 345 | 'POST', 'GetUserInfo', {'UserID': userID, 'GroupID': 0}, timeout, **kwargs 346 | ) 347 | 348 | def get_cookies(self, timeout=2, **kwargs) -> dict: 349 | """获取cookies""" 350 | return self.baseSender('GET', 'GetUserCook', timeout=timeout, **kwargs) 351 | 352 | def get_group_list(self, timeout=5, **kwargs) -> dict: 353 | """获取群组列表""" 354 | return self.baseSender( 355 | 'POST', 'GetGroupList', {"NextToken": ""}, timeout, **kwargs 356 | ) 357 | 358 | def get_group_admin_list(self, groupid: int, timeout=5, **kwargs) -> List[dict]: 359 | """获取群管理员列表""" 360 | members = self.get_group_user_list(groupid, timeout, **kwargs) 361 | return [member for member in members if member['GroupAdmin'] == 1] 362 | 363 | def get_group_all_admin_list(self, groupid: int, timeout=5, **kwargs) -> List[dict]: 364 | """群管理列表+群主""" 365 | owner = None 366 | for group in self.get_group_list(timeout)['TroopList']: 367 | if group['GroupId'] == groupid: 368 | owner = group['GroupOwner'] 369 | break 370 | members = self.get_group_user_list(groupid, timeout, **kwargs) 371 | return [ 372 | member 373 | for member in members 374 | if member['GroupAdmin'] == 1 or member['MemberUin'] == owner 375 | ] 376 | 377 | def get_group_user_list( 378 | self, groupid: int, timeout=5, **kwargs 379 | ) -> Generator[dict, None, None]: 380 | """获取群成员列表""" 381 | LastUin = 0 382 | while True: 383 | data = self.baseSender( 384 | 'POST', 385 | 'GetGroupUserList', 386 | {"GroupUin": groupid, "LastUin": LastUin}, 387 | timeout, 388 | **kwargs, 389 | ) 390 | LastUin = data['LastUin'] # 上面请求失败会返回空字典。但是这里不处理错误, 必须正常抛出 391 | for member in data['MemberList']: 392 | yield member 393 | if LastUin == 0: 394 | break 395 | time.sleep(0.8) 396 | 397 | def set_unique_title( 398 | self, groupid: int, userid: int, Title: str, timeout=5, **kwargs 399 | ) -> dict: 400 | """设置群成员头衔""" 401 | return self.baseSender( 402 | 'POST', 403 | 'OidbSvc.0x8fc_2', 404 | {"GroupID": groupid, "UserID": userid, "NewTitle": Title}, 405 | timeout, 406 | **kwargs, 407 | ) 408 | 409 | def modify_group_card( 410 | self, userID: int, groupID: int, newNick: str, timeout=5, **kwargs 411 | ) -> dict: 412 | """修改群名片 413 | :params userID: 修改的QQ号 414 | :params groupID: 群号 415 | :params newNick: 新群名片 416 | """ 417 | data = {'UserID': userID, 'GroupID': groupID, 'NewNick': newNick} 418 | return self.baseSender('POST', 'ModifyGroupCard', data, timeout, **kwargs) 419 | 420 | def refresh_keys(self, timeout=20) -> bool: 421 | '''刷新key二次登陆, 成功返回True, 失败返回False''' 422 | try: 423 | rep = self.s.get( 424 | f'{self.config.address}/v1/RefreshKeys?qq={self.qq}', timeout=timeout 425 | ) 426 | if rep.json()['Ret'] == 'ok': 427 | return True 428 | except Exception: 429 | pass 430 | return False 431 | 432 | def get_balance(self) -> dict: 433 | '''获取QQ钱包余额''' 434 | # TODO 435 | 436 | # return self.baseSender('GET', 'GetBalance', timeout=timeout, **kwargs) 437 | 438 | def get_status(self, timeout=20) -> dict: 439 | '''获取机器人状态''' 440 | rep = self.s.get(f'{self.config.address}/v1/ClusterInfo', timeout=timeout) 441 | return rep.json() 442 | 443 | def send_single_red_bag(self) -> dict: 444 | '''发送群/好友红包''' 445 | # TODO 446 | # data = {} 447 | # return self.baseSender('POST', 'SendSingleRed', data, timeout, **kwargs) 448 | 449 | def send_qzone_red_bag(self) -> dict: 450 | '''发送QQ空间红包''' 451 | # TODO 452 | # data = {} 453 | # return self.baseSender('POST', 'SendQzoneRed', data, timeout, **kwargs) 454 | 455 | def send_transfer(self) -> dict: 456 | '''支付转账''' 457 | # TODO 458 | # data = {} 459 | # return self.baseSender('POST', 'Transfer', data, timeout, **kwargs) 460 | 461 | def open_red_bag(self, OpenRedBag) -> dict: 462 | '''打开红包 传入红包数据结构''' 463 | # TODO 464 | 465 | # return self.baseSender('POST', 'OpenRedBag', OpenRedBag, timeout, **kwargs) 466 | 467 | def add_friend( 468 | self, 469 | userID: int, 470 | groupID: int, 471 | content='加个好友!', 472 | AddFromSource=2004, 473 | timeout=20, 474 | **kwargs, 475 | ) -> dict: 476 | """添加好友""" 477 | data = { 478 | "AddUserUid": userID, 479 | "FromGroupID": groupID, 480 | "AddFromSource": AddFromSource, 481 | "Content": content, 482 | } 483 | return self.baseSender('POST', 'AddQQUser', data, timeout, **kwargs) 484 | 485 | def get_friend_file(self, FileID: str, timeout=20, **kwargs) -> dict: 486 | """获取好友文件下载链接""" 487 | funcname = 'OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_DOWNLOAD-1200' 488 | data = {'FileID': FileID} 489 | return self.baseSender('POST', funcname, data, timeout, **kwargs) 490 | 491 | def get_group_file(self, groupID: int, FileID: str, timeout=20, **kwargs) -> dict: 492 | """获取群文件下载链接""" 493 | funcname = 'OidbSvc.0x6d6_2' 494 | data = {'FileID': FileID, 'GroupID': groupID} 495 | return self.baseSender('POST', funcname, data, timeout, **kwargs) 496 | 497 | def set_group_announce( 498 | self, 499 | groupID: int, 500 | Title: str, 501 | Text: str, 502 | Pinned=0, 503 | Type=10, 504 | timeout=5, 505 | **kwargs, 506 | ) -> dict: 507 | """设置群公告""" 508 | data = { 509 | 'GroupID': groupID, # 发布的群号 510 | "Title": Title, # 公告标题 511 | "Text": Text, # 公告内容 512 | "Pinned": Pinned, # 1为置顶,0为普通公告 513 | "Type": Type, # 发布类型(10为使用弹窗公告,20为发送给新成员,其他暂未知) 514 | } 515 | try: 516 | res = self.s.post( 517 | f'{self.config.address}/v1/Group/Announce?qq={self.qq}', 518 | data=data, 519 | timeout=timeout, 520 | **kwargs, 521 | ) 522 | return res.json() 523 | except Exception: 524 | return {} 525 | 526 | def deal_friend(self, Action: int) -> dict: 527 | """处理好友请求""" 528 | # TODO 529 | # --Action 1 忽略 2 同意 3 拒绝 530 | # data = { 531 | # 'Action':Action 532 | # } 533 | # return self.baseSender('POST', 'DealFriend', data, timeout, **kwargs) 534 | 535 | def deal_group(self, Action: int) -> dict: 536 | '''处理群邀请''' 537 | # TODO 538 | # --Action 14 忽略 1 同意 21 拒绝 539 | # data = { 540 | # 'Action':Action 541 | # } 542 | # return self.baseSender('POST', 'AnswerInviteGroup', data, timeout, **kwargs) 543 | 544 | def all_shut_up_on(self, groupid, timeout=20, **kwargs) -> dict: 545 | """开启全员禁言""" 546 | return self.baseSender( 547 | 'POST', 548 | 'OidbSvc.0x89a_0', 549 | {"GroupID": groupid, "Switch": 1}, 550 | timeout, 551 | **kwargs, 552 | ) 553 | 554 | def all_shut_up_off(self, groupid, timeout=20, **kwargs) -> dict: 555 | """关闭全员禁言""" 556 | return self.baseSender( 557 | 'POST', 558 | 'OidbSvc.0x89a_0', 559 | {"GroupID": groupid, "Switch": 0}, 560 | timeout, 561 | **kwargs, 562 | ) 563 | 564 | def you_shut_up(self, groupid, userid, shut_time=0, timeout=20, **kwargs) -> dict: 565 | """群成员禁言""" 566 | return self.baseSender( 567 | 'POST', 568 | 'OidbSvc.0x570_8', 569 | {"GroupID": groupid, "ShutUpUserID": userid, "ShutTime": shut_time}, 570 | timeout, 571 | **kwargs, 572 | ) 573 | 574 | def like(self, userid: int, timeout=10, **kwargs) -> dict: 575 | """通用点赞""" 576 | return self.baseSender( 577 | 'POST', 'OidbSvc.0x7e5_4', {"UserID": userid}, timeout, **kwargs 578 | ) 579 | 580 | def like_2(self, userid: int, timeout=10, **kwargs) -> dict: 581 | """测试赞(这里的测试只是与webapi描述一致)""" 582 | return self.baseSender('POST', 'QQZan', {"UserID": userid}, timeout, **kwargs) 583 | 584 | def logout(self, flag=False, timeout=5, **kwargs) -> bool: 585 | """退出QQ 586 | :param flag:是否删除设备信息文件 587 | """ 588 | return self.baseSender('POST', 'LogOut', {"Flag": flag}, timeout, **kwargs) 589 | 590 | def set_group_admin(self, groupID: int, userID: int, timeout=10, **kwargs) -> dict: 591 | '''设置群管理员''' 592 | return self.baseSender( 593 | 'POST', 594 | 'OidbSvc.0x55c_1', 595 | {"GroupID": groupID, "UserID": userID, "Flag": 1}, 596 | timeout, 597 | **kwargs, 598 | ) 599 | 600 | def cancel_group_admin( 601 | self, groupID: int, userID: int, timeout=10, **kwargs 602 | ) -> dict: 603 | '''取消群管理员''' 604 | return self.baseSender( 605 | 'POST', 606 | 'OidbSvc.0x55c_1', 607 | {"GroupID": groupID, "UserID": userID, "Flag": 0}, 608 | timeout, 609 | **kwargs, 610 | ) 611 | 612 | def repost_video_to_group( 613 | self, groupID: int, forwordBuf: str, timeout=10, **kwargs 614 | ) -> dict: 615 | '''转发视频到群聊''' 616 | data = { 617 | "toUser": groupID, 618 | "sendToType": 2, 619 | "sendMsgType": "ForwordMsg", 620 | "content": "", 621 | "groupid": 0, 622 | "forwordBuf": forwordBuf, 623 | "forwordField": 19, 624 | } 625 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 626 | 627 | def repost_video_to_friend( 628 | self, userID: int, forwordBuf: str, timeout=10, **kwargs 629 | ) -> dict: 630 | '''转发视频给好友''' 631 | data = { 632 | "toUser": userID, 633 | "sendToType": 1, 634 | "sendMsgType": "ForwordMsg", 635 | "content": "", 636 | "groupid": 0, 637 | "forwordBuf": forwordBuf, 638 | "forwordField": 19, 639 | } 640 | return self.baseSender('POST', 'SendMsg', data, timeout, **kwargs) 641 | 642 | def get_login_qrcode(self) -> str: 643 | '''返回登录二维码的base64''' 644 | try: 645 | resp = self.s.get( 646 | '{}/v1/Login/GetQRcode'.format(self.config.address), timeout=10 647 | ) 648 | except Exception as e: 649 | logger.error('http请求错误 %s' % str(e)) 650 | else: 651 | try: 652 | return re.findall(r'"data:image/png;base64,(.*?)"', resp.text)[0] 653 | except IndexError: 654 | logger.error('base64获取失败') 655 | return '' 656 | 657 | def get_schedules(self, **kwargs) -> dict: 658 | """获取定时任务总数 659 | response {"Crons": "任务总数0\n", "Ret": 0} 660 | """ 661 | return self.baseSender('GET', 'GetCrons', **kwargs) 662 | 663 | def add_schedules(self, Sepc: str, FileName: str, FuncName: str, **kwargs) -> dict: 664 | """添加定时任务 665 | :param sepc: cron表达式 666 | :param FileName: 执行的lua文件名 667 | :param FuncName: 指定在该lua文件下的方法 668 | """ 669 | data = { 670 | "QQ": str(self.qq), # 执行任务的机器人 671 | "Sepc": Sepc, # cron表达式 每5秒执行一次 672 | "FileName": FileName, # 执行的lua文件名 673 | "FuncName": FuncName, # 执行的lua文件名下的TaskTwo方法名 674 | } 675 | return self.baseSender('POST', 'AddCrons', data, **kwargs) 676 | 677 | def del_schedules(self, TaskID: int, **kwargs) -> dict: 678 | """删除定时任务 679 | :param TaskID: 任务ID 680 | """ 681 | return self.baseSender('POST', 'DelCrons', {'TaskID': TaskID}, **kwargs) 682 | 683 | def send_phone_text_msg(self, content: str, **kwargs) -> dict: 684 | """给手机发送消息""" 685 | data = { 686 | "ToUserUid": self.qq, 687 | "SendToType": 2, 688 | "SendMsgType": "PhoneMsg", 689 | "Content": content, 690 | } 691 | return self.baseSender('POST', 'SendMsgV2', data, **kwargs) 692 | 693 | def baseSender( 694 | self, 695 | method: str, 696 | funcname: str, 697 | data: dict = None, 698 | timeout: int = None, 699 | iot_timeout: int = None, 700 | bot_qq: int = None, 701 | **kwargs, 702 | ) -> dict: 703 | """ 704 | :param method: 请求方法 705 | :param funcname: 请求类型 706 | :param data: post的数据 707 | :param timeout: 发送请求等待响应的时间 708 | :param api_path: 默认为/v1/LuaApiCaller 709 | :param iot_timeout: IOT端处理请求等待的时间 710 | :param bot_qq: 机器人QQ 711 | 712 | :return: iotbot端返回的json数据(字典),其他情况一律返回空字典 713 | """ 714 | job = functools.partial( 715 | self._baseSender, 716 | method=method, 717 | funcname=funcname, 718 | data=data, 719 | timeout=timeout, 720 | iot_timeout=iot_timeout, 721 | bot_qq=bot_qq, 722 | ) 723 | functools.update_wrapper(job, self.baseSender) 724 | if self._use_queue: 725 | self._send_thread.put_task( 726 | _Task(target=job, callback=kwargs.get('callback')) 727 | ) 728 | return None 729 | return job() 730 | 731 | def _baseSender( 732 | self, 733 | method: str, 734 | funcname: str, 735 | data: dict = None, 736 | timeout: int = None, 737 | iot_timeout: int = None, 738 | bot_qq: int = None, 739 | ) -> dict: 740 | params = { 741 | 'funcname': funcname, 742 | 'timeout': iot_timeout, 743 | 'qq': bot_qq or self.qq, 744 | } 745 | if data is None: 746 | data = {} 747 | try: 748 | rep = self.s.request( 749 | method=method, 750 | url=f'{self.config.address}/v1/LuaApiCaller', 751 | headers={'Content-Type': 'application/json'}, 752 | params=params, 753 | json=data, 754 | timeout=timeout, 755 | ) 756 | if rep.status_code != 200: 757 | logger.error( 758 | f'HTTP响应码错误, 请检查地址端口是否正确, \ 759 | {self.config.address} => {rep.status_code}' 760 | ) 761 | return {} 762 | response = rep.json() 763 | self._report_response(response) 764 | return response 765 | except Exception as e: 766 | if isinstance(e, Timeout): 767 | logger.warning('响应超时,但不代表处理未成功, 结果未知!') 768 | else: 769 | logger.error(f'出现错误 => {traceback.format_exc()}') 770 | return {} 771 | 772 | def _report_response(self, response): 773 | if response is None: 774 | logger.error( 775 | '可能是SendMsg返回为null\n' 776 | '可能的原因:\n' 777 | '1. 发送消息超过特定版本的长度限制,比如在x86_linux上发送超过215个汉字或645个ASCII\n' 778 | ) 779 | elif isinstance(response, dict) and 'Ret' in response: 780 | ret = response['Ret'] 781 | if ret == 0: 782 | pass 783 | elif ret == 34: 784 | logger.error(f'未知错误,跟消息长度似乎无关,可以尝试分段重新发送 => {response}') 785 | elif ret == 110: 786 | logger.error(f'发送失败,你已被移出该群,请重新加群 => {response}') 787 | elif ret == 120: 788 | logger.error(f'机器人被禁言 => {response}') 789 | elif ret == 241: 790 | logger.error(f'消息发送频率过高,对同一个群或好友,建议发消息的最小间隔控制在1100ms以上 => {response}') 791 | elif ret == 299: 792 | logger.error(f'超过群发言频率限制 => {response}') 793 | else: 794 | logger.error(f'请求发送成功, 但处理失败 => {response}') 795 | -------------------------------------------------------------------------------- /iotbot/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | 5 | def cli(): 6 | parser = argparse.ArgumentParser(description='iotbot模板生成') 7 | parser.add_argument('-n', default='bot', type=str, help='生成的文件名') 8 | parser.add_argument('-q', default=12345678, type=int, help='机器人QQ号') 9 | parser.add_argument('-p', default=None, type=str, help='插件名') 10 | 11 | args = parser.parse_args() 12 | fileName = args.n 13 | qq = args.q 14 | plug_name = args.p 15 | 16 | if plug_name is not None: 17 | file = f'bot_{plug_name}.py' 18 | if input(f'将生成{file},这是覆盖写操作,确定? y/N ').lower() == 'y': 19 | with open(file, 'w', encoding='utf-8') as f: 20 | f.write( 21 | """from iotbot import Action, FriendMsg, GroupMsg, EventMsg 22 | 23 | 24 | # 下面三个函数名不能改,否则不会调用 25 | # 但是都是可选项,建议把不需要用到的函数删除,节约资源 26 | 27 | def receive_group_msg(ctx: GroupMsg): 28 | Action(ctx.CurrentQQ) 29 | 30 | def receive_friend_msg(ctx: FriendMsg): 31 | Action(ctx.CurrentQQ) 32 | 33 | def receive_events(ctx: EventMsg): 34 | Action(ctx.CurrentQQ) 35 | """ 36 | ) 37 | print('OK!') 38 | return 39 | else: 40 | print('bye~') 41 | return 42 | 43 | template_path = os.path.join( 44 | os.path.dirname(os.path.abspath(__file__)), 'template.py' 45 | ) 46 | 47 | c = input(f'将创建{fileName}.py文件, 机器人QQ为:{qq}。是否确定? y/N: ') 48 | if c.lower() == 'y': 49 | with open(template_path, 'r', encoding='utf-8') as f: 50 | temp = f.read() 51 | 52 | temp = temp.replace('bot_qq = 11', f'bot_qq = {qq}') 53 | 54 | with open(f'{fileName}.py', 'w', encoding='utf-8') as f: 55 | f.write(temp) 56 | 57 | print() 58 | print('创建成功~') 59 | print( 60 | f""" 61 | 执行如下命令:python {fileName}.py 62 | 63 | 在机器人所在的群或私聊机器人发送:.test 64 | """ 65 | ) 66 | else: 67 | print('已取消操作') 68 | 69 | 70 | if __name__ == "__main__": 71 | cli() 72 | -------------------------------------------------------------------------------- /iotbot/client.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import functools 3 | import sys 4 | import traceback 5 | from collections.abc import Sequence 6 | from concurrent.futures import ThreadPoolExecutor 7 | from typing import Callable, List, Tuple, Union 8 | 9 | import socketio 10 | 11 | from .config import Config 12 | from .logger import enble_log_file, logger 13 | from .model import EventMsg, FriendMsg, GroupMsg 14 | from .plugin import PluginManager 15 | from .typing import EventMsgReceiver, FriendMsgReceiver, GroupMsgReceiver 16 | 17 | 18 | def _deco_creater(bind_type): 19 | def deco(self, func): 20 | if bind_type == 'OnGroupMsgs': 21 | self.add_group_msg_receiver(func) 22 | elif bind_type == 'OnFriendMsgs': 23 | self.add_friend_msg_receiver(func) 24 | else: 25 | self.add_event_receiver(func) 26 | 27 | return deco 28 | 29 | 30 | class IOTBOT: # pylint: disable = too-many-instance-attributes 31 | """ 32 | :param qq: 机器人QQ号(多Q就传qq号列表) 33 | :param use_plugins: 是否开启插件功能 34 | :param plugin_dir: 插件存放目录 35 | :param group_blacklist: 群黑名单, 此名单中的群聊消息不会被处理,默认为空,即全部处理 36 | :param friend_whitelist: 好友白名单,只有此名单中的好友消息才会被处理,默认为空,即全部处理 37 | :param blocked_users: 用户黑名单,即包括群消息和好友消息, 该用户的消息都不会处理 38 | :param log: 是否开启日志 39 | :param log_file: 是否输出文件日志 40 | :param port: 运行端口 41 | :param host: ip,需要包含schema 42 | """ 43 | 44 | def __init__( 45 | self, 46 | qq: Union[int, List[int]], 47 | use_plugins: bool = False, 48 | plugin_dir: str = 'plugins', 49 | group_blacklist: List[int] = None, 50 | friend_blacklist: List[int] = None, 51 | blocked_users: List[int] = None, 52 | log: bool = True, 53 | log_file: bool = True, 54 | port: int = None, 55 | host: str = None, 56 | ): 57 | if isinstance(qq, Sequence): 58 | self.qq = list(qq) 59 | else: 60 | self.qq = [qq] 61 | self.use_plugins = use_plugins 62 | self.plugin_dir = plugin_dir 63 | self.config = Config( 64 | host, port, group_blacklist, friend_blacklist, blocked_users 65 | ) 66 | 67 | # 作为程序是否应该退出的标志,以便后续用到 68 | self._exit = False 69 | 70 | if log: 71 | if log_file: 72 | enble_log_file() 73 | else: 74 | logger.disable(__name__) 75 | 76 | # 手动添加的消息接收函数 77 | self.__friend_msg_receivers_from_hand = [] 78 | self.__group_msg_receivers_from_hand = [] 79 | self.__event_receivers_from_hand = [] 80 | 81 | # webhook 里的消息接收函数,是特例 82 | if self.config.webhook: 83 | from . import webhook # pylint:disable=import-outside-toplevel 84 | 85 | # 直接加载进 `hand` 86 | self.__friend_msg_receivers_from_hand.append(webhook.receive_friend_msg) 87 | self.__group_msg_receivers_from_hand.append(webhook.receive_group_msg) 88 | self.__event_receivers_from_hand.append(webhook.receive_events) 89 | 90 | # 消息上下文对象中间件 91 | self.__friend_context_middleware: Callable[[FriendMsg], FriendMsg] = None 92 | self.__group_context_middleware: Callable[[GroupMsg], GroupMsg] = None 93 | self.__event_context_middleware: Callable[[EventMsg], EventMsg] = None 94 | 95 | # 插件管理 96 | self.plugMgr = PluginManager(self.plugin_dir) 97 | if use_plugins: 98 | self.plugMgr.load_plugins() 99 | print(self.plugin_status) 100 | 101 | # 当连接上或断开连接运行的函数 102 | # 如果通过装饰器注册了, 这两个字段设置成(func, every_time) 103 | # func 是需要执行的函数, every_time 表示是否每一次连接或断开都会执行 104 | self.__when_connected_do: Tuple[Callable, bool] = None 105 | self.__when_disconnected_do: Tuple[Callable, bool] = None 106 | 107 | # 依次各种初始化 108 | self.__initialize_socketio() 109 | self.__refresh_executor() 110 | self.__initialize_handlers() 111 | 112 | ######################################################################## 113 | # shortcuts to call plugin manager methods 114 | ######################################################################## 115 | # 只推荐使用这几个方法,其他的更细致的方法需要通过 plugMgr 对象访问 116 | def load_plugins(self): 117 | '''加载新插件''' 118 | self.plugMgr.load_plugins() 119 | 120 | def reload_plugins(self): 121 | '''重载旧插件,加载新插件''' 122 | self.plugMgr.reload_plugins() 123 | 124 | def reload_plugin(self, plugin_name: str): 125 | '''重载指定插件''' 126 | self.plugMgr.reload_plugin(plugin_name) 127 | 128 | def refresh_plugins(self): 129 | '''刷新插件目录所有插件''' 130 | self.plugMgr.refresh() 131 | 132 | def remove_plugin(self, plugin_name: str): 133 | '''停用指定插件''' 134 | self.plugMgr.remove_plugin(plugin_name) 135 | 136 | def recover_plugin(self, plugin_name: str): 137 | '''启用指定插件''' 138 | self.plugMgr.recover_plugin(plugin_name) 139 | 140 | @property 141 | def plugin_status(self): 142 | '''插件启用状态''' 143 | return self.plugMgr.info_table 144 | 145 | @property 146 | def plugins(self): 147 | '''插件名列表''' 148 | return self.plugMgr.plugins 149 | 150 | @property 151 | def removed_plugins(self): 152 | '''已停用的插件名列表''' 153 | return self.plugMgr.removed_plugins 154 | 155 | ########################################################################## 156 | # decorators for registering hook function when connected or disconnected 157 | ########################################################################## 158 | def when_connected(self, func: Callable = None, *, every_time=False): 159 | if func is None: 160 | return functools.partial(self.when_connected, every_time=every_time) 161 | self.__when_connected_do = (func, every_time) 162 | return None 163 | 164 | def when_disconnected(self, func: Callable = None, *, every_time=False): 165 | if func is None: 166 | return functools.partial(self.when_disconnected, every_time=every_time) 167 | self.__when_disconnected_do = (func, every_time) 168 | return None 169 | 170 | ######################################################################## 171 | # about socketio 172 | ######################################################################## 173 | def connect(self): 174 | logger.success('Connected to the server successfully!') 175 | 176 | # GetWebConn 177 | for qq in self.qq: 178 | self.socketio.emit( 179 | 'GetWebConn', 180 | str(qq), 181 | callback=lambda x: logger.info( 182 | f'GetWebConn -> {qq} => {x}' # pylint: disable=cell-var-from-loop 183 | ), 184 | ) 185 | 186 | # 连接成功执行用户定义的函数,如果有 187 | if self.__when_connected_do is not None: 188 | self.__when_connected_do[0]() 189 | if not self.__when_connected_do[1]: # 如果不需要每次运行,这里运行一次后就废弃设定的函数 190 | self.__when_connected_do = None 191 | 192 | def disconnect(self): 193 | logger.warning('Disconnected to the server!') 194 | # 断开连接后执行用户定义的函数,如果有 195 | if self.__when_disconnected_do is not None: 196 | self.__when_disconnected_do[0]() 197 | if not self.__when_disconnected_do[1]: 198 | self.__when_disconnected_do = None 199 | 200 | def __initialize_socketio(self): 201 | self.socketio = socketio.Client() 202 | self.socketio.event()(self.connect) 203 | self.socketio.event()(self.disconnect) 204 | 205 | def close(self, status=0): 206 | self.socketio.disconnect() 207 | self.__executor.shutdown(wait=False) 208 | self._exit = True 209 | sys.exit(status) 210 | 211 | def run(self): 212 | logger.info('Connecting to the server...') 213 | try: 214 | self.socketio.connect(self.config.address, transports=['websocket']) 215 | except Exception: 216 | logger.error(traceback.format_exc()) 217 | self.close(1) 218 | else: 219 | try: 220 | self.socketio.wait() 221 | except KeyboardInterrupt: 222 | pass 223 | finally: 224 | self.close(0) 225 | 226 | ######################################################################## 227 | # initialize thread pool 228 | ######################################################################## 229 | def __refresh_executor(self): 230 | # 根据消息接收函数数量初始化线程池 231 | self.__executor = ThreadPoolExecutor( 232 | max_workers=min( 233 | 50, 234 | len( 235 | [ # 减小数量,控制消息频率 236 | *self.plugMgr.friend_msg_receivers, 237 | *self.__friend_msg_receivers_from_hand, 238 | *self.plugMgr.group_msg_receivers, 239 | *self.__group_msg_receivers_from_hand, 240 | *self.plugMgr.event_receivers, 241 | *self.__event_receivers_from_hand, 242 | *range(3), 243 | ] 244 | ) 245 | * 2, 246 | ) 247 | ) 248 | 249 | ######################################################################## 250 | # Add message receiver manually 251 | ######################################################################## 252 | def add_group_msg_receiver(self, func: GroupMsgReceiver): 253 | '''添加群消息接收函数''' 254 | self.__group_msg_receivers_from_hand.append(func) 255 | 256 | def add_friend_msg_receiver(self, func: FriendMsgReceiver): 257 | '''添加好友消息接收函数''' 258 | self.__friend_msg_receivers_from_hand.append(func) 259 | 260 | def add_event_receiver(self, func: EventMsgReceiver): 261 | '''添加事件消息接收函数''' 262 | self.__event_receivers_from_hand.append(func) 263 | 264 | @property 265 | def receivers(self): 266 | '''消息处理函数数量''' 267 | return { 268 | 'friend': len( 269 | ( 270 | *self.plugMgr.friend_msg_receivers, 271 | *self.__friend_msg_receivers_from_hand, 272 | ) 273 | ), 274 | 'group': len( 275 | ( 276 | *self.plugMgr.group_msg_receivers, 277 | *self.__group_msg_receivers_from_hand, 278 | ) 279 | ), 280 | 'event': len( 281 | (*self.plugMgr.event_receivers, *self.__event_receivers_from_hand) 282 | ), 283 | } 284 | 285 | ######################################################################## 286 | # context distributor 287 | ######################################################################## 288 | def __friend_context_distributor(self, context: FriendMsg): 289 | for f_receiver in [ 290 | *self.__friend_msg_receivers_from_hand, 291 | *self.plugMgr.friend_msg_receivers, 292 | ]: 293 | self.__executor.submit( 294 | f_receiver, copy.deepcopy(context) 295 | ).add_done_callback(self.__thread_pool_callback) 296 | 297 | def __group_context_distributor(self, context: GroupMsg): 298 | for g_receiver in [ 299 | *self.__group_msg_receivers_from_hand, 300 | *self.plugMgr.group_msg_receivers, 301 | ]: 302 | self.__executor.submit( 303 | g_receiver, copy.deepcopy(context) 304 | ).add_done_callback(self.__thread_pool_callback) 305 | 306 | def __event_context_distributor(self, context: EventMsg): 307 | for e_receiver in [ 308 | *self.__event_receivers_from_hand, 309 | *self.plugMgr.event_receivers, 310 | ]: 311 | self.__executor.submit( 312 | e_receiver, copy.deepcopy(context) 313 | ).add_done_callback(self.__thread_pool_callback) 314 | 315 | ######################################################################## 316 | # register context middleware 317 | ######################################################################## 318 | def register_friend_context_middleware( 319 | self, middleware: Callable[[FriendMsg], FriendMsg] 320 | ): 321 | """注册好友消息中间件""" 322 | if self.__friend_context_middleware is not None: 323 | raise Exception('Cannot register more than one middleware(friend)') 324 | self.__friend_context_middleware = middleware 325 | 326 | def register_group_context_middleware( 327 | self, middleware: Callable[[GroupMsg], GroupMsg] 328 | ): 329 | """注册群消息中间件""" 330 | if self.__group_context_middleware is not None: 331 | raise Exception('Cannot register more than one middleware(group)') 332 | self.__group_context_middleware = middleware 333 | 334 | def register_event_context_middleware( 335 | self, middleware: Callable[[EventMsg], EventMsg] 336 | ): 337 | """注册事件消息中间件""" 338 | if self.__event_context_middleware is not None: 339 | raise Exception('Cannot register more than one middleware(event)') 340 | self.__event_context_middleware = middleware 341 | 342 | ######################################################################## 343 | # message handler 344 | ######################################################################## 345 | @logger.catch 346 | def __thread_pool_callback(self, worker): 347 | worker_exception = worker.exception() 348 | if worker_exception: 349 | raise worker_exception 350 | 351 | def __friend_msg_handler(self, msg): 352 | context = FriendMsg(msg) 353 | if context.CurrentQQ not in self.qq: 354 | return 355 | logger.info(f'{context.__class__.__name__} -> {context.data}') 356 | # 黑名单 357 | if context.FromUin in self.config.friend_blacklist: 358 | return 359 | # 屏蔽用户 360 | if context.FromUin in self.config.blocked_users: 361 | return 362 | # 中间件 363 | if self.__friend_context_middleware is not None: 364 | new_context = self.__friend_context_middleware(context) 365 | if isinstance(new_context, type(context)): 366 | context = new_context 367 | else: 368 | return 369 | self.__executor.submit(self.__friend_context_distributor, context) 370 | 371 | def __group_msg_handler(self, msg): 372 | context = GroupMsg(msg) 373 | if context.CurrentQQ not in self.qq: 374 | return 375 | logger.info(f'{context.__class__.__name__} -> {context.data}') 376 | # 黑名单 377 | if context.FromGroupId in self.config.group_blacklist: 378 | return 379 | # 屏蔽用户 380 | if context.FromUserId in self.config.blocked_users: 381 | return 382 | # 中间件 383 | if self.__group_context_middleware is not None: 384 | new_context = self.__group_context_middleware(context) 385 | if isinstance(new_context, type(context)): 386 | context = new_context 387 | else: 388 | return 389 | self.__executor.submit(self.__group_context_distributor, context) 390 | 391 | def __event_msg_handler(self, msg): 392 | context = EventMsg(msg) 393 | if context.CurrentQQ not in self.qq: 394 | return 395 | logger.info(f'{context.__class__.__name__} -> {context.data}') 396 | # 中间件 397 | if self.__event_context_middleware is not None: 398 | new_context = self.__event_context_middleware(context) 399 | if isinstance(new_context, type(context)): 400 | context = new_context 401 | else: 402 | return 403 | self.__executor.submit(self.__event_context_distributor, context) 404 | 405 | def __initialize_handlers(self): 406 | self.socketio.on('OnGroupMsgs')(self.__group_msg_handler) 407 | self.socketio.on('OnFriendMsgs')(self.__friend_msg_handler) 408 | self.socketio.on('OnEvents')(self.__event_msg_handler) 409 | 410 | ########################################################################### 411 | # decorators 412 | on_group_msg = _deco_creater('OnGroupMsgs') 413 | on_friend_msg = _deco_creater('OnFriendMsgs') 414 | on_event = _deco_creater('OnEvents') 415 | 416 | def __repr__(self): 417 | return 'IOTBOT <{}> '.format( 418 | " ".join([str(i) for i in self.qq]), 419 | self.config.host, 420 | self.config.port, 421 | self.config.address, 422 | ) 423 | -------------------------------------------------------------------------------- /iotbot/config.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from . import json 4 | from .exceptions import InvalidConfigError 5 | from .utils import check_schema 6 | 7 | # | host | ip或域名 | 127.0.0.1 | 8 | # | port | 端口,也可以不需要 | 8888 | 9 | # | group_blacklist | 群消息黑名单 | [] | 10 | # | friend_blacklist | 好友消息黑名单 | [] | 11 | # | blocked_users | 用户黑名单,即包括群消息和好友消息 | [] | 12 | # | webhook | 是否开启webhook功能 | false | 13 | # | webhook_post_url | webhook上报地址 | None | 14 | # | webhook_timeout | webhook等待响应的延时 | 20 | 15 | 16 | 17 | # 写得很乱,总之就是优先指定的参数值,然后文件中的配置值,最后默认值 18 | class Config: 19 | def __init__( 20 | self, 21 | host: str = None, 22 | port: int = None, 23 | group_blacklist: List[int] = None, 24 | friend_blacklist: List[int] = None, 25 | blocked_users: List[int] = None, 26 | webhook: str = None, 27 | webhook_post_url: str = None, 28 | webhook_timeout: int = None, 29 | ): 30 | self.host = host 31 | self.port = port 32 | self.address = None # 仅做代码提示 33 | self.group_blacklist = group_blacklist 34 | self.friend_blacklist = friend_blacklist 35 | self.blocked_users = blocked_users 36 | self.webhook = webhook 37 | self.webhook_post_url = webhook_post_url 38 | self.webhook_timeout = webhook_timeout 39 | self.load_file() 40 | 41 | def load_file(self): 42 | _c = {} 43 | try: 44 | with open('./.iotbot.json', encoding='utf-8') as f: 45 | _c = json.load(f) 46 | except FileNotFoundError: 47 | pass 48 | except json.JSONDecodeError as e: 49 | raise InvalidConfigError('配置文件不规范') from e 50 | 51 | # host 52 | if self.host is None: 53 | host = _c.get('host') 54 | if host is None: 55 | self.host = 'http://127.0.0.1' 56 | else: 57 | self.host = check_schema(host) 58 | 59 | # port 60 | if self.port is None: 61 | port = _c.get('port') 62 | if port is None: # 没有配置用默认值 63 | self.port = 8888 64 | elif port == 0: # 配置为0, 则不用端口, 比如单独一个域名 65 | self.port = 0 66 | else: 67 | self.port = int(port) 68 | 69 | # address 由host和port拼接 70 | if self.port == 0: 71 | self.address = host 72 | else: 73 | self.address = f'{self.host}:{self.port}' 74 | 75 | # group_blacklist 76 | if self.group_blacklist is None: 77 | self.group_blacklist = _c.get('group_blacklist') or list() 78 | 79 | # friend_blacklist 80 | if self.friend_blacklist is None: 81 | self.friend_blacklist = _c.get('friend_blacklist') or list() 82 | 83 | # blocked_users 84 | if self.blocked_users is None: 85 | self.blocked_users = _c.get('blocked_users') or list() 86 | 87 | # webhook 88 | if self.webhook is None: 89 | self.webhook = bool(_c.get('webhook')) 90 | 91 | if self.webhook: 92 | # webhook_post_url 93 | if self.webhook_post_url is None: 94 | webhook_post_url = _c.get('webhook_post_url') 95 | self.webhook_post_url = check_schema(webhook_post_url) 96 | else: 97 | self.webhook_post_url = check_schema(self.webhook_post_url) 98 | # webhook_timeout 99 | webhook_timeout = _c.get('webhook_timeout') 100 | if webhook_timeout is None: 101 | self.webhook_timeout = 20 102 | -------------------------------------------------------------------------------- /iotbot/decorators.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import functools 3 | import re 4 | 5 | from . import json 6 | from .model import FriendMsg, GroupMsg 7 | from .refine import ( 8 | _PicFriendMsg, 9 | _PicGroupMsg, 10 | refine_pic_friend_msg, 11 | refine_pic_group_msg, 12 | ) 13 | from .utils import MsgTypes 14 | 15 | 16 | def startswith(string: str, trim=True): 17 | """content以指定前缀开头时 18 | :param string: 前缀字符串 19 | :param trim: 是否将原始Content部分替换为裁剪掉前缀的内容 20 | """ 21 | 22 | def deco(func): 23 | def inner(ctx): 24 | if isinstance(ctx, (GroupMsg, FriendMsg)): 25 | if isinstance(ctx, GroupMsg): 26 | refine_ctx = refine_pic_group_msg(ctx) # type: _PicGroupMsg 27 | else: 28 | refine_ctx = refine_pic_friend_msg(ctx) # type:_PicFriendMsg 29 | content = refine_ctx.Content # type:str 30 | if content.startswith(string): 31 | # 不需要裁剪,直接传入原始ctx 32 | if not trim: 33 | return func(ctx) 34 | # 需要裁剪 35 | # 处理完图片消息中的Content后,需重新编码为字符串,保证传入接收函数ctx一致性 36 | new_content = content[len(string) :] 37 | if ctx.MsgType == MsgTypes.PicMsg: 38 | raw_content_data = json.loads(ctx.Content) 39 | raw_content_data['Content'] = new_content 40 | ctx.Content = json.dumps(raw_content_data, ensure_ascii=False) 41 | else: 42 | ctx.Content = new_content 43 | return func(ctx) 44 | return None 45 | 46 | return inner 47 | 48 | return deco 49 | 50 | 51 | def in_content(string: str): 52 | """ 53 | 接受消息content字段含有指定消息时, 不支持事件类型消息 54 | :param string: 支持正则 55 | """ 56 | 57 | def deco(func): 58 | def inner(ctx): 59 | if isinstance(ctx, (GroupMsg, FriendMsg)): 60 | if re.findall(string, ctx.Content): 61 | return func(ctx) 62 | return None 63 | 64 | return inner 65 | 66 | return deco 67 | 68 | 69 | def equal_content(string: str): 70 | """ 71 | content字段与指定消息相等时, 不支持事件类型消息 72 | """ 73 | 74 | def deco(func): 75 | def inner(ctx): 76 | if isinstance(ctx, (GroupMsg, FriendMsg)): 77 | new_ctx = copy.deepcopy(ctx) 78 | if isinstance(ctx, GroupMsg): 79 | if ctx.MsgType == MsgTypes.PicMsg: 80 | new_ctx = refine_pic_group_msg(new_ctx) 81 | else: 82 | if ctx.MsgType == MsgTypes.PicMsg: 83 | new_ctx = refine_pic_friend_msg(new_ctx) 84 | if new_ctx.Content == string: 85 | return func(ctx) 86 | return None 87 | 88 | return inner 89 | 90 | return deco 91 | 92 | 93 | def not_botself(func=None): 94 | """忽略机器人自身的消息""" 95 | if func is None: 96 | return functools.partial(not_botself) 97 | 98 | def inner(ctx): 99 | if isinstance(ctx, (GroupMsg, FriendMsg)): 100 | if isinstance(ctx, GroupMsg): 101 | userid = ctx.FromUserId 102 | else: 103 | userid = ctx.FromUin 104 | if userid != ctx.CurrentQQ: 105 | return func(ctx) 106 | return None 107 | 108 | return inner 109 | 110 | 111 | def is_botself(func=None): 112 | """只要机器人自身的消息""" 113 | if func is None: 114 | return functools.partial(not_botself) 115 | 116 | def inner(ctx): 117 | if isinstance(ctx, (GroupMsg, FriendMsg)): 118 | if isinstance(ctx, GroupMsg): 119 | userid = ctx.FromUserId 120 | else: 121 | userid = ctx.FromUin 122 | if userid == ctx.CurrentQQ: 123 | return func(ctx) 124 | return None 125 | 126 | return inner 127 | 128 | 129 | def not_these_users(users: list): # pylint:disable=W0613 130 | """不接受这些人的消息 131 | :param users: qq号列表 132 | """ 133 | 134 | def deco(func): 135 | def inner(ctx): 136 | nonlocal users 137 | if isinstance(ctx, (GroupMsg, FriendMsg)): 138 | if not hasattr(users, '__iter__'): 139 | users = [users] 140 | if isinstance(ctx, GroupMsg): 141 | from_user = ctx.FromUserId 142 | elif isinstance(ctx, FriendMsg): 143 | from_user = ctx.FromUin 144 | if from_user not in users: 145 | return func(ctx) 146 | return None 147 | 148 | return inner 149 | 150 | return deco 151 | 152 | 153 | def only_these_users(users: list): # pylint:disable=W0613 154 | """仅接受这些人的消息 155 | :param users: qq号列表 156 | """ 157 | 158 | def deco(func): 159 | def inner(ctx): 160 | nonlocal users 161 | if isinstance(ctx, (GroupMsg, FriendMsg)): 162 | if not hasattr(users, '__iter__'): 163 | users = [users] 164 | if isinstance(ctx, GroupMsg): 165 | from_user = ctx.FromUserId 166 | elif isinstance(ctx, FriendMsg): 167 | from_user = ctx.FromUin 168 | if from_user in users: 169 | return func(ctx) 170 | return None 171 | 172 | return inner 173 | 174 | return deco 175 | 176 | 177 | def only_this_msg_type(msg_type: str): 178 | """仅接受该类型消息 179 | :param msg_type: TextMsg, PicMsg, AtMsg, ReplyMsg, VoiceMsg之一 180 | """ 181 | 182 | def deco(func): 183 | def inner(ctx): 184 | if isinstance(ctx, (GroupMsg, FriendMsg)): 185 | if ctx.MsgType == msg_type: 186 | return func(ctx) 187 | return None 188 | 189 | return inner 190 | 191 | return deco 192 | 193 | 194 | def not_these_groups(groups: list): # pylint:disable=W0613 195 | """不接受这些群组的消息 196 | :param groups: 群号列表 197 | """ 198 | 199 | def deco(func): 200 | def inner(ctx): 201 | nonlocal groups 202 | if isinstance(ctx, GroupMsg): 203 | if not hasattr(groups, '__iter__'): 204 | groups = [groups] 205 | from_group = ctx.FromGroupId 206 | if from_group not in groups: 207 | return func(ctx) 208 | return None 209 | 210 | return inner 211 | 212 | return deco 213 | 214 | 215 | def only_these_groups(groups: list): # pylint:disable=W0613 216 | """只接受这些群组的消息 217 | :param groups: 群号列表 218 | """ 219 | 220 | def deco(func): 221 | def inner(ctx): 222 | nonlocal groups 223 | if isinstance(ctx, GroupMsg): 224 | if not hasattr(groups, '__iter__'): 225 | groups = [groups] 226 | from_group = ctx.FromGroupId 227 | if from_group in groups: 228 | return func(ctx) 229 | return None 230 | 231 | return inner 232 | 233 | return deco 234 | -------------------------------------------------------------------------------- /iotbot/exceptions.py: -------------------------------------------------------------------------------- 1 | class ContextTypeError(Exception): 2 | """不是正确的消息上下文对象""" 3 | 4 | 5 | class InvalidConfigError(Exception): 6 | """配置文件有毛病""" 7 | 8 | 9 | class InvalidPluginError(Exception): 10 | """插件问题""" 11 | -------------------------------------------------------------------------------- /iotbot/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from loguru import logger 4 | 5 | 6 | logger.remove() 7 | logger.add( 8 | sys.stdout, 9 | format='{level.icon} {time:YYYY-MM-DD HH:mm:ss} {level}\t{message}', 10 | colorize=True, 11 | ) 12 | 13 | 14 | def enble_log_file(): 15 | logger.add( 16 | './logs/{time}.log', 17 | format='{time:YYYY-MM-DD HH:mm} {level}\t{message}', 18 | rotation='1 day', 19 | encoding='utf-8', 20 | ) 21 | -------------------------------------------------------------------------------- /iotbot/macro.py: -------------------------------------------------------------------------------- 1 | """辅助构建发送消息宏""" 2 | import random 3 | from collections.abc import Sequence 4 | from typing import List, Union 5 | 6 | 7 | def atUser(users: Union[List[int], int]) -> str: 8 | """艾特(ATUSER) 9 | :param users: 需要艾特的QQ号,如需多个请传列表 10 | """ 11 | if not isinstance(users, Sequence): 12 | users = [users] 13 | return '[ATUSER(%s)]' % ','.join([str(i) for i in users]) 14 | 15 | 16 | def picFlag() -> str: 17 | """改变图片顺序""" 18 | return '[PICFLAG]' 19 | 20 | 21 | def showPic(code: int = None) -> str: 22 | """秀图宏 23 | :param code: 秀图样式编号 40000-40006, 如不传则随机选取一个 24 | 40000 秀图 40001 幻影 40002 抖动 25 | 40003 生日 40004 爱你 40005 征友 26 | 40006 无(只显示大图无特效) 27 | """ 28 | if code is None: 29 | code = random.randint(40000, 40007) 30 | return '[秀图%d]' % code 31 | 32 | 33 | def getUserNick(user: int) -> str: 34 | """获取用户昵称""" 35 | return '[GETUSERNICK(%d)]' % user 36 | -------------------------------------------------------------------------------- /iotbot/model.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-instance-attributes 2 | """消息模型,仅提取固定的字段""" 3 | 4 | 5 | class GroupMsg: 6 | def __init__(self, message: dict): 7 | self.message: dict = message 8 | self.CurrentQQ: int = message.get('CurrentQQ') 9 | 10 | temp = message.get('CurrentPacket') 11 | self.data: dict = temp.get('Data') if temp is not None else {} 12 | 13 | self.FromGroupId: int = self.data.get('FromGroupId') 14 | self.FromGroupName: str = self.data.get('FromGroupName') 15 | self.FromUserId: int = self.data.get('FromUserId') 16 | self.FromNickName: str = self.data.get('FromNickName') 17 | self.Content: str = self.data.get('Content') 18 | self.MsgType: str = self.data.get('MsgType') 19 | self.MsgTime: int = self.data.get('MsgTime') 20 | self.MsgSeq: int = self.data.get('MsgSeq') 21 | self.MsgRandom: int = self.data.get('MsgRandom') 22 | self.RedBaginfo: dict = self.data.get('RedBaginfo') 23 | 24 | def __getitem__(self, key): 25 | return self.message[key] 26 | 27 | 28 | class FriendMsg: 29 | def __init__(self, message: dict): 30 | self.message: dict = message 31 | self.CurrentQQ: int = message.get('CurrentQQ') 32 | 33 | temp = message.get('CurrentPacket') 34 | self.data: dict = temp.get('Data') if temp is not None else {} 35 | 36 | self.FromUin: int = self.data.get('FromUin') 37 | self.ToUin: int = self.data.get('ToUin') 38 | self.MsgType: str = self.data.get('MsgType') 39 | self.MsgSeq: int = self.data.get('MsgSeq') 40 | self.Content: str = self.data.get('Content') 41 | self.RedBaginfo: dict = self.data.get('RedBaginfo') 42 | 43 | # 私聊(临时会话)特有 44 | self.TempUin: int = self.data.get('TempUin') # 入口群聊ID 45 | 46 | def __getitem__(self, key): 47 | return self.message[key] 48 | 49 | 50 | class EventMsg: 51 | def __init__(self, message: dict): 52 | self.message: dict = message 53 | self.CurrentQQ: int = message.get('CurrentQQ') 54 | 55 | temp = message.get('CurrentPacket') 56 | self.data: dict = temp.get('Data') if temp is not None else {} 57 | 58 | self.EventName: str = self.data.get('EventName') 59 | self.EventData: dict = self.data.get('EventData') 60 | self.EventMsg: dict = self.data.get('EventMsg') 61 | 62 | self.Content: str = self.EventMsg.get('Content') 63 | self.FromUin: int = self.EventMsg.get('FromUin') 64 | self.MsgSeq: int = self.EventMsg.get('MsgSeq') 65 | self.MsgType: str = self.EventMsg.get('MsgType') 66 | self.ToUin: int = self.EventMsg.get('ToUin') 67 | self.RedBaginfo = self.EventMsg.get('RedBaginfo') 68 | 69 | def __getitem__(self, key): 70 | return self.message[key] 71 | 72 | 73 | model_map = {'OnGroupMsgs': GroupMsg, 'OnFriendMsgs': FriendMsg, 'OnEvents': EventMsg} 74 | -------------------------------------------------------------------------------- /iotbot/plugin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import re 4 | from pathlib import Path 5 | from types import ModuleType 6 | from typing import Dict, List 7 | 8 | from prettytable import PrettyTable 9 | 10 | from . import json 11 | from .typing import EventMsgReceiver, FriendMsgReceiver, GroupMsgReceiver 12 | 13 | 14 | class Plugin: 15 | def __init__(self, module: ModuleType): 16 | self.module = module 17 | 18 | def reload(self): 19 | self.module = importlib.reload(self.module) 20 | 21 | @property 22 | def name(self): 23 | return Path(self.module.__file__).stem[4:] 24 | 25 | @property 26 | def receive_group_msg(self) -> GroupMsgReceiver: 27 | return self.module.__dict__.get('receive_group_msg') 28 | 29 | @property 30 | def receive_friend_msg(self) -> FriendMsgReceiver: 31 | return self.module.__dict__.get('receive_friend_msg') 32 | 33 | @property 34 | def receive_events(self) -> EventMsgReceiver: 35 | return self.module.__dict__.get('receive_events') 36 | 37 | 38 | class PluginManager: 39 | def __init__(self, plugin_dir: str = 'plugins'): 40 | self.plugin_dir = plugin_dir 41 | self._plugins: Dict[str, Plugin] = dict() 42 | self._removed_plugins: Dict[str, Plugin] = dict() 43 | 44 | # 本地缓存的停用的插件名称列表 45 | self._load_removed_plugin_names() 46 | 47 | def _load_removed_plugin_names(self): 48 | if os.path.exists('.REMOVED_PLUGINS'): 49 | with open('.REMOVED_PLUGINS', encoding='utf8') as f: 50 | self._removed_plugin_names = json.load(f)['plugins'] 51 | else: 52 | with open('.REMOVED_PLUGINS', 'w', encoding='utf8') as f: 53 | json.dump( 54 | {'tips': '用于存储已停用插件信息,请不要修改这个文件', 'plugins': []}, 55 | f, 56 | ensure_ascii=False, 57 | ) 58 | self._removed_plugin_names = [] 59 | 60 | def _update_removed_plugin_names(self): 61 | data = { 62 | 'tips': '用于存储已停用插件信息,请不要修改这个文件', 63 | 'plugins': list(set(self._removed_plugin_names)), # 去重,虽然显得多余 64 | } 65 | with open('.REMOVED_PLUGINS', 'w', encoding='utf8') as f: 66 | json.dump(data, f, ensure_ascii=False) 67 | 68 | def load_plugins(self, plugin_dir: str = None) -> None: 69 | if plugin_dir is None: 70 | plugin_dir = self.plugin_dir 71 | plugin_files = ( 72 | i for i in os.listdir(plugin_dir) if re.search(r'^bot_\w+\.py$', i) 73 | ) 74 | for plugin_file in plugin_files: 75 | module = importlib.import_module( 76 | '{}.{}'.format(plugin_dir.replace('/', '.'), plugin_file.split('.')[0]) 77 | ) 78 | plugin = Plugin(module) 79 | if plugin.name in self._removed_plugin_names: 80 | self._removed_plugins[plugin.name] = plugin 81 | else: 82 | self._plugins[plugin.name] = plugin 83 | 84 | def refresh(self, plugin_dir: str = None) -> None: 85 | '''reload all plugins''' 86 | return self.reload_plugins(plugin_dir) # 之前写错了,为了兼容留下来 87 | 88 | def reload_plugins(self, plugin_dir: str = None) -> None: 89 | '''reload old, load new.''' 90 | # reload old 91 | old_plugins = self._plugins.copy() 92 | for old_plugin in old_plugins.values(): 93 | old_plugins[old_plugin.name].reload() 94 | # load new 95 | self.load_plugins(plugin_dir) 96 | # tidy 97 | self._plugins.update(old_plugins) 98 | 99 | def reload_plugin(self, plugin_name: str) -> None: 100 | """reload one plugin according to plugin name 101 | whether the plugin exists or not, it will always keep quiet. 102 | """ 103 | if plugin_name in self._plugins: 104 | self._plugins[plugin_name].reload() 105 | 106 | def remove_plugin(self, plugin_name: str) -> None: 107 | '''remove not delete.''' 108 | try: 109 | if plugin_name in self._plugins: 110 | self._removed_plugins[plugin_name] = self._plugins.pop(plugin_name) 111 | # 缓存到本地 112 | self._removed_plugin_names.append(plugin_name) 113 | self._update_removed_plugin_names() 114 | except KeyError: # 可能由self._removed_plugins[plugin_name]引发 115 | pass 116 | 117 | def recover_plugin(self, plugin_name: str) -> None: 118 | '''recover plugin if it's in the removed plugins list.''' 119 | try: 120 | if plugin_name in self._removed_plugins: 121 | self._plugins[plugin_name] = self._removed_plugins.pop(plugin_name) 122 | if plugin_name in self._removed_plugin_names: 123 | self._removed_plugin_names.remove(plugin_name) 124 | self._update_removed_plugin_names() 125 | except KeyError: 126 | pass 127 | 128 | @property 129 | def plugins(self) -> List[str]: 130 | '''return a list of plugin name''' 131 | return list(self._plugins) 132 | 133 | @property 134 | def removed_plugins(self) -> List[str]: 135 | '''return a list of removed plugin name''' 136 | return list(self._removed_plugins) 137 | 138 | @property 139 | def friend_msg_receivers(self) -> List[FriendMsgReceiver]: 140 | '''funcs to handle (friend msg)context''' 141 | return [ 142 | plugin.receive_friend_msg 143 | for plugin in self._plugins.values() 144 | if plugin.receive_friend_msg 145 | ] 146 | 147 | @property 148 | def group_msg_receivers(self) -> List[GroupMsgReceiver]: 149 | '''funcs to handle (group msg)context''' 150 | return [ 151 | plugin.receive_group_msg 152 | for plugin in self._plugins.values() 153 | if plugin.receive_group_msg 154 | ] 155 | 156 | @property 157 | def event_receivers(self) -> List[EventMsgReceiver]: 158 | '''funcs to handle (event msg)context''' 159 | return [ 160 | plugin.receive_events 161 | for plugin in self._plugins.values() 162 | if plugin.receive_events 163 | ] 164 | 165 | @property 166 | def info_table(self) -> str: 167 | table = PrettyTable(['Receiver', 'Count', 'Info']) 168 | table.add_row( 169 | [ 170 | 'Friend Msg Receiver', 171 | len(self.friend_msg_receivers), 172 | '/'.join( 173 | [ 174 | f'{p.name}' 175 | for p in self._plugins.values() 176 | if p.receive_friend_msg 177 | ] 178 | ), 179 | ] 180 | ) 181 | table.add_row( 182 | [ 183 | 'Group Msg Receiver', 184 | len(self.group_msg_receivers), 185 | '/'.join( 186 | [f'{p.name}' for p in self._plugins.values() if p.receive_group_msg] 187 | ), 188 | ] 189 | ) 190 | table.add_row( 191 | [ 192 | 'Event Receiver', 193 | len(self.event_receivers), 194 | '/'.join( 195 | [f'{p.name}' for p in self._plugins.values() if p.receive_events] 196 | ), 197 | ] 198 | ) 199 | table_removed = PrettyTable(['Removed Plugins']) 200 | table_removed.add_row(['/'.join(self.removed_plugins)]) 201 | return str(table) + '\n' + str(table_removed) 202 | -------------------------------------------------------------------------------- /iotbot/refine.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-instance-attributes, super-init-not-called 2 | """进一步提取消息详细信息的函数""" 3 | import copy 4 | import functools 5 | from typing import List 6 | 7 | from . import json 8 | from .exceptions import ContextTypeError 9 | from .model import EventMsg, FriendMsg, GroupMsg 10 | from .utils import EventNames, MsgTypes 11 | 12 | 13 | def copy_ctx(f): 14 | @functools.wraps(f) 15 | def i(ctx): 16 | return f(copy.deepcopy(ctx)) 17 | 18 | return i 19 | 20 | 21 | #############################Event begin######################################## 22 | 23 | 24 | class _EventMsg(EventMsg): 25 | def _carry_properties(self, ctx: EventMsg): 26 | self.message = ctx.message 27 | self.CurrentQQ = ctx.CurrentQQ 28 | 29 | self.data = ctx.data 30 | 31 | self.EventName = ctx.EventName 32 | self.EventData = ctx.EventData 33 | self.EventMsg = ctx.EventMsg 34 | 35 | self.Content = ctx.Content 36 | self.FromUin = ctx.FromUin 37 | self.MsgSeq = ctx.MsgSeq 38 | self.MsgType = ctx.MsgType 39 | self.ToUin = ctx.ToUin 40 | self.RedBaginfo = ctx.RedBaginfo 41 | 42 | 43 | class _GroupRevokeEventMsg(_EventMsg): 44 | """群成员撤回消息事件""" 45 | 46 | def __init__(self, ctx: EventMsg): 47 | event_data = ctx.EventData 48 | self.AdminUserID: int = event_data.get('AdminUserID') 49 | self.GroupID: int = event_data.get('GroupID') 50 | self.MsgRandom: int = event_data.get('MsgRandom') 51 | self.MsgSeq: int = event_data.get('MsgSeq') 52 | self.UserID: int = event_data.get('UserID') 53 | super()._carry_properties(ctx) 54 | 55 | 56 | class _GroupExitEventMsg(_EventMsg): 57 | """群成员退出群聊事件""" 58 | 59 | def __init__(self, ctx: EventMsg): 60 | self.UserID = ctx.EventData.get('UserID') 61 | super()._carry_properties(ctx) 62 | 63 | 64 | class _GroupJoinEventMsg(_EventMsg): 65 | """某人进群事件""" 66 | 67 | def __init__(self, ctx: EventMsg): 68 | e_data = ctx.EventData 69 | self.InviteUin: int = e_data.get('InviteUin') 70 | self.UserID: int = e_data.get('UserID') 71 | self.UserName: str = e_data.get('UserName') 72 | super()._carry_properties(ctx) 73 | 74 | 75 | class _FriendRevokeEventMsg(_EventMsg): 76 | """好友撤回消息事件""" 77 | 78 | def __init__(self, ctx: EventMsg): 79 | self.MsgSeq = ctx.EventData.get('MsgSeq') 80 | self.UserID = ctx.EventData.get('UserID') 81 | super()._carry_properties(ctx) 82 | 83 | 84 | class _FriendDeleteEventMsg(_EventMsg): 85 | """删除好友事件""" 86 | 87 | def __init__(self, ctx: EventMsg): 88 | self.UserID: int = ctx.EventData.get('UserID') 89 | super()._carry_properties(ctx) 90 | 91 | 92 | class _GroupAdminsysnotifyEventMsg(_EventMsg): 93 | """QQ群系统消息通知(加群申请在这里面""" 94 | 95 | def __init__(self, ctx: EventMsg): 96 | edata = ctx.EventData 97 | self.Type: int = edata.get('Type') # 事件类型 98 | self.MsgTypeStr: str = edata.get('MsgTypeStr') # 消息类型 99 | self.MsgStatusStr: str = edata.get('MsgStatusStr') # 消息类型状态 100 | self.Who: int = edata.get('Who') # 触发消息的对象 101 | self.WhoName: int = edata.get('WhoName') # 触发消息的对象昵称 102 | self.GroupID: int = edata.get('GroupId') # 来自群 103 | self.GroupName: str = edata.get('GroupName') # 群名 104 | self.ActionUin: int = edata.get('ActionUin') # 邀请人(处理人) 105 | self.ActionName: str = edata.get('ActionName') # 邀请人(处理人)昵称 106 | self.ActionGroupCard: str = edata.get('ActionGroupCard') # 邀请人(处理人)群名片 107 | self.Action: str = edata.get('Action') # 加群理由 11 agree 14 忽略 12/21 disagree 108 | self.Content: int = edata.get('Content') 109 | super()._carry_properties(ctx) 110 | 111 | 112 | class _GroupShutEventMsg(_EventMsg): 113 | """群禁言事件""" 114 | 115 | def __init__(self, ctx: EventMsg): 116 | self.GroupID: int = ctx.EventData.get('GroupID') 117 | self.ShutTime: int = ctx.EventData.get('ShutTime') 118 | self.UserID: int = ctx.EventData.get('UserID') 119 | super()._carry_properties(ctx) 120 | 121 | 122 | class _GroupAdminEventMsg(_EventMsg): 123 | """管理员变更事件""" 124 | 125 | def __init__(self, ctx: EventMsg): 126 | self.Flag: int = ctx.EventData.get('Flag') 127 | self.GroupID: int = ctx.EventData.get('GroupID') 128 | self.UserID: int = ctx.EventData.get('UserID') 129 | super()._carry_properties(ctx) 130 | 131 | 132 | @copy_ctx 133 | def refine_group_revoke_event_msg(ctx: EventMsg) -> _GroupRevokeEventMsg: 134 | """群成员撤回消息事件""" 135 | if not isinstance(ctx, EventMsg): 136 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 137 | if ctx.EventName == EventNames.ON_EVENT_GROUP_REVOKE: 138 | return _GroupRevokeEventMsg(ctx) 139 | return None 140 | 141 | 142 | @copy_ctx 143 | def refine_group_exit_event_msg(ctx: EventMsg) -> _GroupExitEventMsg: 144 | """群成员退出群聊事件""" 145 | if not isinstance(ctx, EventMsg): 146 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 147 | if ctx.EventName == EventNames.ON_EVENT_GROUP_EXIT: 148 | return _GroupExitEventMsg(ctx) 149 | return None 150 | 151 | 152 | @copy_ctx 153 | def refine_group_join_event_msg(ctx: EventMsg) -> _GroupJoinEventMsg: 154 | """某人进群事件""" 155 | if not isinstance(ctx, EventMsg): 156 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 157 | if ctx.EventName == EventNames.ON_EVENT_GROUP_JOIN: 158 | return _GroupJoinEventMsg(ctx) 159 | return None 160 | 161 | 162 | @copy_ctx 163 | def refine_friend_revoke_event_msg(ctx: EventMsg) -> _FriendRevokeEventMsg: 164 | """好友撤回消息事件""" 165 | if not isinstance(ctx, EventMsg): 166 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 167 | if ctx.EventName == EventNames.ON_EVENT_FRIEND_REVOKE: 168 | return _FriendRevokeEventMsg(ctx) 169 | return None 170 | 171 | 172 | @copy_ctx 173 | def refine_friend_delete_event_msg(ctx: EventMsg) -> _FriendDeleteEventMsg: 174 | """删除好友事件""" 175 | if not isinstance(ctx, EventMsg): 176 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 177 | if ctx.EventName == EventNames.ON_EVENT_FRIEND_DELETE: 178 | return _FriendDeleteEventMsg(ctx) 179 | return None 180 | 181 | 182 | @copy_ctx 183 | def refine_group_adminsysnotify_event_msg( 184 | ctx: EventMsg, 185 | ) -> _GroupAdminsysnotifyEventMsg: 186 | """加群申请""" 187 | if not isinstance(ctx, EventMsg): 188 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 189 | if ctx.EventName == EventNames.ON_EVENT_GROUP_ADMINSYSNOTIFY: 190 | return _GroupAdminsysnotifyEventMsg(ctx) 191 | return None 192 | 193 | 194 | @copy_ctx 195 | def refine_group_shut_event_msg(ctx: EventMsg) -> _GroupShutEventMsg: 196 | """群禁言事件""" 197 | if not isinstance(ctx, EventMsg): 198 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 199 | if ctx.EventName == EventNames.ON_EVENT_GROUP_SHUT: 200 | return _GroupShutEventMsg(ctx) 201 | return None 202 | 203 | 204 | @copy_ctx 205 | def refine_group_admin_event_msg(ctx: EventMsg) -> _GroupAdminEventMsg: 206 | """管理员变更事件""" 207 | if not isinstance(ctx, EventMsg): 208 | raise ContextTypeError('Expected `EventMsg`, but got `%s`' % ctx.__class__) 209 | if ctx.EventName == EventNames.ON_EVENT_GROUP_ADMIN: 210 | return _GroupAdminEventMsg(ctx) 211 | return None 212 | 213 | 214 | #############################Event end########################################## 215 | 216 | 217 | #############################Group start######################################## 218 | class _GroupMsg(GroupMsg): 219 | def _carry_properties(self, ctx: GroupMsg): 220 | self.message = ctx.message 221 | self.CurrentQQ = ctx.CurrentQQ 222 | 223 | self.data = ctx.data 224 | 225 | self.FromGroupId: int = ctx.FromGroupId 226 | self.FromGroupName: str = ctx.FromGroupId 227 | self.FromUserId: int = ctx.FromUserId 228 | self.FromNickName: str = ctx.FromNickName 229 | self.Content: str = ctx.Content 230 | self.MsgType: str = ctx.MsgType 231 | self.MsgTime: int = ctx.MsgTime 232 | self.MsgSeq: int = ctx.MsgSeq 233 | self.MsgRandom: int = ctx.MsgRandom 234 | self.RedBaginfo: dict = ctx.RedBaginfo 235 | 236 | 237 | class _VoiceGroupMsg(_GroupMsg): 238 | """群语音消息""" 239 | 240 | def __init__(self, ctx: GroupMsg): 241 | voice_data = json.loads(ctx.Content) 242 | self.VoiceUrl: str = voice_data['Url'] 243 | self.Tips: str = voice_data['Tips'] 244 | super()._carry_properties(ctx) 245 | 246 | 247 | class _VideoGroupMsg(_GroupMsg): 248 | """群视频消息""" 249 | 250 | def __init__(self, ctx: GroupMsg): 251 | video_data = json.loads(ctx.Content) 252 | self.ForwordBuf: str = video_data['ForwordBuf'] 253 | self.ForwordField: int = video_data['ForwordField'] 254 | self.Tips: str = video_data['Tips'] 255 | self.VideoMd5: str = video_data['VideoMd5'] 256 | self.VideoSize: str = video_data['VideoSize'] 257 | self.VideoUrl: str = video_data['VideoUrl'] 258 | super()._carry_properties(ctx) 259 | 260 | 261 | class _GroupPic: 262 | def __init__(self, pic: dict): 263 | '''[{"FileId":2161733733,"FileMd5":"","FileSize":449416,"ForwordBuf":"","ForwordField":8,"Url":""}''' 264 | self.FileId: int = pic.get('FileId') 265 | self.FileMd5: str = pic.get('FileMd5') 266 | self.FileSize: int = pic.get('FileSize') 267 | self.ForwordBuf: str = pic.get('ForwordBuf') 268 | self.ForwordField: int = pic.get('ForwordField') 269 | self.Url: str = pic.get('Url') 270 | 271 | 272 | class _PicGroupMsg(_GroupMsg): 273 | """群图片/表情包消息""" 274 | 275 | def __init__(self, ctx: GroupMsg): 276 | pic_data = json.loads(ctx.Content) 277 | self.GroupPic: List[_GroupPic] = [_GroupPic(i) for i in pic_data['GroupPic']] 278 | self.Tips: str = pic_data['Tips'] 279 | super()._carry_properties(ctx) 280 | self.Content: str = pic_data.get('Content') 281 | 282 | 283 | class _AtGroupMsg(_GroupMsg): 284 | def __init__(self, ctx: GroupMsg): 285 | super()._carry_properties(ctx) 286 | 287 | 288 | class _RedBagGroupMsg(_GroupMsg): 289 | """群红包消息""" 290 | 291 | def __init__(self, ctx: GroupMsg): 292 | redbag_info = ctx.RedBaginfo 293 | self.RedBag_Authkey: str = redbag_info.get('Authkey') 294 | self.RedBag_Channel: int = redbag_info.get('Channel') 295 | self.RedBag_Des: str = redbag_info.get('Des') 296 | self.RedBag_FromType: int = redbag_info.get('FromType') 297 | self.RedBag_FromUin: int = redbag_info.get('FromUin') 298 | self.RedBag_Listid: str = redbag_info.get('Listid') 299 | self.RedBag_RedType: int = redbag_info.get('RedType') 300 | self.RedBag_StingIndex: str = redbag_info.get('StingIndex') 301 | self.RedBag_Tittle: str = redbag_info.get('Tittle') 302 | self.RedBag_Token_17_2: str = redbag_info.get('Token_17_2') 303 | self.RedBag_Token_17_3: str = redbag_info.get('Token_17_3') 304 | super()._carry_properties(ctx) 305 | 306 | 307 | @copy_ctx 308 | def refine_voice_group_msg(ctx: GroupMsg) -> _VoiceGroupMsg: 309 | """群语音消息""" 310 | if not isinstance(ctx, GroupMsg): 311 | raise ContextTypeError('Expected `GroupMsg`, but got `%s`' % ctx.__class__) 312 | if ctx.MsgType == MsgTypes.VoiceMsg: 313 | return _VoiceGroupMsg(ctx) 314 | return None 315 | 316 | 317 | @copy_ctx 318 | def refine_video_group_msg(ctx: GroupMsg) -> _VideoGroupMsg: 319 | """群视频消息""" 320 | if not isinstance(ctx, GroupMsg): 321 | raise ContextTypeError('Expected `GroupMsg`, but got `%s`' % ctx.__class__) 322 | if ctx.MsgType == MsgTypes.VideoMsg: 323 | return _VideoGroupMsg(ctx) 324 | return None 325 | 326 | 327 | @copy_ctx 328 | def refine_pic_group_msg(ctx: GroupMsg) -> _PicGroupMsg: 329 | """群图片/表情包消息""" 330 | if not isinstance(ctx, GroupMsg): 331 | raise ContextTypeError('Expected `GroupMsg`, but got `%s`' % ctx.__class__) 332 | if ctx.MsgType == MsgTypes.PicMsg: 333 | return _PicGroupMsg(ctx) 334 | return None 335 | 336 | 337 | @copy_ctx 338 | def refine_RedBag_group_msg(ctx: GroupMsg) -> _RedBagGroupMsg: 339 | """群红包消息""" 340 | if not isinstance(ctx, GroupMsg): 341 | raise ContextTypeError('Expected `GroupMsg`, but got `%s`' % ctx.__class__) 342 | if ctx.MsgType == MsgTypes.RedBagMsg: 343 | return _RedBagGroupMsg(ctx) 344 | return None 345 | 346 | 347 | #############################Group end########################################## 348 | 349 | 350 | #############################Friend start####################################### 351 | class _FriendMsg(FriendMsg): 352 | def _carry_properties(self, ctx: FriendMsg): 353 | self.message = ctx.message 354 | self.CurrentQQ = ctx.CurrentQQ 355 | 356 | self.data = ctx.data 357 | 358 | self.FromUin: int = ctx.FromUin 359 | self.ToUin: int = ctx.ToUin 360 | self.MsgType: str = ctx.MsgType 361 | self.MsgSeq: int = ctx.MsgSeq 362 | self.Content: str = ctx.Content 363 | self.RedBaginfo: dict = ctx.RedBaginfo 364 | 365 | 366 | class _VoiceFriendMsg(_FriendMsg): 367 | """好友语音消息""" 368 | 369 | def __init__(self, ctx: FriendMsg): 370 | voice_data = json.loads(ctx.Content) 371 | self.VoiceUrl: str = voice_data['Url'] 372 | self.Tips: str = voice_data['Tips'] 373 | super()._carry_properties(ctx) 374 | 375 | 376 | class _VideoFriendMsg(_FriendMsg): 377 | """好友视频消息""" 378 | 379 | def __init__(self, ctx: FriendMsg): 380 | video_data = json.loads(ctx.Content) 381 | self.ForwordBuf: str = video_data['ForwordBuf'] 382 | self.ForwordField: int = video_data['ForwordField'] 383 | self.Tips: str = video_data['Tips'] 384 | self.VideoMd5: str = video_data['VideoMd5'] 385 | self.VideoSize: str = video_data['VideoSize'] 386 | self.VideoUrl: str = video_data['VideoUrl'] 387 | super()._carry_properties(ctx) 388 | 389 | 390 | class _FriendPic: 391 | def __init__(self, pic: dict): 392 | """好友图片单个图片所包含的数据 393 | [{"FileMd5":"","FileSize":0,"Path":"","Url":""}]中的一个 394 | """ 395 | self.FileMd5: str = pic.get('FileMd5') 396 | self.FileSize: int = pic.get('FileSize') 397 | self.Path: str = pic.get('Path') 398 | self.Url: str = pic.get('Url') 399 | 400 | 401 | class _PicFriendMsg(_FriendMsg): 402 | """好友图片/表情包消息""" 403 | 404 | def __init__(self, ctx: FriendMsg): 405 | pic_data = json.loads(ctx.Content) 406 | self.FriendPic: List[_FriendPic] = [ 407 | _FriendPic(i) for i in pic_data['FriendPic'] 408 | ] 409 | self.Tips: str = pic_data['Tips'] 410 | super()._carry_properties(ctx) 411 | self.Content = pic_data.get('Content') 412 | 413 | 414 | class _RedBagFriendMsg(_FriendMsg): 415 | """好友红包消息""" 416 | 417 | def __init__(self, ctx: FriendMsg): 418 | redbag_info = ctx.RedBaginfo 419 | self.RedBag_Authkey: str = redbag_info.get('Authkey') 420 | self.RedBag_Channel: int = redbag_info.get('Channel') 421 | self.RedBag_Des: str = redbag_info.get('Des') 422 | self.RedBag_FromType: int = redbag_info.get('FromType') 423 | self.RedBag_FromUin: int = redbag_info.get('FromUin') 424 | self.RedBag_Listid: str = redbag_info.get('Listid') 425 | self.RedBag_RedType: int = redbag_info.get('RedType') 426 | self.RedBag_StingIndex: str = redbag_info.get('StingIndex') 427 | self.RedBag_Tittle: str = redbag_info.get('Tittle') 428 | self.RedBag_Token_17_2: str = redbag_info.get('Token_17_2') 429 | self.RedBag_Token_17_3: str = redbag_info.get('Token_17_3') 430 | super()._carry_properties(ctx) 431 | 432 | 433 | @copy_ctx 434 | def refine_voice_friend_msg(ctx: FriendMsg) -> _VoiceFriendMsg: 435 | """好友语音消息""" 436 | if not isinstance(ctx, FriendMsg): 437 | raise ContextTypeError('Expected `FriendMsg`, but got `%s`' % ctx.__class__) 438 | if ctx.MsgType == MsgTypes.VoiceMsg: 439 | return _VoiceFriendMsg(ctx) 440 | return None 441 | 442 | 443 | @copy_ctx 444 | def refine_video_friend_msg(ctx: FriendMsg) -> _VideoFriendMsg: 445 | """好友视频消息""" 446 | if not isinstance(ctx, FriendMsg): 447 | raise ContextTypeError('Expected `FriendMsg`, but got `%s`' % ctx.__class__) 448 | if ctx.MsgType == MsgTypes.VideoMsg: 449 | return _VideoFriendMsg(ctx) 450 | return None 451 | 452 | 453 | @copy_ctx 454 | def refine_pic_friend_msg(ctx: FriendMsg) -> _PicFriendMsg: 455 | """好友图片/表情包消息""" 456 | if not isinstance(ctx, FriendMsg): 457 | raise ContextTypeError('Expected `FriendMsg`, but got `%s`' % ctx.__class__) 458 | if ctx.MsgType == MsgTypes.PicMsg: 459 | return _PicFriendMsg(ctx) 460 | return None 461 | 462 | 463 | @copy_ctx 464 | def refine_RedBag_friend_msg(ctx: FriendMsg) -> _RedBagFriendMsg: 465 | """好友红包消息""" 466 | if not isinstance(ctx, FriendMsg): 467 | raise ContextTypeError('Expected `FriendMsg`, but got `%s`' % ctx.__class__) 468 | if ctx.MsgType == MsgTypes.RedBagMsg: 469 | return _RedBagFriendMsg(ctx) 470 | return None 471 | 472 | 473 | #############################Friend end######################################### 474 | -------------------------------------------------------------------------------- /iotbot/sugar.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access 2 | """封装部分最常用发送操作 3 | 使用的前提: 4 | 已经通过.iotbot.json配置好地址和端口 或者 地址端口保持默认值,即127.0.0.1:8888 5 | """ 6 | import sys 7 | from typing import Union 8 | 9 | from .action import Action 10 | from .exceptions import ContextTypeError 11 | from .model import FriendMsg, GroupMsg 12 | from .utils import file_to_base64 13 | 14 | 15 | def Text(text: str, at=False): 16 | """发送文字 经支持群消息和好友消息接收函数内调用 17 | :param text: 文字内容 18 | :param at:是否艾特发送该消息的用户 19 | """ 20 | text = str(text) 21 | # 查找消息上下文 `ctx`变量 22 | ctx = None 23 | f = sys._getframe() 24 | upper = f.f_back 25 | upper_locals = upper.f_locals 26 | if 'ctx' in upper_locals and isinstance(upper_locals['ctx'], (FriendMsg, GroupMsg)): 27 | ctx = upper_locals['ctx'] 28 | else: 29 | for v in upper_locals.values(): 30 | if isinstance(v, (GroupMsg, FriendMsg)): 31 | ctx = v 32 | break 33 | if ctx is None: 34 | raise ContextTypeError('经支持群消息和好友消息接收函数内调用') 35 | 36 | action = Action(ctx.CurrentQQ) 37 | 38 | if isinstance(ctx, GroupMsg): 39 | return action.send_group_text_msg( 40 | ctx.FromGroupId, content=text, atUser=ctx.FromUserId if at else 0 41 | ) 42 | if isinstance(ctx, FriendMsg): 43 | if ctx.TempUin: 44 | return action.send_private_text_msg( 45 | toUser=ctx.FromUin, content=text, groupid=ctx.TempUin 46 | ) 47 | else: 48 | return action.send_friend_text_msg(ctx.FromUin, text) 49 | return None 50 | 51 | 52 | def Picture(pic_url='', pic_base64='', pic_path='', content=''): 53 | """发送图片 经支持群消息和好友消息接收函数内调用 54 | :param pic_url: 图片链接 55 | :param pic_base64: 图片base64编码 56 | :param pic_path: 图片文件路径 57 | :param content: 包含的文字消息 58 | 59 | pic_url, pic_base64, pic_path必须给定一项 60 | """ 61 | assert any([pic_url, pic_base64, pic_path]), '必须给定一项' 62 | 63 | ctx = None 64 | f = sys._getframe() 65 | upper = f.f_back 66 | upper_locals = upper.f_locals 67 | if 'ctx' in upper_locals and isinstance(upper_locals['ctx'], (FriendMsg, GroupMsg)): 68 | ctx = upper_locals['ctx'] 69 | else: 70 | for v in upper_locals.values(): 71 | if isinstance(v, (FriendMsg, GroupMsg)): 72 | ctx = v 73 | break 74 | if ctx is None: 75 | raise ContextTypeError('经支持群消息和好友消息接收函数内调用') 76 | 77 | action = Action(ctx.CurrentQQ) 78 | 79 | if isinstance(ctx, GroupMsg): 80 | if pic_url: 81 | return action.send_group_pic_msg( 82 | ctx.FromGroupId, picUrl=pic_url, content=content 83 | ) 84 | elif pic_base64: 85 | return action.send_group_pic_msg( 86 | ctx.FromGroupId, picBase64Buf=pic_base64, content=content 87 | ) 88 | elif pic_path: 89 | return action.send_group_pic_msg( 90 | ctx.FromGroupId, picBase64Buf=file_to_base64(pic_path), content=content 91 | ) 92 | if isinstance(ctx, FriendMsg): 93 | if pic_url: 94 | if ctx.TempUin: 95 | return action.send_private_pic_msg( 96 | toUser=ctx.FromUin, 97 | groupid=ctx.TempUin, 98 | picUrl=pic_url, 99 | content=content, 100 | ) 101 | else: 102 | return action.send_friend_pic_msg( 103 | ctx.FromUin, picUrl=pic_url, content=content 104 | ) 105 | elif pic_base64: 106 | if ctx.TempUin: 107 | return action.send_private_pic_msg( 108 | toUser=ctx.FromUin, 109 | groupid=ctx.TempUin, 110 | picBase64Buf=pic_base64, 111 | content=content, 112 | ) 113 | else: 114 | return action.send_friend_pic_msg( 115 | ctx.FromUin, picBase64Buf=pic_base64, content=content 116 | ) 117 | elif pic_path: 118 | if ctx.TempUin: 119 | return action.send_private_pic_msg( 120 | toUser=ctx.FromUin, 121 | groupid=ctx.TempUin, 122 | picBase64Buf=file_to_base64(pic_path), 123 | content=content, 124 | ) 125 | else: 126 | return action.send_friend_pic_msg( 127 | ctx.FromUin, picBase64Buf=file_to_base64(pic_path), content=content 128 | ) 129 | return None 130 | 131 | 132 | def Voice(voice_url='', voice_base64='', voice_path=''): 133 | """发送语音 经支持群消息和好友消息接收函数内调用 134 | :param voice_url: 语音链接 135 | :param voice_base64: 语音base64编码 136 | :param voice_path: 语音文件路径 137 | 138 | voice_url, voice_base64, voice_path必须给定一项 139 | """ 140 | assert any([voice_url, voice_base64, voice_path]), '必须给定一项' 141 | 142 | ctx = None 143 | f = sys._getframe() 144 | upper = f.f_back 145 | upper_locals = upper.f_locals 146 | if 'ctx' in upper_locals and isinstance(upper_locals['ctx'], (FriendMsg, GroupMsg)): 147 | ctx = upper_locals['ctx'] 148 | else: 149 | for v in upper_locals.values(): 150 | if isinstance(v, (GroupMsg, FriendMsg)): 151 | ctx = v 152 | break 153 | if ctx is None: 154 | raise ContextTypeError('经支持群消息和好友消息接收函数内调用') 155 | 156 | action = Action(ctx.CurrentQQ) 157 | 158 | if isinstance(ctx, GroupMsg): 159 | if voice_url: 160 | return action.send_group_voice_msg(ctx.FromGroupId, voiceUrl=voice_url) 161 | elif voice_base64: 162 | return action.send_group_voice_msg( 163 | ctx.FromGroupId, voiceBase64Buf=voice_base64 164 | ) 165 | elif voice_path: 166 | return action.send_group_voice_msg( 167 | ctx.FromGroupId, voiceBase64Buf=file_to_base64(voice_path) 168 | ) 169 | if isinstance(ctx, FriendMsg): 170 | if voice_url: 171 | if ctx.TempUin: 172 | return action.send_private_voice_msg( 173 | toUser=ctx.FromUin, groupid=ctx.TempUin, voiceUrl=voice_url 174 | ) 175 | else: 176 | return action.send_friend_voice_msg(ctx.FromUin, voiceUrl=voice_url) 177 | elif voice_base64: 178 | if ctx.TempUin: 179 | return action.send_private_voice_msg( 180 | toUser=ctx.FromUin, groupid=ctx.TempUin, voiceBase64Buf=voice_base64 181 | ) 182 | else: 183 | return action.send_friend_voice_msg( 184 | ctx.FromUin, voiceBase64Buf=voice_base64 185 | ) 186 | elif voice_path: 187 | if ctx.TempUin: 188 | return action.send_private_voice_msg( 189 | toUser=ctx.FromUin, 190 | groupid=ctx.TempUin, 191 | voiceBase64Buf=file_to_base64(voice_path), 192 | ) 193 | else: 194 | return action.send_friend_voice_msg( 195 | ctx.FromUin, voiceBase64Buf=file_to_base64(voice_path) 196 | ) 197 | return None 198 | 199 | 200 | def Send( 201 | ctx: Union[FriendMsg, GroupMsg], 202 | *, 203 | text: str = '', 204 | pic_url: str = '', 205 | pic_base64: str = '', 206 | pic_path: str = '', 207 | voice_url: str = '', 208 | voice_base64: str = '', 209 | voice_path: str = '' 210 | ): 211 | """ 212 | 根据给定参数自动选择发送方式, 支持对群聊、好友(包括私聊) 213 | 所有参数均为可选参数 214 | 图片和语音支持的三种参数类型,分别三选一,如果传入多个则优先级为: url > base64 > path 215 | """ 216 | assert isinstance(ctx, (FriendMsg, GroupMsg)) 217 | if text: 218 | Text(text) 219 | if any((pic_url, pic_base64, pic_path)): 220 | Picture(pic_url, pic_base64, pic_path) 221 | if any((voice_url, voice_base64, voice_path)): 222 | Voice(voice_url, voice_base64, voice_path) 223 | -------------------------------------------------------------------------------- /iotbot/template.py: -------------------------------------------------------------------------------- 1 | from iotbot import IOTBOT, Action, EventMsg, FriendMsg, GroupMsg 2 | 3 | bot_qq = 11 4 | bot = IOTBOT(bot_qq, use_plugins=False) 5 | action = Action(bot) 6 | 7 | 8 | @bot.on_group_msg 9 | def on_group_msg(ctx: GroupMsg): 10 | print(ctx.message) 11 | if ctx.Content == '刷新插件': 12 | bot.refresh_plugins() 13 | if ctx.Content == '.test': 14 | action.send_group_pic_msg(ctx.FromGroupId, 'http://url.cn/cqjYvc06') 15 | 16 | 17 | @bot.on_friend_msg 18 | def on_friend_msg(ctx: FriendMsg): 19 | print(ctx.message) 20 | if ctx.Content == '.test': 21 | action.send_friend_pic_msg(ctx.FromUin, picUrl='http://url.cn/cqjYvc06') 22 | 23 | 24 | @bot.on_event 25 | def on_event(ctx: EventMsg): 26 | print(ctx.message) 27 | print(ctx.EventName) 28 | 29 | 30 | def test_group(ctx: GroupMsg): 31 | print('In test_group', ctx.FromNickName) 32 | 33 | 34 | bot.add_group_msg_receiver(test_group) 35 | 36 | if __name__ == "__main__": 37 | print(bot.receivers) 38 | bot.run() 39 | -------------------------------------------------------------------------------- /iotbot/typing.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | 3 | from .model import EventMsg, FriendMsg, GroupMsg 4 | 5 | GroupMsgReceiver = Callable[[GroupMsg], Any] 6 | FriendMsgReceiver = Callable[[FriendMsg], Any] 7 | EventMsgReceiver = Callable[[EventMsg], Any] 8 | -------------------------------------------------------------------------------- /iotbot/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | 4 | 5 | class MsgTypes: 6 | """消息类型""" 7 | 8 | AtMsg = 'AtMsg' 9 | PicMsg = 'PicMsg' 10 | TextMsg = 'TextMsg' 11 | ReplyMsg = 'ReplyMsg' 12 | VoiceMsg = 'VoiceMsg' 13 | VideoMsg = 'VideoMsg' 14 | PhoneMsg = 'PhoneMsg' 15 | RedBagMsg = 'RedBagMsg' 16 | TempSessionMsg = 'TempSessionMsg' 17 | 18 | 19 | class EventNames: 20 | """事件类型""" 21 | 22 | ON_EVENT_GROUP_REVOKE = 'ON_EVENT_GROUP_REVOKE' 23 | ON_EVENT_GROUP_EXIT = 'ON_EVENT_GROUP_EXIT' 24 | ON_EVENT_GROUP_JOIN = 'ON_EVENT_GROUP_JOIN' 25 | ON_EVENT_GROUP_ADMINSYSNOTIFY = 'ON_EVENT_GROUP_ADMINSYSNOTIFY' 26 | ON_EVENT_FRIEND_REVOKE = 'ON_EVENT_FRIEND_REVOKE' 27 | ON_EVENT_FRIEND_DELETE = 'ON_EVENT_FRIEND_DELETE' 28 | ON_EVENT_GROUP_SHUT = 'ON_EVENT_GROUP_SHUT' 29 | ON_EVENT_GROUP_ADMIN = 'ON_EVENT_GROUP_ADMIN' 30 | 31 | 32 | # pylint: disable=non-ascii-name 33 | class Emoticons: 34 | """表情""" 35 | 36 | k歌 = '[表情140]' 37 | 爱你 = '[表情122]' 38 | 爱情 = '[表情42]' 39 | 爱心 = '[表情66]' 40 | 傲慢 = '[表情23]' 41 | 白眼 = '[表情22]' 42 | 棒棒糖 = '[表情142]' 43 | 抱拳 = '[表情118]' 44 | 爆筋 = '[表情142]' 45 | 鄙视 = '[表情105]' 46 | 闭嘴 = '[表情7]' 47 | 鞭炮 = '[表情137]' 48 | 便便 = '[表情59]' 49 | 不 = '[表情123]' 50 | 搽汗 = '[表情97]' 51 | 彩球 = '[表情164]' 52 | 菜刀 = '[表情112]' 53 | 差劲 = '[表情121]' 54 | 钞票 = '[表情158]' 55 | 车厢 = '[表情154]' 56 | 呲牙 = '[表情13]' 57 | 打伞 = '[表情163]' 58 | 大兵 = '[表情29]' 59 | 大哭 = '[表情9]' 60 | 蛋糕 = '[表情53]' 61 | 刀 = '[表情56]' 62 | 得意 = '[表情4]' 63 | 灯笼 = '[表情138]' 64 | 灯泡 = '[表情160]' 65 | 凋谢 = '[表情64]' 66 | 多云 = '[表情156]' 67 | 发财 = '[表情139]' 68 | 发呆 = '[表情3]' 69 | 发抖 = '[表情41]' 70 | 发怒 = '[表情11]' 71 | 饭 = '[表情61]' 72 | 飞机 = '[表情151]' 73 | 飞吻 = '[表情85]' 74 | 奋斗 = '[表情30]' 75 | 风车 = '[表情161]' 76 | 尴尬 = '[表情10]' 77 | 高铁右车头 = '[表情155]' 78 | 高铁左车头 = '[表情153]' 79 | 勾引 = '[表情119]' 80 | 购物 = '[表情141]' 81 | 鼓掌 = '[表情99]' 82 | 哈欠 = '[表情104]' 83 | 害羞 = '[表情6]' 84 | 憨笑 = '[表情28]' 85 | 好 = '[表情124]' 86 | 喝彩 = '[表情144]' 87 | 喝奶 = '[表情148]' 88 | 坏笑 = '[表情101]' 89 | 揉手 = '[表情129]' 90 | 回头 = '[表情127]' 91 | 饥饿 = '[表情24]' 92 | 激动 = '[表情130]' 93 | 街舞 = '[表情131]' 94 | 惊恐 = '[表情26]' 95 | 惊讶 = '[表情0]' 96 | 咖啡 = '[表情60]' 97 | 开车 = '[表情152]' 98 | 磕头 = '[表情126]' 99 | 可爱 = '[表情21]' 100 | 可怜 = '[表情111]' 101 | 抠鼻 = '[表情98]' 102 | 骷髅 = '[表情37]' 103 | 酷 = '[表情16]' 104 | 快哭了 = '[表情107]' 105 | 困 = '[表情25]' 106 | 篮球 = '[表情114]' 107 | 冷汗 = '[表情96]' 108 | 礼物 = '[表情69]' 109 | 流汗 = '[表情27]' 110 | 流泪 = '[表情5]' 111 | 玫瑰 = '[表情63]' 112 | 难过 = '[表情15]' 113 | 闹钟 = '[表情162]' 114 | 怄火 = '[表情86]' 115 | 啤酒 = '[表情113]' 116 | 瓢虫 = '[表情117]' 117 | 撇嘴 = '[表情1]' 118 | 乒乓 = '[表情115]' 119 | 祈祷 = '[表情145]' 120 | 强 = '[表情76]' 121 | 敲打 = '[表情38]' 122 | 亲亲 = '[表情109]' 123 | 青蛙 = '[表情170]' 124 | 糗大了 = '[表情100]' 125 | 拳头 = '[表情120]' 126 | 弱 = '[表情77]' 127 | 色 = '[表情2]' 128 | 沙发 = '[表情166]' 129 | 闪电 = '[表情54]' 130 | 胜利 = '[表情79]' 131 | 示爱 = '[表情110]' 132 | 手枪 = '[表情169]' 133 | 衰 = '[表情36]' 134 | 帅 = '[表情143]' 135 | 双喜 = '[表情136]' 136 | 睡 = '[表情8]' 137 | 太阳 = '[表情74]' 138 | 调皮 = '[表情12]' 139 | 跳绳 = '[表情128]' 140 | 跳跳 = '[表情43]' 141 | 偷笑 = '[表情20]' 142 | 吐 = '[表情19]' 143 | 微笑 = '[表情14]' 144 | 委屈 = '[表情106]' 145 | 握手 = '[表情78]' 146 | 西瓜 = '[表情89]' 147 | 下面 = '[表情149]' 148 | 下雨 = '[表情157]' 149 | 吓 = '[表情110]' 150 | 献吻 = '[表情132]' 151 | 香蕉 = '[表情150]' 152 | 心碎 = '[表情67]' 153 | 熊猫 = '[表情159]' 154 | 药 = '[表情168]' 155 | 疑问 = '[表情32]' 156 | 阴险 = '[表情108]' 157 | 拥抱 = '[表情49]' 158 | 邮件 = '[表情142]' 159 | 右哼哼 = '[表情103]' 160 | 右太极 = '[表情134]' 161 | 月亮 = '[表情75]' 162 | 晕 = '[表情34]' 163 | 再见 = '[表情39]' 164 | 炸弹 = '[表情55]' 165 | 折磨 = '[表情35]' 166 | 纸巾 = '[表情167]' 167 | 咒骂 = '[表情31]' 168 | 猪头 = '[表情46]' 169 | 抓狂 = '[表情18]' 170 | 转圈 = '[表情125]' 171 | 足球 = '[表情57]' 172 | 钻戒 = '[表情165]' 173 | 左哼哼 = '[表情102]' 174 | 左太极 = '[表情133]' 175 | 176 | 177 | def file_to_base64(path): 178 | with open(path, 'rb') as f: 179 | content = f.read() 180 | return base64.b64encode(content).decode() 181 | 182 | 183 | def check_schema(url: str) -> str: 184 | url = url.strip('/') 185 | if not re.findall(r'(http://|https://)', url): 186 | return "http://" + url 187 | return url 188 | -------------------------------------------------------------------------------- /iotbot/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.7.4' 2 | -------------------------------------------------------------------------------- /iotbot/webhook.py: -------------------------------------------------------------------------------- 1 | # 该功能即是一个内置插件 2 | import traceback 3 | 4 | import requests 5 | 6 | from . import sugar 7 | from .config import config 8 | from .exceptions import InvalidConfigError 9 | from .model import EventMsg, FriendMsg, GroupMsg 10 | 11 | 12 | def _check_config(): 13 | if not config.webhook_post_url: 14 | raise InvalidConfigError('缺少配置项: webhook_post_url') 15 | 16 | 17 | _check_config() 18 | 19 | 20 | def receive_group_msg(ctx: GroupMsg): 21 | try: 22 | resp = requests.post( 23 | config.webhook_post_url, json=ctx.message, timeout=config.webhook_timeout 24 | ) 25 | resp.raise_for_status() 26 | except Exception: 27 | print(traceback.format_exc()) 28 | else: 29 | try: 30 | data = resp.json() 31 | assert isinstance(data, dict) 32 | except Exception: 33 | pass 34 | else: 35 | # 取出所有支持的字段 36 | # 1. 图片(pic)存在,发送图片消息,此时文字(msg)存在, 则发送图文消息 37 | # 2. 图片(pic)不存在, 文字(msg)存在,单独发送文字消息 38 | # 3. 语音(voice)只要存在,则发送 39 | msg: str = data.get('msg') or '' 40 | at: bool = bool(data.get('at')) or False # 只要存在值就判定真 41 | pic_url: str = data.get('pic_url') 42 | pic_base64: str = data.get('pic_base64') 43 | voice_url: str = data.get('voice_url') 44 | voice_base64: str = data.get('voice_base64') 45 | if any([pic_url, pic_base64]): # 图片,纯文字二选一 46 | sugar.Picture(pic_url=pic_url, pic_base64=pic_base64, content=msg) 47 | elif msg: 48 | sugar.Text(msg, at) 49 | if any([voice_url, voice_base64]): 50 | sugar.Voice(voice_url=voice_url, voice_base64=voice_base64) 51 | return None 52 | return None 53 | 54 | 55 | def receive_friend_msg(ctx: FriendMsg): 56 | try: 57 | resp = requests.post( 58 | config.webhook_post_url, json=ctx.message, timeout=config.webhook_timeout 59 | ) 60 | resp.raise_for_status() 61 | except Exception: 62 | print(traceback.format_exc()) 63 | else: 64 | try: 65 | data = resp.json() 66 | assert isinstance(data, dict) 67 | except Exception: 68 | pass 69 | else: 70 | # 取出所有支持的字段 71 | # 1. 图片(pic)存在,发送图片消息,此时文字(msg)存在, 则发送图文消息 72 | # 2. 图片(pic)不存在, 文字(msg)存在,单独发送文字消息 73 | # 3. 语音(voice)只要存在,则发送 74 | msg: str = data.get('msg') or '' 75 | at: bool = bool(data.get('at')) or False # 只要存在值就判定真 76 | pic_url: str = data.get('pic_url') 77 | pic_base64: str = data.get('pic_base64') 78 | voice_url: str = data.get('voice_url') 79 | voice_base64: str = data.get('voice_base64') 80 | if any([pic_url, pic_base64]): # 图片,纯文字二选一 81 | sugar.Picture(pic_url=pic_url, pic_base64=pic_base64, content=msg) 82 | elif msg: 83 | sugar.Text(msg, at) 84 | if any([voice_url, voice_base64]): 85 | sugar.Voice(voice_url=voice_url, voice_base64=voice_base64) 86 | return None 87 | return None 88 | 89 | 90 | def receive_events(ctx: EventMsg): 91 | # 事件消息只上报(懒) 92 | try: 93 | requests.post( 94 | config.webhook_post_url, json=ctx.message, timeout=config.webhook_timeout 95 | ) 96 | except Exception: 97 | pass 98 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | python ./setup.py sdist bdist_wheel 4 | twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD 5 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | 一些单纯简单地发送文字和图片的插件,还是建议使用原生支持的 lua 写插件 2 | 3 | `bot.py` 4 | 5 | - 召唤群友 6 | - nmsl 7 | - 舔我 8 | - 插件管理 9 | -------------------------------------------------------------------------------- /sample/bot.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from iotbot import IOTBOT, Action, GroupMsg 4 | 5 | bot_qq = 123 6 | bot = IOTBOT(bot_qq, use_plugins=True, plugin_dir='plugins') 7 | action = Action(bot) 8 | 9 | 10 | # 群消息中间件使用示例 11 | def group_ctx_middleware(ctx): 12 | ctx.master = 333 # 主人qq 13 | return ctx # 2.7.1新增,必须返回原来的对象,否则消息永远不会被调用 14 | 15 | 16 | bot.register_group_context_middleware(group_ctx_middleware) 17 | #################### 18 | 19 | 20 | @bot.on_group_msg 21 | def on_group_msg(ctx: GroupMsg): 22 | content = ctx.Content 23 | # 召唤群友(回执消息) 24 | if ctx.FromUserId == ctx.master and content == '召唤群友': 25 | card = """1""" 26 | action.send_group_xml_msg(ctx.FromGroupId, content=card) 27 | return 28 | 29 | # nmsl 30 | if content == 'nmsl': 31 | action.send_group_text_msg( 32 | ctx.FromGroupId, 33 | requests.get('https://nmsl.shadiao.app/api.php?level=min&lang=zh_cn').text, 34 | ) 35 | return 36 | 37 | # 舔我 38 | if content == '舔我': 39 | action.send_group_text_msg( 40 | ctx.FromGroupId, 41 | '\n' + requests.get('https://chp.shadiao.app/api.php').text, 42 | ctx.FromUserId, 43 | ) 44 | return 45 | 46 | 47 | @bot.on_group_msg 48 | def manage_plugin(ctx: GroupMsg): 49 | if ctx.FromUserId != ctx.master: 50 | return 51 | c = ctx.Content 52 | if c == '插件管理': 53 | action.send_group_text_msg( 54 | ctx.FromGroupId, 55 | ( 56 | 'py插件 => 发送启用插件列表\n' 57 | '已停用py插件 => 发送停用插件列表\n' 58 | '刷新py插件 => 刷新所有插件,包括新建文件\n' 59 | '重载py插件+插件名 => 重载指定插件\n' 60 | '停用py插件+插件名 => 停用指定插件\n' 61 | '启用py插件+插件名 => 启用指定插件\n' 62 | ), 63 | ) 64 | return 65 | # 发送启用插件列表 66 | if c == 'py插件': 67 | action.send_group_text_msg(ctx.FromGroupId, '\n'.join(bot.plugins)) 68 | return 69 | # 发送停用插件列表 70 | if c == '已停用py插件': 71 | action.send_group_text_msg(ctx.FromGroupId, '\n'.join(bot.removed_plugins)) 72 | return 73 | with __import__('threading').Lock(): 74 | try: 75 | if c == '刷新py插件': 76 | bot.refresh_plugins() 77 | # 重载指定插件 重载py插件+[插件名] 78 | elif c.startswith('重载py插件'): 79 | plugin_name = c[6:] 80 | bot.reload_plugin(plugin_name) 81 | # 停用指定插件 停用py插件+[插件名] 82 | elif c.startswith('停用py插件'): 83 | plugin_name = c[6:] 84 | bot.remove_plugin(plugin_name) 85 | # 启用指定插件 启用py插件+[插件名] 86 | elif c.startswith('启用py插件'): 87 | plugin_name = c[6:] 88 | bot.recover_plugin(plugin_name) 89 | except Exception as e: 90 | action.send_group_text_msg(ctx.FromGroupId, '操作失败: %s' % e) 91 | 92 | 93 | if __name__ == "__main__": 94 | bot.run() 95 | -------------------------------------------------------------------------------- /sample/plugins/README.md: -------------------------------------------------------------------------------- 1 | | 名称 | 功能 | 2 | | ----------- | --------------------------- | 3 | | 163_comment | 网易云热评 | 4 | | auto_repeat | 自动加 1(回复) | 5 | | cmd | 执行系统命令 | 6 | | event | 加群退群提醒 | 7 | | morning | 极其简陋的早安回复 | 8 | | phlogo | 快速生成 p 站风格 logo 图片 | 9 | | pic | 发图 | 10 | | qrcode | 生成二维码 | 11 | | replay | 升级版复读机 | 12 | | setu_v2 | 发二次元 se 图 | 13 | | stop_revoke | 防撤回 | 14 | | sysinfo | 系统信息 | 15 | | to_card | 转卡片,可用于调试 | 16 | | verse | 诗句 | 17 | | auto_revoke | 关键字自动撤回 | 18 | | whatis | 查网络缩写词意思 | 19 | -------------------------------------------------------------------------------- /sample/plugins/bot_163_comment.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from iotbot import Action, GroupMsg 4 | from iotbot.decorators import equal_content, not_botself 5 | 6 | # 推荐用lua写 7 | 8 | 9 | @not_botself 10 | @equal_content('网易云热评') 11 | def receive_group_msg(ctx: GroupMsg): 12 | try: 13 | rep = requests.get('https://www.mouse123.cn/api/163/api.php', timeout=10) 14 | rep.raise_for_status() 15 | data = rep.json() 16 | Action(ctx.CurrentQQ).send_group_pic_msg( 17 | ctx.FromGroupId, 18 | content='歌曲: {title}\n歌手: {author}\n评论: {comment}'.format( 19 | title=data['title'], 20 | author=data['author'], 21 | comment=data['comment_content'], 22 | ), 23 | picUrl=data['images'], 24 | ) 25 | except Exception: 26 | pass 27 | -------------------------------------------------------------------------------- /sample/plugins/bot_auto_repeat.py: -------------------------------------------------------------------------------- 1 | import os 2 | from queue import deque 3 | 4 | from iotbot import GroupMsg 5 | from iotbot.decorators import not_botself, only_these_groups 6 | from iotbot.sugar import Text 7 | 8 | # 连续多少条相同信息时+1 9 | big_mouth_len = int(os.getenv('big_mouth_len') or 4) 10 | big_mouth_deque = deque(maxlen=big_mouth_len) 11 | for i in range(big_mouth_len): 12 | big_mouth_deque.append(i) 13 | 14 | 15 | @not_botself 16 | @only_these_groups([11111111111]) 17 | def receive_group_msg(ctx: GroupMsg): 18 | content = ctx.Content 19 | if len(content) < 30: 20 | big_mouth_deque.append(content) 21 | if len(set(big_mouth_deque)) == 1: 22 | Text(content) 23 | for i in range(big_mouth_len): 24 | big_mouth_deque.append(i) 25 | -------------------------------------------------------------------------------- /sample/plugins/bot_auto_revoke.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import re 4 | import time 5 | 6 | from iotbot import Action, GroupMsg 7 | from iotbot.decorators import in_content, is_botself 8 | 9 | # 机器人自己发送的消息中包含关键字 revoke 就自动撤回 10 | # 仅包含关键字 随机30s-80s后撤回 11 | # revoke[10] => 10s后撤回 12 | # revoke[20] => 20s后撤回 13 | 14 | 15 | @is_botself 16 | @in_content('revoke') 17 | def receive_group_msg(ctx: GroupMsg): 18 | delay = re.findall(r'revoke\[(\d+)\]', ctx.Content) 19 | if delay: 20 | delay = min(int(delay[0]), 90) 21 | else: 22 | random.seed(os.urandom(30)) 23 | delay = random.randint(30, 80) 24 | time.sleep(delay) 25 | 26 | Action(ctx.CurrentQQ).revoke_msg( 27 | groupid=ctx.FromGroupId, msgseq=ctx.MsgSeq, msgrandom=ctx.MsgRandom 28 | ) 29 | -------------------------------------------------------------------------------- /sample/plugins/bot_cmd.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iotbot import Action, GroupMsg 4 | 5 | master = 1234567 # 只允许自己用 6 | 7 | 8 | def receive_group_msg(ctx: GroupMsg): 9 | if ctx.FromUserId == master and ctx.Content.startswith('cmd'): 10 | try: 11 | msg = str( 12 | os.popen( 13 | ctx.Content.replace('sudo', '') 14 | .replace('rm', '') 15 | .replace('cmd', '') 16 | .strip() 17 | ).read() 18 | ) 19 | except Exception: 20 | msg = 'error' 21 | finally: 22 | Action(ctx.CurrentQQ).send_group_text_msg(ctx.FromGroupId, content=msg) 23 | -------------------------------------------------------------------------------- /sample/plugins/bot_event.py: -------------------------------------------------------------------------------- 1 | from iotbot import Action, EventMsg 2 | 3 | 4 | def receive_events(ctx: EventMsg): 5 | # 这样写是因为第一次整这个插件时事件上下文还是字典, 6 | # 这里我因为懒得改了,所以这样写 7 | # 用对象的字段会方便和简洁一点,自己修改吧 8 | ctx = ctx.message # 9 | ###################### 10 | action = Action(ctx['CurrentQQ']) 11 | data = ctx['CurrentPacket']['Data'] 12 | 13 | # 欢迎新群员 14 | if data['EventName'] == "ON_EVENT_GROUP_JOIN": 15 | uid = data['EventData']['UserID'] 16 | uname = data['EventData']['UserName'] 17 | gid = data['EventMsg']['FromUin'] 18 | action.send_group_text_msg(gid, '欢迎<%s>入群![表情175]' % uname, uid) 19 | return 20 | 21 | # 群友退群 22 | if data['EventName'] == "ON_EVENT_GROUP_EXIT": 23 | uid = data['EventData']['UserID'] 24 | gid = data['EventMsg']['FromUin'] 25 | action.send_group_text_msg(gid, f'群友【{uid}】\n[表情107]离开了本群!\n[表情66]请珍惜在一起的每一分钟!') 26 | return 27 | -------------------------------------------------------------------------------- /sample/plugins/bot_morning.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from iotbot.decorators import equal_content 4 | from iotbot.sugar import Text 5 | 6 | 7 | def get_msg(): 8 | now_ = datetime.now() 9 | hour = now_.hour 10 | minute = now_.minute 11 | now = hour + minute / 60 12 | 13 | if 5.5 < now < 8: 14 | msg = '早~' 15 | elif 8 <= now < 18: 16 | msg = '这个点还早啥,昨晚干啥去了?' 17 | elif 18 <= now < 21: 18 | msg = '尼玛,老子准备洗洗睡了' 19 | else: 20 | msg = '你是夜猫子?' 21 | return msg 22 | 23 | 24 | @equal_content('早') 25 | def receive_group_msg(_): 26 | Text(get_msg()) 27 | 28 | 29 | @equal_content('早') 30 | def receive_friend_msg(_): 31 | Text(get_msg()) 32 | -------------------------------------------------------------------------------- /sample/plugins/bot_phlogo.py: -------------------------------------------------------------------------------- 1 | # 主要代码部分 https://github.com/akarrin/ph-logo/ 2 | import base64 3 | import io 4 | 5 | from PIL import Image, ImageDraw, ImageFont 6 | 7 | from iotbot import GroupMsg 8 | from iotbot.decorators import not_botself 9 | from iotbot.sugar import Picture 10 | 11 | LEFT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT = 2 12 | LEFT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH = 1 / 4 13 | RIGHT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT = 1 14 | RIGHT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH = 1 / 4 15 | RIGHT_PART_RADII = 10 16 | BG_COLOR = '#000000' 17 | BOX_COLOR = '#F7971D' 18 | LEFT_TEXT_COLOR = '#FFFFFF' 19 | RIGHT_TEXT_COLOR = '#000000' 20 | FONT_SIZE = 50 21 | 22 | FONT_PATH = 'ArialEnUnicodeBold.ttf' # TODO: 23 | 24 | 25 | def create_left_part_img(text: str, font_size: int, type_='h'): 26 | font = ImageFont.truetype(FONT_PATH, font_size) 27 | font_width, font_height = font.getsize(text) 28 | offset_y = font.font.getsize(text)[1][1] 29 | if type_ == 'h': 30 | blank_height = font_height * 2 31 | else: 32 | blank_height = font_height 33 | right_blank = int( 34 | font_width / len(text) * LEFT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH 35 | ) 36 | img_height = font_height + offset_y + blank_height * 2 37 | image_width = font_width + right_blank 38 | image_size = image_width, img_height 39 | image = Image.new('RGBA', image_size, BG_COLOR) 40 | draw = ImageDraw.Draw(image) 41 | draw.text((0, blank_height), text, fill=LEFT_TEXT_COLOR, font=font) 42 | return image 43 | 44 | 45 | def create_right_part_img(text: str, font_size: int): 46 | radii = RIGHT_PART_RADII 47 | font = ImageFont.truetype(FONT_PATH, font_size) 48 | font_width, font_height = font.getsize(text) 49 | offset_y = font.font.getsize(text)[1][1] 50 | blank_height = font_height * RIGHT_PART_VERTICAL_BLANK_MULTIPLY_FONT_HEIGHT 51 | left_blank = int( 52 | font_width / len(text) * RIGHT_PART_HORIZONTAL_BLANK_MULTIPLY_FONT_WIDTH 53 | ) 54 | image_width = font_width + 2 * left_blank 55 | image_height = font_height + offset_y + blank_height * 2 56 | image = Image.new('RGBA', (image_width, image_height), BOX_COLOR) 57 | draw = ImageDraw.Draw(image) 58 | draw.text((left_blank, blank_height), text, fill=RIGHT_TEXT_COLOR, font=font) 59 | 60 | # 圆 61 | magnify_time = 10 62 | magnified_radii = radii * magnify_time 63 | circle = Image.new( 64 | 'L', (magnified_radii * 2, magnified_radii * 2), 0 65 | ) # 创建一个黑色背景的画布 66 | draw = ImageDraw.Draw(circle) 67 | draw.ellipse((0, 0, magnified_radii * 2, magnified_radii * 2), fill=255) # 画白色圆形 68 | 69 | # 画4个角(将整圆分离为4个部分) 70 | magnified_alpha_width = image_width * magnify_time 71 | magnified_alpha_height = image_height * magnify_time 72 | alpha = Image.new('L', (magnified_alpha_width, magnified_alpha_height), 255) 73 | alpha.paste(circle.crop((0, 0, magnified_radii, magnified_radii)), (0, 0)) # 左上角 74 | alpha.paste( 75 | circle.crop((magnified_radii, 0, magnified_radii * 2, magnified_radii)), 76 | (magnified_alpha_width - magnified_radii, 0), 77 | ) # 右上角 78 | alpha.paste( 79 | circle.crop( 80 | (magnified_radii, magnified_radii, magnified_radii * 2, magnified_radii * 2) 81 | ), 82 | ( 83 | magnified_alpha_width - magnified_radii, 84 | magnified_alpha_height - magnified_radii, 85 | ), 86 | ) # 右下角 87 | alpha.paste( 88 | circle.crop((0, magnified_radii, magnified_radii, magnified_radii * 2)), 89 | (0, magnified_alpha_height - magnified_radii), 90 | ) # 左下角 91 | alpha = alpha.resize((image_width, image_height), Image.ANTIALIAS) 92 | image.putalpha(alpha) 93 | return image 94 | 95 | 96 | def combine_img_horizontal( 97 | left_text: str, right_text, font_size: int = FONT_SIZE 98 | ) -> str: 99 | left_img = create_left_part_img(left_text, font_size) 100 | right_img = create_right_part_img(right_text, font_size) 101 | blank = 30 102 | bg_img_width = left_img.width + right_img.width + blank * 2 103 | bg_img_height = left_img.height 104 | bg_img = Image.new('RGBA', (bg_img_width, bg_img_height), BG_COLOR) 105 | bg_img.paste(left_img, (blank, 0)) 106 | bg_img.paste( 107 | right_img, 108 | (blank + left_img.width, int((bg_img_height - right_img.height) / 2)), 109 | mask=right_img, 110 | ) 111 | buffer = io.BytesIO() 112 | bg_img.save(buffer, format='png') 113 | return base64.b64encode(buffer.getvalue()).decode() 114 | 115 | 116 | def combine_img_vertical(left_text: str, right_text, font_size: int = FONT_SIZE) -> str: 117 | left_img = create_left_part_img(left_text, font_size, type_='v') 118 | right_img = create_right_part_img(right_text, font_size) 119 | blank = 15 120 | bg_img_width = max(left_img.width, right_img.width) + blank * 2 121 | bg_img_height = left_img.height + right_img.height + blank * 2 122 | bg_img = Image.new('RGBA', (bg_img_width, bg_img_height), BG_COLOR) 123 | bg_img.paste(left_img, (int((bg_img_width - left_img.width) / 2), blank)) 124 | bg_img.paste( 125 | right_img, 126 | (int((bg_img_width - right_img.width) / 2), blank + left_img.height), 127 | mask=right_img, 128 | ) 129 | buffer = io.BytesIO() 130 | bg_img.save(buffer, format='png') 131 | return base64.b64encode(buffer.getvalue()).decode() 132 | 133 | 134 | @not_botself 135 | def receive_group_msg(ctx: GroupMsg): 136 | if ctx.Content.startswith('ph '): 137 | args = [i.strip() for i in ctx.Content.split(' ') if i.strip()] 138 | if len(args) >= 3: 139 | left = args[1] 140 | right = args[2] 141 | f = combine_img_horizontal 142 | if len(args) >= 4: 143 | if args[3] == '1': 144 | f = combine_img_vertical 145 | Picture(pic_base64=f(left, right)) 146 | -------------------------------------------------------------------------------- /sample/plugins/bot_pic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import re 4 | 5 | from iotbot import Action, GroupMsg 6 | 7 | 8 | def receive_group_msg(ctx: GroupMsg): 9 | if ctx.FromUserId == ctx.CurrentQQ: 10 | return 11 | 12 | content = ctx.Content # type:str 13 | 14 | # 妹子图 15 | if re.findall(r"(妹子|小姐姐|美女)", content): 16 | random.seed(os.urandom(100)) 17 | Action(ctx.CurrentQQ).send_group_pic_msg( 18 | ctx.FromGroupId, 19 | picUrl=random.choice( 20 | [ # 随便找的api 21 | 'http://api.btstu.cn/sjbz/?lx=meizi', 22 | 'http://api.btstu.cn/sjbz/?lx=m_meizi', 23 | 'http://api.btstu.cn/sjbz/?m_lx=suiji', 24 | ] 25 | ), 26 | ) 27 | return 28 | -------------------------------------------------------------------------------- /sample/plugins/bot_qrcode.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 生成二维码+{数据(文字)} 3 | ''' 4 | import base64 5 | import io 6 | 7 | from iotbot import FriendMsg, GroupMsg 8 | from iotbot.decorators import in_content, not_botself 9 | from iotbot.sugar import Picture 10 | 11 | try: 12 | import qrcode 13 | except ImportError: 14 | raise ImportError('请先安装依赖库: pip install qrcode, pillow') 15 | 16 | 17 | def gen_qrcode(text: str) -> str: 18 | img = qrcode.make(text) 19 | img_buffer = io.BytesIO() 20 | img.save(img_buffer) 21 | return base64.b64encode(img_buffer.getvalue()).decode() 22 | 23 | 24 | @not_botself 25 | @in_content('生成二维码') 26 | def receive_group_msg(ctx: GroupMsg): 27 | Picture(pic_base64=gen_qrcode(ctx.Content.replace('生成二维码', ''))) 28 | 29 | 30 | @not_botself 31 | @in_content('生成二维码') 32 | def receive_friend_msg(ctx: FriendMsg): 33 | Picture(pic_base64=gen_qrcode(ctx.Content.replace('生成二维码', ''))) 34 | -------------------------------------------------------------------------------- /sample/plugins/bot_replay.py: -------------------------------------------------------------------------------- 1 | # 增强版复读机 2 | from time import sleep 3 | 4 | from iotbot import GroupMsg 5 | from iotbot.decorators import in_content, not_botself 6 | from iotbot.refine import refine_pic_group_msg 7 | from iotbot.sugar import Picture, Text 8 | from iotbot.utils import MsgTypes 9 | 10 | 11 | @not_botself 12 | @in_content('复读机') 13 | def receive_group_msg(ctx: GroupMsg): 14 | if ctx.MsgType == MsgTypes.TextMsg: 15 | if not ctx.Content.startswith('复读机'): 16 | return 17 | text = ctx.Content[3:] # type: str 18 | while '复读机' in text: # 防止别人搞鬼发这种东西: 复读机复复读机读复读机机 19 | text = text.replace('复读机', '') 20 | if text: 21 | Text(text) 22 | elif ctx.MsgType == MsgTypes.PicMsg: 23 | pic_ctx = refine_pic_group_msg(ctx) 24 | if pic_ctx is None: 25 | return 26 | for pic in pic_ctx.GroupPic: 27 | Picture(pic_url=pic.Url) 28 | sleep(1) 29 | elif ctx.MsgType == MsgTypes.AtMsg: 30 | # TODO 31 | pass 32 | -------------------------------------------------------------------------------- /sample/plugins/bot_send_local_image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import random 4 | 5 | from iotbot import Action, GroupMsg 6 | 7 | """ 8 | 随机发送本地图片,假设图片文件夹是同目录的images文件夹 9 | """ 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | image_dir = os.path.join(here, 'images') 12 | file_list = os.listdir(image_dir) 13 | 14 | 15 | def receive_group_msg(ctx: GroupMsg): 16 | if '图片' in ctx.Content: 17 | random.seed(os.urandom(100)) 18 | choose_file = os.path.join(image_dir, random.choice(file_list)) # 随机取出一张,完整路径 19 | # 二进制转base64 20 | with open(choose_file, 'rb') as f: 21 | content = f.read() 22 | b64_str = base64.b64encode(content).decode() 23 | 24 | Action(ctx.CurrentQQ).send_group_pic_msg(ctx.FromGroupId, picBase64Buf=b64_str) 25 | -------------------------------------------------------------------------------- /sample/plugins/bot_setu.py: -------------------------------------------------------------------------------- 1 | # https://github.com/yuban10703/IOTQQ-color_pic 2 | # 仅做示例 3 | import re 4 | 5 | import requests 6 | 7 | from iotbot import GroupMsg 8 | from iotbot.decorators import in_content 9 | from iotbot.sugar import Picture, Text 10 | 11 | pattern = r'来(.*?)[点丶份张幅](.*?)的?[色瑟涩][图圖]' 12 | api = 'http://api.yuban10703.xyz:2333/setu_v3' 13 | 14 | 15 | @in_content('[色瑟涩][图圖]') 16 | def receive_group_msg(ctx: GroupMsg): 17 | if ctx.FromUserId == ctx.CurrentQQ: 18 | return 19 | 20 | tag = re.findall(pattern, ctx.Content) 21 | tag = tag[0][1] if tag else '' 22 | try: 23 | r = requests.get(api, params={'tag': tag, 'num': 1}, timeout=10) 24 | except Exception: 25 | Text('老子出毛病了,稍后再试~') 26 | else: 27 | info = r.json() 28 | if info['count'] == 0: 29 | Text('没有没有,不要问了,没有!') 30 | else: 31 | Picture( 32 | 'https://cdn.jsdelivr.net/gh/laosepi/setu/pics_original/' 33 | + info['data'][0]['filename'] 34 | ) 35 | -------------------------------------------------------------------------------- /sample/plugins/bot_setu_v2.py: -------------------------------------------------------------------------------- 1 | # https://github.com/yuban10703/IOTQQ-color_pic 2 | import os 3 | import re 4 | 5 | import requests 6 | 7 | from iotbot import Action, GroupMsg 8 | from iotbot import decorators as deco 9 | from iotbot.sugar import Text 10 | 11 | pattern = r'来(.*?)[点丶份张幅](.*?)的?[色瑟涩][图圖]' 12 | api = 'http://api.yuban10703.xyz:2333/setu_v3' 13 | 14 | action = Action(int(os.getenv('BOTQQ') or 123456), queue=True) 15 | 16 | 17 | @deco.not_botself 18 | @deco.in_content(pattern) 19 | def receive_group_msg(ctx: GroupMsg): 20 | num = 1 21 | tag = '' 22 | 23 | info = re.findall(pattern, ctx.Content) 24 | if info: 25 | num = int(info[0][0] or 1) 26 | tag = info[0][1] 27 | if num > 5: 28 | Text('服了,要那么多干嘛,我只发5张!') 29 | num = 5 30 | 31 | try: 32 | r = requests.get(api, params={'tag': tag, 'num': num}, timeout=10) 33 | except Exception: 34 | Text('老子出毛病了,稍后再试~') 35 | else: 36 | info = r.json() 37 | if info['count'] == 0: 38 | Text('没有没有,不要问了,没有!') 39 | else: 40 | for i in info['data']: 41 | action.send_group_pic_msg( 42 | ctx.FromGroupId, 43 | picUrl='https://cdn.jsdelivr.net/gh/laosepi/setu/pics_original/' 44 | + i['filename'], 45 | content=f"标题:{i['title']}\n作者: {i['author']}", 46 | ) 47 | -------------------------------------------------------------------------------- /sample/plugins/bot_stop_revoke.py: -------------------------------------------------------------------------------- 1 | # 防撤回插件 2 | import json 3 | import os 4 | from pathlib import Path 5 | 6 | # pip install pymongo 7 | import pymongo 8 | from iotbot import Action, EventMsg, GroupMsg 9 | from iotbot import decorators as deco 10 | from iotbot.refine_message import refine_group_revoke_event_msg 11 | from iotbot.sugar import Text 12 | from iotbot.utils import MsgTypes 13 | 14 | # TODO: 对不同的群做不同的开关逻辑 15 | 16 | client = pymongo.MongoClient() # TODO 自行修改配置 17 | db = client.iotbot.revoke 18 | 19 | FLAG = Path(__file__).absolute().parent / 'STOPREVOKEFLAG' # 功能开关标记文件 20 | # 在哪些群开启 21 | whiteList = [1, 222] 22 | # 哪些人可以控制开关, 发送“开关防撤回”切换插件开启状态 23 | admins = [ 24 | 333, 25 | 4444, 26 | 55555, 27 | 666666, 28 | ] 29 | 30 | 31 | @deco.not_botself 32 | @deco.only_these_groups(whiteList) 33 | def receive_group_msg(ctx: GroupMsg): 34 | if ctx.Content == '开关防撤回' and ctx.FromUserId in admins: 35 | if FLAG.exists(): # 存在说明开启状态,则关闭。 36 | os.remove(FLAG) 37 | else: 38 | FLAG.touch() 39 | Text('ok') 40 | 41 | # 储存消息 42 | if ctx.MsgType in [MsgTypes.TextMsg, MsgTypes.PicMsg, MsgTypes.AtMsg]: 43 | db.insert_one( 44 | { 45 | 'msg_type': ctx.MsgType, 46 | 'msg_random': ctx.MsgRandom, 47 | 'msg_seq': ctx.MsgSeq, 48 | 'msg_time': ctx.MsgTime, 49 | 'user_id': ctx.FromUserId, 50 | 'user_name': ctx.FromNickName, 51 | 'group_id': ctx.FromGroupId, 52 | 'content': ctx.Content, 53 | } 54 | ) 55 | 56 | 57 | def receive_events(ctx: EventMsg): 58 | if not FLAG.exists(): # 功能关闭状态 59 | return 60 | 61 | revoke_ctx = refine_group_revoke_event_msg(ctx) 62 | if revoke_ctx is None: # 只处理撤回事件 63 | return 64 | 65 | bot = revoke_ctx.CurrentQQ 66 | user_id = revoke_ctx.UserID 67 | if user_id == bot: # 不接受自己的消息 68 | return 69 | 70 | admin = revoke_ctx.AdminUserID 71 | group_id = revoke_ctx.FromUin 72 | msg_random = revoke_ctx.MsgRandom 73 | msg_seq = revoke_ctx.MsgSeq 74 | 75 | found = db.find_one( 76 | { 77 | 'msg_random': msg_random, 78 | 'msg_seq': msg_seq, 79 | 'user_id': user_id, 80 | 'group_id': group_id, 81 | } 82 | ) 83 | 84 | if found: 85 | msg_type = found['msg_type'] 86 | user_id = found['user_id'] 87 | user_name = found['user_name'] 88 | group_id = found['group_id'] 89 | content = found['content'] 90 | 91 | if admin != found['user_id']: # 不相等說明是管理員撤回 92 | return 93 | 94 | if msg_type == MsgTypes.TextMsg: 95 | Action(bot).send_group_text_msg( 96 | found['group_id'], f'[{user_id}{user_name}]撤回了: \n{content}' 97 | ) 98 | elif msg_type == MsgTypes.PicMsg: 99 | pic_data = json.loads(content) 100 | Action(bot).send_group_pic_msg( 101 | group_id, 102 | fileMd5=pic_data['GroupPic'][0]['FileMd5'], 103 | content='[{}{}]撤回了:\n[PICFLAG]{}'.format( 104 | user_id, user_name, pic_data.get('Content') or '' 105 | ), 106 | ) 107 | elif msg_type == MsgTypes.AtMsg: 108 | at_data = json.loads(found['content']) 109 | at_content = at_data['Content'] 110 | Action(bot).send_group_text_msg( 111 | group_id, 112 | content='[{who}{user_name} 刚刚撤回了艾特{at_user}]\n{content}'.format( 113 | who=user_id, 114 | user_name=user_name, 115 | at_user='&'.join((str(i) for i in at_data['UserID'])), 116 | content=at_content[at_content.rindex(' ') + 1 :], 117 | ), 118 | ) 119 | -------------------------------------------------------------------------------- /sample/plugins/bot_sysinfo.py: -------------------------------------------------------------------------------- 1 | # 代码块拷贝自https://github.com/yuban10703/IOTQQ-color_pic 2 | import datetime 3 | import time 4 | 5 | from iotbot.decorators import equal_content 6 | from iotbot.sugar import Text 7 | 8 | try: 9 | import cpuinfo 10 | import psutil 11 | except ImportError as e: 12 | raise Exception('请先安装依赖库:pip install py-cpuinfo, psutil') from e 13 | 14 | 15 | def get_cpu_info(): 16 | info = cpuinfo.get_cpu_info() # 获取CPU型号等 17 | cpu_count = psutil.cpu_count(logical=False) # 1代表单核CPU,2代表双核CPU 18 | xc_count = psutil.cpu_count() # 线程数,如双核四线程 19 | cpu_percent = round((psutil.cpu_percent()), 2) # cpu使用率 20 | try: 21 | model = info['hardware_raw'] # cpu型号 22 | except Exception: 23 | model = info['brand_raw'] # cpu型号 24 | try: # 频率 25 | freq = info['hz_actual_friendly'] 26 | except Exception: 27 | freq = 'null' 28 | cpu_info = (model, freq, info['arch'], cpu_count, xc_count, cpu_percent) 29 | return cpu_info 30 | 31 | 32 | def get_memory_info(): 33 | memory = psutil.virtual_memory() 34 | swap = psutil.swap_memory() 35 | total_nc = round((float(memory.total) / 1024 / 1024 / 1024), 3) # 总内存 36 | used_nc = round((float(memory.used) / 1024 / 1024 / 1024), 3) # 已用内存 37 | available_nc = round((float(memory.available) / 1024 / 1024 / 1024), 3) # 空闲内存 38 | percent_nc = memory.percent # 内存使用率 39 | swap_total = round((float(swap.total) / 1024 / 1024 / 1024), 3) # 总swap 40 | swap_used = round((float(swap.used) / 1024 / 1024 / 1024), 3) # 已用swap 41 | swap_free = round((float(swap.free) / 1024 / 1024 / 1024), 3) # 空闲swap 42 | swap_percent = swap.percent # swap使用率 43 | men_info = ( 44 | total_nc, 45 | used_nc, 46 | available_nc, 47 | percent_nc, 48 | swap_total, 49 | swap_used, 50 | swap_free, 51 | swap_percent, 52 | ) 53 | return men_info 54 | 55 | 56 | def uptime(): 57 | now = time.time() 58 | boot = psutil.boot_time() 59 | boottime = datetime.datetime.fromtimestamp(boot).strftime("%Y-%m-%d %H:%M:%S") 60 | nowtime = datetime.datetime.fromtimestamp(now).strftime("%Y-%m-%d %H:%M:%S") 61 | up_time = str( 62 | datetime.datetime.utcfromtimestamp(now).replace(microsecond=0) 63 | - datetime.datetime.utcfromtimestamp(boot).replace(microsecond=0) 64 | ) 65 | alltime = (boottime, nowtime, up_time) 66 | return alltime 67 | 68 | 69 | def sysinfo(): 70 | cpu_info = get_cpu_info() 71 | mem_info = get_memory_info() 72 | up_time = uptime() 73 | msg = ( 74 | 'CPU型号:{0}\r\n频率:{1}\r\n架构:{2}\r\n核心数:{3}\r\n线程数:{4}\r\n负载:{5}%\r\n{6}\r\n' 75 | '总内存:{7}G\r\n已用内存:{8}G\r\n空闲内存:{9}G\r\n内存使用率:{10}%\r\n{6}\r\n' 76 | 'swap:{11}G\r\n已用swap:{12}G\r\n空闲swap:{13}G\r\nswap使用率:{14}%\r\n{6}\r\n' 77 | '开机时间:{15}\r\n当前时间:{16}\r\n已运行时间:{17}' 78 | ) 79 | full_meg = msg.format( 80 | cpu_info[0], 81 | cpu_info[1], 82 | cpu_info[2], 83 | cpu_info[3], 84 | cpu_info[4], 85 | cpu_info[5], 86 | '*' * 20, 87 | mem_info[0], 88 | mem_info[1], 89 | mem_info[2], 90 | mem_info[3], 91 | mem_info[4], 92 | mem_info[5], 93 | mem_info[6], 94 | mem_info[7], 95 | up_time[0], 96 | up_time[1], 97 | up_time[2], 98 | ) 99 | return full_meg 100 | 101 | 102 | @equal_content('sysinfo') 103 | def receive_group_msg(_): 104 | Text(sysinfo()) 105 | 106 | 107 | @equal_content('sysinfo') 108 | def receive_friend_msg(_): 109 | Text(sysinfo()) 110 | -------------------------------------------------------------------------------- /sample/plugins/bot_test_middleware.py: -------------------------------------------------------------------------------- 1 | from iotbot.decorators import equal_content 2 | from iotbot.sugar import Text 3 | 4 | # 下面三个函数名不能改,否则不会调用 5 | # 但是都是可选项,建议把不需要用到的函数删除,节约资源 6 | 7 | 8 | @equal_content('middleware') 9 | def receive_group_msg(ctx): 10 | print('------------------') 11 | print(dir(ctx)) 12 | if hasattr(ctx, 'master'): 13 | print(ctx.master) 14 | print(type(ctx)) 15 | Text(ctx.master) 16 | -------------------------------------------------------------------------------- /sample/plugins/bot_test_queue.py: -------------------------------------------------------------------------------- 1 | from iotbot import Action, GroupMsg 2 | from iotbot.decorators import equal_content, not_botself 3 | 4 | action = Action(123456, queue=True, queue_delay=2) # 唯一写法 5 | 6 | 7 | @not_botself 8 | @equal_content('queue') 9 | def receive_group_msg(ctx: GroupMsg): 10 | action.send_group_text_msg(ctx.FromGroupId, '这条消息每次发送间隔不会低于2秒。。。', callback=print) 11 | -------------------------------------------------------------------------------- /sample/plugins/bot_test_refine_funcs.py: -------------------------------------------------------------------------------- 1 | from iotbot import Action, EventMsg, FriendMsg, GroupMsg 2 | from iotbot import refine_message as refine 3 | from iotbot.decorators import not_botself 4 | from iotbot.sugar import Text 5 | 6 | # 2020-09-10注: 部分内容已不适用最新版本框架,api可能有所改变 7 | 8 | # refine_?函数只是方便解析一些消息变化的部分 9 | # 一般用在`事件`消息上,因为每个事件都有很多不同的数据 10 | # 其他的用处主要在需要解析图片,语音之类的信息吧 11 | 12 | 13 | @not_botself # 忽略机器人自己的消息 14 | def receive_group_msg(ctx: GroupMsg): 15 | # 群红包消息 16 | redbag_ctx = refine.refine_RedBag_group_msg(ctx) 17 | if redbag_ctx is not None: 18 | Text(redbag_ctx.RedBag_Authkey) # 如果是红包,就在刚才的群内发送刚才红包的`AuthKey`字段 19 | del redbag_ctx 20 | return 21 | 22 | 23 | @not_botself 24 | def receive_friend_msg(ctx: FriendMsg): 25 | # 好友图片消息 26 | pic_ctx = refine.refine_pic_friend_msg(ctx) 27 | if pic_ctx is not None: 28 | print('----friend------------') 29 | Text(pic_ctx.FriendPic[0].Url) # 给他发送刚才图片的链接 30 | del pic_ctx 31 | return 32 | # 好友语音消息 33 | voice_ctx = refine.refine_voice_friend_msg(ctx) # 给他发送刚才语音的链接 34 | if voice_ctx is not None: 35 | Text(voice_ctx.VoiceUrl) 36 | del voice_ctx 37 | return 38 | 39 | 40 | def receive_events(ctx: EventMsg): 41 | print(ctx.EventName) # 打印事件名 42 | 43 | # 群消息撤回事件 44 | revoke_ctx = refine.refine_group_revoke_event_msg(ctx) 45 | if revoke_ctx is not None: 46 | print('------------------') 47 | print(revoke_ctx.AdminUserID) # 谁撤回的 48 | print(revoke_ctx.UserID) # 撤回谁的 49 | print('------------------') 50 | del revoke_ctx 51 | return 52 | 53 | # 群禁言事件 54 | shut_ctx = refine.refine_group_shut_event_msg(ctx) 55 | if shut_ctx is not None: 56 | if shut_ctx.UserID != 0: # 没有特定的用, 为0说明是全体禁言 57 | if shut_ctx.ShutTime == 0: # 为0是解除 58 | msg = '{}被解除禁言了'.format(shut_ctx.UserID) 59 | else: 60 | msg = '{}被禁言了{}分钟, 哈哈哈哈哈'.format( 61 | shut_ctx.UserID, shut_ctx.ShutTime / 60 62 | ) 63 | print(msg) 64 | if shut_ctx.UserID != shut_ctx.CurrentQQ: # 如果不是自己被禁言,就发送给该群消息 65 | Action(shut_ctx.CurrentQQ).send_group_text_msg(shut_ctx.FromUin, msg) 66 | else: 67 | if shut_ctx.ShutTime == 0: 68 | print(f'{shut_ctx.FromUin}解除全体禁言') 69 | else: 70 | print(f'{shut_ctx.FromUin}全体禁言') 71 | del shut_ctx 72 | return 73 | 74 | # 某人加群事件 75 | join_ctx = refine.refine_group_join_event_msg(ctx) 76 | if join_ctx is not None: 77 | Action(join_ctx.CurrentQQ).send_group_text_msg( 78 | join_ctx.FromUin, '欢迎 <%s>' % join_ctx.UserName 79 | ) 80 | del join_ctx 81 | return 82 | 83 | # 某人退群事件 84 | exit_ctx = refine.refine_group_exit_event_msg(ctx) 85 | if exit_ctx is not None: 86 | Action(exit_ctx.CurrentQQ).send_group_text_msg( 87 | exit_ctx.FromUin, f'群友<{exit_ctx.UserID}>离开了我们' 88 | ) 89 | del exit_ctx 90 | return 91 | -------------------------------------------------------------------------------- /sample/plugins/bot_to_card.py: -------------------------------------------------------------------------------- 1 | # 可用于调试卡片消息 2 | import json 3 | 4 | from iotbot import Action 5 | from iotbot import GroupMsg 6 | from iotbot.decorators import in_content 7 | 8 | 9 | @in_content('转卡片') 10 | def receive_group_msg(ctx: GroupMsg): 11 | content = ctx.Content.replace('转卡片', '').strip() 12 | action = Action(ctx.CurrentQQ) 13 | try: 14 | json_text = json.dumps(json.loads(content)) 15 | action.send_group_json_msg(ctx.FromGroupId, content=json_text) 16 | except Exception: 17 | action.send_group_xml_msg(ctx.FromGroupId, content=content) 18 | -------------------------------------------------------------------------------- /sample/plugins/bot_verse.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from iotbot import Action, GroupMsg 4 | from iotbot.decorators import equal_content, not_botself 5 | from iotbot.sugar import Text 6 | 7 | # 推荐用lua写 8 | 9 | 10 | @not_botself 11 | @equal_content('诗句') 12 | def receive_group_msg(ctx: GroupMsg): 13 | try: 14 | rep = requests.get('https://v1.jinrishici.com/all.json', timeout=10) 15 | rep.raise_for_status() 16 | content: str = rep.json()['content'] 17 | origin: str = rep.json()['origin'] 18 | author: str = rep.json()['author'] 19 | temp = [origin, f'【{author}】', content] 20 | max_len = max([len(x) for x in temp]) 21 | Text('\n'.join([x.center(max_len) for x in temp])) 22 | except Exception as e: 23 | print(e) 24 | -------------------------------------------------------------------------------- /sample/plugins/bot_whatis.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from iotbot import GroupMsg 3 | from iotbot.decorators import not_botself, only_this_msg_type 4 | from iotbot.sugar import Text 5 | from iotbot.utils import MsgTypes 6 | 7 | # 查网络缩写词的意思 8 | # ?nmsl => 查nmsl 9 | # ?awsl => 查awsl 10 | 11 | 12 | def whatis(text): 13 | if not (1 < len(text) < 10): 14 | return '' 15 | try: 16 | resp = requests.post( 17 | 'https://lab.magiconch.com/api/nbnhhsh/guess', 18 | data={'text': text}, 19 | timeout=10, 20 | ) 21 | resp.raise_for_status() 22 | data = resp.json() 23 | except Exception as e: 24 | print(e) 25 | return '' 26 | else: 27 | if not data: 28 | return '' 29 | name, trans = data[0]['name'], data[0]['trans'] 30 | trans_str = '、'.join(trans) 31 | return f'【{name}】{trans_str}' 32 | 33 | 34 | @not_botself 35 | @only_this_msg_type(MsgTypes.TextMsg) 36 | def receive_group_msg(ctx: GroupMsg): 37 | if ctx.Content.startswith('?'): 38 | text = ctx.Content[1:] 39 | ret = whatis(text) 40 | if ret: 41 | Text(ret) 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import setup 4 | 5 | 6 | def read_files(files): 7 | data = [] 8 | for file in files: 9 | with io.open(file, encoding='utf-8') as f: 10 | data.append(f.read()) 11 | return "\n".join(data) 12 | 13 | 14 | long_description = read_files(['README.md', 'CHANGELOG.md']) 15 | 16 | meta = {} 17 | 18 | with io.open('./iotbot/version.py', encoding='utf-8') as f: 19 | exec(f.read(), meta) # pylint: disable=W0122 20 | 21 | setup( 22 | name="python-iotbot", 23 | description="IOTBOT SDK with python!", 24 | long_description=long_description, 25 | long_description_content_type='text/markdown', 26 | version=meta['__version__'], 27 | author="wongxy", 28 | author_email="xiyao.wong@foxmail.com", 29 | url="https://github.com/XiyaoWong/python-iotbot", 30 | license='MIT', 31 | keywords=['iotbot', 'iotbot sdk', 'iotqq', 'OPQ', 'OPQBot'], 32 | packages=['iotbot'], 33 | install_requires=[ 34 | 'python-socketio >= 4.5.1', 35 | 'websocket-client >= 0.57.0', 36 | 'requests >= 2.23.0', 37 | 'prettytable >= 0.7.2', 38 | 'loguru >= 0.5.1', 39 | ], 40 | entry_points=''' 41 | [console_scripts] 42 | iotbot=iotbot.cli:cli 43 | ''', 44 | classifiers=[ 45 | 'Programming Language :: Python :: 3.6', 46 | 'Programming Language :: Python :: 3.7', 47 | 'Programming Language :: Python :: 3.8', 48 | 'Programming Language :: Python :: Implementation :: PyPy', 49 | 'Intended Audience :: Developers', 50 | 'Intended Audience :: System Administrators', 51 | 'License :: OSI Approved :: MIT License', 52 | 'Operating System :: OS Independent', 53 | ], 54 | python_requires='>=3.6', 55 | ) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiyaowong/python--iotbot/9963c880d8bbd61a52ccba3b22a08d7294741baa/tests/__init__.py --------------------------------------------------------------------------------