├── .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 | [](https://pypi.org/project/python-iotbot/)
5 | [](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
--------------------------------------------------------------------------------