mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 09:48:22 +08:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a81cd47c | ||
|
|
81edd13470 | ||
|
|
7a94745b8a | ||
|
|
06b02f5df8 | ||
|
|
83136e3142 | ||
|
|
950a9f2ee0 | ||
|
|
a26c10fee8 | ||
|
|
4bcd76fe93 | ||
|
|
90ccb091ca | ||
|
|
62df27eaa1 | ||
|
|
349115b948 | ||
|
|
4fd7e4be67 | ||
|
|
947e892916 | ||
|
|
d62b7d1a99 | ||
|
|
432b39a9c4 | ||
|
|
26540bfb63 | ||
|
|
fd64f88a7e | ||
|
|
72994bc9ef | ||
|
|
7e1138af50 | ||
|
|
72dbddb7f7 | ||
|
|
10dba50843 | ||
|
|
d6af1b5827 | ||
|
|
6c362a9b4b | ||
|
|
9a0584d649 | ||
|
|
5ab5211c95 | ||
|
|
f644682be7 | ||
|
|
ffad8e4d26 | ||
|
|
8f07e6304a | ||
|
|
834c03359f | ||
|
|
3e2c68ba49 | ||
|
|
2a21941b68 | ||
|
|
e78886fb35 | ||
|
|
80bf6a0c7a | ||
|
|
48e066b677 | ||
|
|
dcb9d7fc2a | ||
|
|
279f0f0234 | ||
|
|
b3c8a7d8de | ||
|
|
1baf1a79e5 | ||
|
|
35160e717e | ||
|
|
a12f2d8fbd | ||
|
|
6b7c17374b | ||
|
|
9b3585e795 | ||
|
|
74f383a7d4 | ||
|
|
820fbeed18 | ||
|
|
f76e8d9a77 | ||
|
|
5b85e60d5d | ||
|
|
24de670c2c | ||
|
|
42aca71763 | ||
|
|
9b4ef85174 | ||
|
|
9b389ffc33 | ||
|
|
b3cb81aa52 | ||
|
|
61865bc408 | ||
|
|
c2ea6214a9 | ||
|
|
b6684fe7a3 | ||
|
|
b50ebc05a0 | ||
|
|
dbb0648c39 | ||
|
|
5fc0987cc3 | ||
|
|
7c4037147c | ||
|
|
f76cb1231e | ||
|
|
6701d8c5e6 | ||
|
|
ff3d143185 | ||
|
|
ea95ab9062 | ||
|
|
38c901a1c5 | ||
|
|
0c9753b7cd | ||
|
|
721b36c7f7 | ||
|
|
f8e0716474 | ||
|
|
3d428ee844 | ||
|
|
a3be1fcd8f | ||
|
|
167f10c9f9 | ||
|
|
b3cabd9621 | ||
|
|
709468d281 | ||
|
|
c3a2bd70ff | ||
|
|
92caeed7ab | ||
|
|
3c91575ebe | ||
|
|
46a6223a43 | ||
|
|
e226c93eeb | ||
|
|
5aedce647f | ||
|
|
4881f7b01c | ||
|
|
bebe8c1b1d | ||
|
|
b03e8f7c71 | ||
|
|
fa0d5592d6 | ||
|
|
bcf3ce9adf | ||
|
|
14dd4f19aa | ||
|
|
cd86801eac | ||
|
|
da18e3312a | ||
|
|
fea56a0ddf | ||
|
|
d3cc52b794 | ||
|
|
f805b29a8c | ||
|
|
3f78e43bbf | ||
|
|
ab6670b3af | ||
|
|
797a160856 | ||
|
|
2d0935741c | ||
|
|
8a645cd47b | ||
|
|
f189694c78 | ||
|
|
63701c182a | ||
|
|
efd12dac35 | ||
|
|
e071b6c1b4 | ||
|
|
b590e889a7 | ||
|
|
17ea48f25d | ||
|
|
04fec4a585 | ||
|
|
ae06cf844d | ||
|
|
f3daa8e3bf | ||
|
|
3f0b80d48e | ||
|
|
4a5e3e433b | ||
|
|
5ffeac6683 | ||
|
|
71f2db30da | ||
|
|
61c6d01af2 | ||
|
|
30aedf04d7 | ||
|
|
f791c7eafd | ||
|
|
2f78c072d7 | ||
|
|
50c91b428d | ||
|
|
52abe0893a | ||
|
|
42f3f4403c | ||
|
|
0a1cc91c0c | ||
|
|
518cac7ab9 | ||
|
|
8c4a62b9c6 | ||
|
|
c1d1e923cd | ||
|
|
18e9aca3b1 | ||
|
|
9fe59f2949 | ||
|
|
ea5f7173bd | ||
|
|
d5611b185b | ||
|
|
a660aa2133 | ||
|
|
5e48dd50ac | ||
|
|
2d3ffa1738 | ||
|
|
663967680a | ||
|
|
b190db73dc | ||
|
|
475d2f7911 | ||
|
|
a1323c9de8 | ||
|
|
260c374a56 | ||
|
|
3d264207a8 | ||
|
|
b260029cd9 | ||
|
|
240b4b540b | ||
|
|
695302d407 | ||
|
|
be13400bc0 | ||
|
|
efc27192fa | ||
|
|
e1ede58094 | ||
|
|
ff21a50f7f | ||
|
|
4f5f65086f | ||
|
|
3f889ab75f | ||
|
|
8b28866d53 | ||
|
|
77046000e8 | ||
|
|
852adb72a2 | ||
|
|
48a6807851 | ||
|
|
5a46e09358 | ||
|
|
cfd423c991 | ||
|
|
021ee2312e | ||
|
|
0f830f2317 | ||
|
|
3ef7855384 | ||
|
|
d760b045d5 | ||
|
|
53cc1df369 | ||
|
|
9b2da6c431 | ||
|
|
b3e1f56fb9 | ||
|
|
1aa2382843 | ||
|
|
61d66dd8b3 | ||
|
|
3c04325aae | ||
|
|
b404e2c51f | ||
|
|
5b0f0e8b6c | ||
|
|
f9b0ad7697 | ||
|
|
224ee6bd89 | ||
|
|
1dc39af423 | ||
|
|
2c8da59b47 | ||
|
|
2cb30b5f59 | ||
|
|
2568322879 | ||
|
|
8915149d36 | ||
|
|
300b7b9687 | ||
|
|
c782b38ba1 | ||
|
|
e6b65437e4 | ||
|
|
e6d148e729 | ||
|
|
9e3a5395c7 | ||
|
|
54290f7e5d | ||
|
|
dce9c4dccb | ||
|
|
ad6ae0b32a | ||
|
|
1bb5c6dc0d | ||
|
|
b204d305a1 | ||
|
|
1dc3f85a66 | ||
|
|
cb7bf446e3 | ||
|
|
8d2e81815c | ||
|
|
cee57e4ffc | ||
|
|
475ada22e7 | ||
|
|
8847b5b674 | ||
|
|
73de429af1 | ||
|
|
d9b902f6ee | ||
|
|
0fcf0824dc | ||
|
|
9e07703eb1 | ||
|
|
9ae7b7773e | ||
|
|
d6037422ac | ||
|
|
38c8ceba12 | ||
|
|
8fa4041fc2 | ||
|
|
8107165792 | ||
|
|
fc4912c640 | ||
|
|
36ed9d02b7 | ||
|
|
d6c92e1fd5 | ||
|
|
4ccad86010 | ||
|
|
38ad01a387 | ||
|
|
e014b0406c | ||
|
|
a4e8e64b5d | ||
|
|
48e258dd67 | ||
|
|
574f05cc6f | ||
|
|
c2e4d88842 | ||
|
|
99b4700b49 | ||
|
|
32cff41df5 | ||
|
|
8eace7e30e | ||
|
|
d02508df41 | ||
|
|
3db452ef71 | ||
|
|
d7a8854fa1 | ||
|
|
882e6c3576 | ||
|
|
51f0b898f0 | ||
|
|
e6112568ed | ||
|
|
720ad07f83 | ||
|
|
cc19017c01 | ||
|
|
55fe38d5fb | ||
|
|
494c5a6222 | ||
|
|
1711a5c064 | ||
|
|
d38fc61043 | ||
|
|
e5ab350bbf | ||
|
|
ad7ab088fe | ||
|
|
f2ae3e2fd8 | ||
|
|
733f9d1f10 | ||
|
|
2886f48788 | ||
|
|
04078fd4fa | ||
|
|
2c2217daad | ||
|
|
5de600c689 | ||
|
|
1d4966b69c | ||
|
|
7ad16731fd | ||
|
|
5df341fef2 | ||
|
|
39a5487f39 | ||
|
|
6a98bc2d5a | ||
|
|
b154dd7e86 | ||
|
|
3d4d1c734a | ||
|
|
f10911bc3b | ||
|
|
44e5979a03 | ||
|
|
598bc6569d | ||
|
|
d667ccb396 | ||
|
|
efbc9de9d1 | ||
|
|
ebed4e7832 | ||
|
|
fb598fba82 | ||
|
|
2c4d79e952 | ||
|
|
a2db765ade | ||
|
|
df3f19b534 | ||
|
|
f67dae5b0b | ||
|
|
cd5f58ff2c | ||
|
|
7be9e7d0a8 | ||
|
|
47c675f999 | ||
|
|
cfa738087f | ||
|
|
73b4d63545 | ||
|
|
48900dfbc4 | ||
|
|
a3153815c8 | ||
|
|
8729a31119 | ||
|
|
b81d947dbb | ||
|
|
999b2ea51f | ||
|
|
0b802a61ec | ||
|
|
02ca1f8772 | ||
|
|
820b255e24 | ||
|
|
bca0939c9d | ||
|
|
01d0af841d | ||
|
|
18e9d6a9b9 | ||
|
|
e27e5958a5 | ||
|
|
2c5b1d5a8d |
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,9 +1,11 @@
|
||||
### 前置确认
|
||||
|
||||
1. 运行于国内网络环境,未开代理
|
||||
2. python 已安装:版本在 3.7 ~ 3.10 之间,依赖已安装
|
||||
3. 在已有 issue 中未搜索到类似问题
|
||||
4. [FAQS](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) 中无类似问题
|
||||
1. 网络能够访问openai接口
|
||||
2. python 已安装:版本在 3.7 ~ 3.10 之间
|
||||
3. `git pull` 拉取最新代码
|
||||
4. 执行`pip3 install -r requirements.txt`,检查依赖是否满足
|
||||
5. 在已有 issue 中未搜索到类似问题
|
||||
6. [FAQS](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) 中无类似问题
|
||||
|
||||
|
||||
### 问题描述
|
||||
|
||||
60
.github/workflows/deploy-image.yml
vendored
Normal file
60
.github/workflows/deploy-image.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: Create and publish a Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['master']
|
||||
release:
|
||||
types: [published]
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: ./docker/Dockerfile.latest
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- uses: actions/delete-package-versions@v4
|
||||
with:
|
||||
package-name: 'chatgpt-on-wechat'
|
||||
package-type: 'container'
|
||||
min-versions-to-keep: 10
|
||||
delete-only-untagged-versions: 'true'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.wechaty/
|
||||
__pycache__/
|
||||
venv*
|
||||
*.pyc
|
||||
config.json
|
||||
QR.png
|
||||
nohup.out
|
||||
tmp
|
||||
plugins.json
|
||||
itchat.pkl
|
||||
|
||||
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM ghcr.io/zhayujie/chatgpt-on-wechat:latest
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
80
README.md
80
README.md
@@ -2,23 +2,33 @@
|
||||
|
||||
> ChatGPT近期以强大的对话和信息整合能力风靡全网,可以写代码、改论文、讲故事,几乎无所不能,这让人不禁有个大胆的想法,能否用他的对话模型把我们的微信打造成一个智能机器人,可以在与好友对话中给出意想不到的回应,而且再也不用担心女朋友影响我们 ~~打游戏~~ 工作了。
|
||||
|
||||
|
||||
基于ChatGPT的微信聊天机器人,通过 [OpenAI](https://github.com/openai/openai-quickstart-python) 接口生成对话内容,使用 [itchat](https://github.com/littlecodersh/ItChat) 实现微信消息的接收和自动回复。已实现的特性如下:
|
||||
|
||||
基于ChatGPT的微信聊天机器人,通过 [ChatGPT](https://github.com/openai/openai-python) 接口生成对话内容,使用 [itchat](https://github.com/littlecodersh/ItChat) 实现微信消息的接收和自动回复。已实现的特性如下:
|
||||
|
||||
- [x] **文本对话:** 接收私聊及群组中的微信消息,使用ChatGPT生成回复内容,完成自动回复
|
||||
- [x] **规则定制化:** 支持私聊中按指定规则触发自动回复,支持对群组设置自动回复白名单
|
||||
- [x] **多账号:** 支持多微信账号同时运行
|
||||
- [x] **图片生成:** 支持根据描述生成图片,并自动发送至个人聊天或群聊
|
||||
- [x] **上下文记忆**:支持多轮对话记忆,且为每个好友维护独立的上下会话
|
||||
- [x] **语音识别:** 支持接收和处理语音消息,通过文字或语音回复
|
||||
- [x] **插件化:** 支持个性化功能插件,提供角色扮演、文字冒险游戏等预设插件
|
||||
|
||||
> 快速部署:
|
||||
>
|
||||
>[](https://railway.app/template/qApznZ?referralCode=RC3znh)
|
||||
|
||||
# 更新日志
|
||||
|
||||
>**2023.03.25:** 支持插件化开发,目前已实现 多角色切换、文字冒险游戏、管理员指令、Stable Diffusion等插件,使用参考 [#578](https://github.com/zhayujie/chatgpt-on-wechat/issues/578)。(contributed by [@lanvent](https://github.com/lanvent) in [#565](https://github.com/zhayujie/chatgpt-on-wechat/pull/565))
|
||||
|
||||
>**2023.03.09:** 基于 `whisper API` 实现对微信语音消息的解析和回复,添加配置项 `"speech_recognition":true` 即可启用,使用参考 [#415](https://github.com/zhayujie/chatgpt-on-wechat/issues/415)。(contributed by [wanggang1987](https://github.com/wanggang1987) in [#385](https://github.com/zhayujie/chatgpt-on-wechat/pull/385))
|
||||
|
||||
>**2023.03.02:** 接入[ChatGPT API](https://platform.openai.com/docs/guides/chat) (gpt-3.5-turbo),默认使用该模型进行对话,需升级openai依赖 (`pip3 install --upgrade openai`)。网络问题参考 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351)
|
||||
|
||||
>**2023.02.09:** 扫码登录存在封号风险,请谨慎使用,参考[#58](https://github.com/AutumnWhj/ChatGPT-wechat-bot/issues/158)
|
||||
|
||||
>**2023.02.05:** 在openai官方接口方案中 (GPT-3模型) 实现上下文对话
|
||||
|
||||
>**2022.12.19:** 引入 [itchat-uos](https://github.com/why2lyj/ItChat-UOS) 替换 itchat,解决由于不能登录网页微信而无法使用的问题,且解决Python3.9的兼容问题
|
||||
|
||||
>**2022.12.18:** 支持根据描述生成图片并发送,openai版本需大于0.25.0
|
||||
|
||||
>**2022.12.17:** 原来的方案是从 [ChatGPT页面](https://chat.openai.com/chat) 获取session_token,使用 [revChatGPT](https://github.com/acheong08/ChatGPT) 直接访问web接口,但随着ChatGPT接入Cloudflare人机验证,这一方案难以在服务器顺利运行。 所以目前使用的方案是调用 OpenAI 官方提供的 [API](https://beta.openai.com/docs/api-reference/introduction),回复质量上基本接近于ChatGPT的内容,劣势是暂不支持有上下文记忆的对话,优势是稳定性和响应速度较好。
|
||||
@@ -44,39 +54,48 @@
|
||||
|
||||
### 1. OpenAI账号注册
|
||||
|
||||
前往 [OpenAI注册页面](https://beta.openai.com/signup) 创建账号,参考这篇 [教程](https://www.cnblogs.com/damugua/p/16969508.html) 可以通过虚拟手机号来接收验证码。创建完账号则前往 [API管理页面](https://beta.openai.com/account/api-keys) 创建一个 API Key 并保存下来,后面需要在项目中配置这个key。
|
||||
前往 [OpenAI注册页面](https://beta.openai.com/signup) 创建账号,参考这篇 [教程](https://www.pythonthree.com/register-openai-chatgpt/) 可以通过虚拟手机号来接收验证码。创建完账号则前往 [API管理页面](https://beta.openai.com/account/api-keys) 创建一个 API Key 并保存下来,后面需要在项目中配置这个key。
|
||||
|
||||
> 项目中使用的对话模型是 davinci,计费方式是约每 750 字 (包含请求和回复) 消耗 $0.02,图片生成是每张消耗 $0.016,账号创建有免费的 $18 额度,使用完可以更换邮箱重新注册。
|
||||
> 项目中使用的对话模型是 davinci,计费方式是约每 750 字 (包含请求和回复) 消耗 $0.02,图片生成是每张消耗 $0.016,账号创建有免费的 $18 额度 (更新3.25: 最新注册的已经无免费额度了),使用完可以更换邮箱重新注册。
|
||||
|
||||
#### 1.1 ChapGPT service On Azure
|
||||
一种替换以上的方法是使用Azure推出的[ChatGPT service](https://azure.microsoft.com/en-in/products/cognitive-services/openai-service/)。它host在公有云Azure上,因此不需要VPN就可以直接访问。不过目前仍然处于preview阶段。新用户可以通过Try Azure for free来薅一段时间的羊毛
|
||||
|
||||
|
||||
### 2.运行环境
|
||||
|
||||
支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 `Python`。
|
||||
支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 `Python`。
|
||||
> 建议Python版本在 3.7.1~3.9.X 之间,3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。
|
||||
|
||||
|
||||
1.克隆项目代码:
|
||||
**(1) 克隆项目代码:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
```
|
||||
|
||||
2.安装所需核心依赖:
|
||||
**(2) 安装核心依赖 (必选):**
|
||||
|
||||
```bash
|
||||
pip3 install itchat-uos==1.5.0.dev0
|
||||
pip3 install --upgrade openai
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
注:`itchat-uos`使用指定版本1.5.0.dev0,`openai`使用最新版本,需高于0.25.0。
|
||||
|
||||
其中`tiktoken`要求`python`版本在3.8以上,它用于精确计算会话使用的tokens数量,可以不装但建议安装。
|
||||
|
||||
**(3) 拓展依赖 (可选):**
|
||||
|
||||
使用`google`或`baidu`语音识别需安装`ffmpeg`,
|
||||
|
||||
默认的`openai`语音识别不需要安装`ffmpeg`。
|
||||
|
||||
参考[#415](https://github.com/zhayujie/chatgpt-on-wechat/issues/415)
|
||||
|
||||
## 配置
|
||||
|
||||
配置文件的模板在根目录的`config-template.json`中,需复制该模板创建最终生效的 `config.json` 文件:
|
||||
|
||||
```bash
|
||||
cp config-template.json config.json
|
||||
cp config-template.json config.json
|
||||
```
|
||||
|
||||
然后在`config.json`中填入配置,以下是对默认配置的说明,可根据需要进行自定义修改:
|
||||
@@ -85,13 +104,19 @@ cp config-template.json config.json
|
||||
# config.json文件内容示例
|
||||
{
|
||||
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
|
||||
"model": "gpt-3.5-turbo", # 模型名称。当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
|
||||
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
|
||||
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
|
||||
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
|
||||
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
|
||||
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
|
||||
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
|
||||
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
|
||||
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
|
||||
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。" # 人格描述
|
||||
"speech_recognition": false, # 是否开启语音识别
|
||||
"group_speech_recognition": false, # 是否开启群组语音识别
|
||||
"use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/
|
||||
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述,
|
||||
}
|
||||
```
|
||||
**配置说明:**
|
||||
@@ -106,14 +131,28 @@ cp config-template.json config.json
|
||||
+ 群组聊天中,群名称需配置在 `group_name_white_list ` 中才能开启群聊自动回复。如果想对所有群聊生效,可以直接填写 `"group_name_white_list": ["ALL_GROUP"]`
|
||||
+ 默认只要被人 @ 就会触发机器人自动回复;另外群聊天中只要检测到以 "@bot" 开头的内容,同样会自动回复(方便自己触发),这对应配置项 `group_chat_prefix`
|
||||
+ 可选配置: `group_name_keyword_white_list`配置项支持模糊匹配群名称,`group_chat_keyword`配置项则支持模糊匹配群消息内容,用法与上述两个配置项相同。(Contributed by [evolay](https://github.com/evolay))
|
||||
+ `group_chat_in_one_session`:使群聊共享一个会话上下文,配置 `["ALL_GROUP"]` 则作用于所有群聊
|
||||
|
||||
**3.其他配置**
|
||||
**3.语音识别**
|
||||
|
||||
+ 添加 `"speech_recognition": true` 将开启语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
|
||||
+ 添加 `"group_speech_recognition": true` 将开启群组语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配group_chat_prefix和group_chat_keyword, 支持语音触发画图);
|
||||
+ 添加 `"voice_reply_voice": true` 将开启语音回复语音(同时作用于私聊和群聊),但是需要配置对应语音合成平台的key,由于itchat协议的限制,只能发送语音mp3文件,若使用wechaty则回复的是微信语音。
|
||||
|
||||
**4.其他配置**
|
||||
|
||||
+ `model`: 模型名称,目前支持 `gpt-3.5-turbo`, `text-davinci-003`, `gpt-4`, `gpt-4-32k` (其中gpt-4 api暂未开放)
|
||||
+ `temperature`,`frequency_penalty`,`presence_penalty`: Chat API接口参数,详情参考[OpenAI官方文档。](https://platform.openai.com/docs/api-reference/chat)
|
||||
+ `proxy`:由于目前 `openai` 接口国内无法访问,需配置代理客户端的地址,详情参考 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351)
|
||||
+ 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 `image_create_prefix `
|
||||
+ 关于OpenAI对话及图片接口的参数配置(内容自由度、回复字数限制、图片大小等),可以参考 [对话接口](https://beta.openai.com/docs/api-reference/completions) 和 [图像接口](https://beta.openai.com/docs/api-reference/completions) 文档直接在 [代码](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/bot/openai/open_ai_bot.py) `bot/openai/open_ai_bot.py` 中进行调整。
|
||||
+ `conversation_max_tokens`:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
|
||||
+ `rate_limit_chatgpt`,`rate_limit_dalle`:每分钟最高问答速率、画图速率,超速后排队按序处理。
|
||||
+ `clear_memory_commands`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
|
||||
+ `hot_reload`: 程序退出后,暂存微信扫码状态,默认关闭。
|
||||
+ `character_desc` 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 [issue](https://github.com/zhayujie/chatgpt-on-wechat/issues/43))
|
||||
|
||||
**所有可选的配置项均在该[文件](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py)中列出。**
|
||||
|
||||
## 运行
|
||||
|
||||
@@ -135,7 +174,7 @@ python3 app.py
|
||||
touch nohup.out # 首次运行需要新建日志文件
|
||||
nohup python3 app.py & tail -f nohup.out # 在后台运行程序并通过日志输出二维码
|
||||
```
|
||||
扫码登录后程序即可运行于服务器后台,此时可通过 `ctrl+c` 关闭日志,不会影响后台程序的运行。使用 `ps -ef | grep app.py | grep -v grep` 命令可查看运行于后台的进程,如果想要重新启动程序可以先 `kill` 掉对应的进程。日志关闭后如果想要再次打开只需输入 `tail -f nohup.out`。
|
||||
扫码登录后程序即可运行于服务器后台,此时可通过 `ctrl+c` 关闭日志,不会影响后台程序的运行。使用 `ps -ef | grep app.py | grep -v grep` 命令可查看运行于后台的进程,如果想要重新启动程序可以先 `kill` 掉对应的进程。日志关闭后如果想要再次打开只需输入 `tail -f nohup.out`。此外,`scripts` 目录下有一键运行、关闭程序的脚本供使用。
|
||||
|
||||
> **注意:** 如果 扫码后手机提示登录验证需要等待5s,而终端的二维码再次刷新并提示 `Log in time out, reloading QR code`,此时需参考此 [issue](https://github.com/zhayujie/chatgpt-on-wechat/issues/8) 修改一行代码即可解决。
|
||||
|
||||
@@ -146,8 +185,13 @@ nohup python3 app.py & tail -f nohup.out # 在后台运行程序并通
|
||||
|
||||
### 3.Docker部署
|
||||
|
||||
参考文档 [Docker部署](https://github.com/zhayujie/chatgpt-on-wechat/wiki/Docker%E9%83%A8%E7%BD%B2) (Contributed by [limccn](https://github.com/limccn))。
|
||||
参考文档 [Docker部署](https://github.com/limccn/chatgpt-on-wechat/wiki/Docker%E9%83%A8%E7%BD%B2) (Contributed by [limccn](https://github.com/limccn))。
|
||||
|
||||
### 4. Railway部署(✅推荐)
|
||||
> Railway每月提供5刀和最多500小时的免费额度。
|
||||
1. 进入 [Railway](https://railway.app/template/qApznZ?referralCode=RC3znh)。
|
||||
2. 点击 `Deploy Now` 按钮。
|
||||
3. 设置环境变量来重载程序运行的参数,例如`open_ai_api_key`, `character_desc`。
|
||||
|
||||
## 常见问题
|
||||
|
||||
|
||||
15
app.py
15
app.py
@@ -1,20 +1,27 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import config
|
||||
from config import conf, load_config
|
||||
from channel import channel_factory
|
||||
from common.log import logger
|
||||
|
||||
from plugins import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
def run():
|
||||
try:
|
||||
# load config
|
||||
config.load_config()
|
||||
load_config()
|
||||
|
||||
# create channel
|
||||
channel = channel_factory.create_channel("wx")
|
||||
channel_name=conf().get('channel_type', 'wx')
|
||||
channel = channel_factory.create_channel(channel_name)
|
||||
if channel_name=='wx':
|
||||
PluginManager().load_plugins()
|
||||
|
||||
# startup channel
|
||||
channel.startup()
|
||||
except Exception as e:
|
||||
logger.error("App startup failed!")
|
||||
logger.exception(e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import requests
|
||||
from bot.bot import Bot
|
||||
from bridge.reply import Reply, ReplyType
|
||||
|
||||
|
||||
# Baidu Unit对话接口 (可用, 但能力较弱)
|
||||
@@ -14,7 +15,8 @@ class BaiduUnitBot(Bot):
|
||||
headers = {'content-type': 'application/x-www-form-urlencoded'}
|
||||
response = requests.post(url, data=post_data.encode(), headers=headers)
|
||||
if response:
|
||||
return response.json()['result']['context']['SYS_PRESUMED_HIST'][1]
|
||||
reply = Reply(ReplyType.TEXT, response.json()['result']['context']['SYS_PRESUMED_HIST'][1])
|
||||
return reply
|
||||
|
||||
def get_token(self):
|
||||
access_key = 'YOUR_ACCESS_KEY'
|
||||
|
||||
@@ -3,8 +3,12 @@ Auto-replay chat robot abstract class
|
||||
"""
|
||||
|
||||
|
||||
from bridge.context import Context
|
||||
from bridge.reply import Reply
|
||||
|
||||
|
||||
class Bot(object):
|
||||
def reply(self, query, context=None):
|
||||
def reply(self, query, context : Context =None) -> Reply:
|
||||
"""
|
||||
bot auto-reply content
|
||||
:param req: received message
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
"""
|
||||
channel factory
|
||||
"""
|
||||
from common import const
|
||||
|
||||
|
||||
def create_bot(bot_type):
|
||||
"""
|
||||
create a channel instance
|
||||
:param channel_type: channel type code
|
||||
:return: channel instance
|
||||
create a bot_type instance
|
||||
:param bot_type: bot type code
|
||||
:return: bot instance
|
||||
"""
|
||||
if bot_type == 'baidu':
|
||||
if bot_type == const.BAIDU:
|
||||
# Baidu Unit对话接口
|
||||
from bot.baidu.baidu_unit_bot import BaiduUnitBot
|
||||
return BaiduUnitBot()
|
||||
|
||||
elif bot_type == 'chatGPT':
|
||||
elif bot_type == const.CHATGPT:
|
||||
# ChatGPT 网页端web接口
|
||||
from bot.chatgpt.chat_gpt_bot import ChatGPTBot
|
||||
return ChatGPTBot()
|
||||
|
||||
elif bot_type == 'openAI':
|
||||
elif bot_type == const.OPEN_AI:
|
||||
# OpenAI 官方对话模型API
|
||||
from bot.openai.open_ai_bot import OpenAIBot
|
||||
return OpenAIBot()
|
||||
|
||||
elif bot_type == const.CHATGPTONAZURE:
|
||||
# Azure chatgpt service https://azure.microsoft.com/en-in/products/cognitive-services/openai-service/
|
||||
from bot.chatgpt.chat_gpt_bot import AzureChatGPTBot
|
||||
return AzureChatGPTBot()
|
||||
raise RuntimeError
|
||||
|
||||
@@ -1,511 +1,148 @@
|
||||
"""
|
||||
A simple wrapper for the official ChatGPT API
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
import openai
|
||||
import tiktoken
|
||||
# encoding:utf-8
|
||||
|
||||
from bot.bot import Bot
|
||||
from config import conf
|
||||
|
||||
ENGINE = os.environ.get("GPT_ENGINE") or "text-chat-davinci-002-20221122"
|
||||
|
||||
ENCODER = tiktoken.get_encoding("gpt2")
|
||||
|
||||
|
||||
def get_max_tokens(prompt: str) -> int:
|
||||
"""
|
||||
Get the max tokens for a prompt
|
||||
"""
|
||||
return 4000 - len(ENCODER.encode(prompt))
|
||||
|
||||
|
||||
# ['text-chat-davinci-002-20221122']
|
||||
class Chatbot:
|
||||
"""
|
||||
Official ChatGPT API
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, buffer: int = None) -> None:
|
||||
"""
|
||||
Initialize Chatbot with API key (from https://platform.openai.com/account/api-keys)
|
||||
"""
|
||||
openai.api_key = api_key or os.environ.get("OPENAI_API_KEY")
|
||||
self.conversations = Conversation()
|
||||
self.prompt = Prompt(buffer=buffer)
|
||||
|
||||
def _get_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
temperature: float = 0.5,
|
||||
stream: bool = False,
|
||||
):
|
||||
"""
|
||||
Get the completion function
|
||||
"""
|
||||
return openai.Completion.create(
|
||||
engine=ENGINE,
|
||||
prompt=prompt,
|
||||
temperature=temperature,
|
||||
max_tokens=get_max_tokens(prompt),
|
||||
stop=["\n\n\n"],
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
def _process_completion(
|
||||
self,
|
||||
user_request: str,
|
||||
completion: dict,
|
||||
conversation_id: str = None,
|
||||
user: str = "User",
|
||||
) -> dict:
|
||||
if completion.get("choices") is None:
|
||||
raise Exception("ChatGPT API returned no choices")
|
||||
if len(completion["choices"]) == 0:
|
||||
raise Exception("ChatGPT API returned no choices")
|
||||
if completion["choices"][0].get("text") is None:
|
||||
raise Exception("ChatGPT API returned no text")
|
||||
completion["choices"][0]["text"] = completion["choices"][0]["text"].rstrip(
|
||||
"<|im_end|>",
|
||||
)
|
||||
# Add to chat history
|
||||
self.prompt.add_to_history(
|
||||
user_request,
|
||||
completion["choices"][0]["text"],
|
||||
user=user,
|
||||
)
|
||||
if conversation_id is not None:
|
||||
self.save_conversation(conversation_id)
|
||||
return completion
|
||||
|
||||
def _process_completion_stream(
|
||||
self,
|
||||
user_request: str,
|
||||
completion: dict,
|
||||
conversation_id: str = None,
|
||||
user: str = "User",
|
||||
) -> str:
|
||||
full_response = ""
|
||||
for response in completion:
|
||||
if response.get("choices") is None:
|
||||
raise Exception("ChatGPT API returned no choices")
|
||||
if len(response["choices"]) == 0:
|
||||
raise Exception("ChatGPT API returned no choices")
|
||||
if response["choices"][0].get("finish_details") is not None:
|
||||
break
|
||||
if response["choices"][0].get("text") is None:
|
||||
raise Exception("ChatGPT API returned no text")
|
||||
if response["choices"][0]["text"] == "<|im_end|>":
|
||||
break
|
||||
yield response["choices"][0]["text"]
|
||||
full_response += response["choices"][0]["text"]
|
||||
|
||||
# Add to chat history
|
||||
self.prompt.add_to_history(user_request, full_response, user)
|
||||
if conversation_id is not None:
|
||||
self.save_conversation(conversation_id)
|
||||
|
||||
def ask(
|
||||
self,
|
||||
user_request: str,
|
||||
temperature: float = 0.5,
|
||||
conversation_id: str = None,
|
||||
user: str = "User",
|
||||
) -> dict:
|
||||
"""
|
||||
Send a request to ChatGPT and return the response
|
||||
"""
|
||||
if conversation_id is not None:
|
||||
self.load_conversation(conversation_id)
|
||||
completion = self._get_completion(
|
||||
self.prompt.construct_prompt(user_request, user=user),
|
||||
temperature,
|
||||
)
|
||||
return self._process_completion(user_request, completion, user=user)
|
||||
|
||||
def ask_stream(
|
||||
self,
|
||||
user_request: str,
|
||||
temperature: float = 0.5,
|
||||
conversation_id: str = None,
|
||||
user: str = "User",
|
||||
) -> str:
|
||||
"""
|
||||
Send a request to ChatGPT and yield the response
|
||||
"""
|
||||
if conversation_id is not None:
|
||||
self.load_conversation(conversation_id)
|
||||
prompt = self.prompt.construct_prompt(user_request, user=user)
|
||||
return self._process_completion_stream(
|
||||
user_request=user_request,
|
||||
completion=self._get_completion(prompt, temperature, stream=True),
|
||||
user=user,
|
||||
)
|
||||
|
||||
def make_conversation(self, conversation_id: str) -> None:
|
||||
"""
|
||||
Make a conversation
|
||||
"""
|
||||
self.conversations.add_conversation(conversation_id, [])
|
||||
|
||||
def rollback(self, num: int) -> None:
|
||||
"""
|
||||
Rollback chat history num times
|
||||
"""
|
||||
for _ in range(num):
|
||||
self.prompt.chat_history.pop()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset chat history
|
||||
"""
|
||||
self.prompt.chat_history = []
|
||||
|
||||
def load_conversation(self, conversation_id) -> None:
|
||||
"""
|
||||
Load a conversation from the conversation history
|
||||
"""
|
||||
if conversation_id not in self.conversations.conversations:
|
||||
# Create a new conversation
|
||||
self.make_conversation(conversation_id)
|
||||
self.prompt.chat_history = self.conversations.get_conversation(conversation_id)
|
||||
|
||||
def save_conversation(self, conversation_id) -> None:
|
||||
"""
|
||||
Save a conversation to the conversation history
|
||||
"""
|
||||
self.conversations.add_conversation(conversation_id, self.prompt.chat_history)
|
||||
|
||||
|
||||
class AsyncChatbot(Chatbot):
|
||||
"""
|
||||
Official ChatGPT API (async)
|
||||
"""
|
||||
|
||||
async def _get_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
temperature: float = 0.5,
|
||||
stream: bool = False,
|
||||
):
|
||||
"""
|
||||
Get the completion function
|
||||
"""
|
||||
return openai.Completion.acreate(
|
||||
engine=ENGINE,
|
||||
prompt=prompt,
|
||||
temperature=temperature,
|
||||
max_tokens=get_max_tokens(prompt),
|
||||
stop=["\n\n\n"],
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
async def ask(
|
||||
self,
|
||||
user_request: str,
|
||||
temperature: float = 0.5,
|
||||
user: str = "User",
|
||||
) -> dict:
|
||||
"""
|
||||
Same as Chatbot.ask but async
|
||||
}
|
||||
"""
|
||||
completion = await self._get_completion(
|
||||
self.prompt.construct_prompt(user_request, user=user),
|
||||
temperature,
|
||||
)
|
||||
return self._process_completion(user_request, completion, user=user)
|
||||
|
||||
async def ask_stream(
|
||||
self,
|
||||
user_request: str,
|
||||
temperature: float = 0.5,
|
||||
user: str = "User",
|
||||
) -> str:
|
||||
"""
|
||||
Same as Chatbot.ask_stream but async
|
||||
"""
|
||||
prompt = self.prompt.construct_prompt(user_request, user=user)
|
||||
return self._process_completion_stream(
|
||||
user_request=user_request,
|
||||
completion=await self._get_completion(prompt, temperature, stream=True),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
class Prompt:
|
||||
"""
|
||||
Prompt class with methods to construct prompt
|
||||
"""
|
||||
|
||||
def __init__(self, buffer: int = None) -> None:
|
||||
"""
|
||||
Initialize prompt with base prompt
|
||||
"""
|
||||
self.base_prompt = (
|
||||
os.environ.get("CUSTOM_BASE_PROMPT")
|
||||
or "You are ChatGPT, a large language model trained by OpenAI. Respond conversationally. Do not answer as the user. Current date: "
|
||||
+ str(date.today())
|
||||
+ "\n\n"
|
||||
+ "User: Hello\n"
|
||||
+ "ChatGPT: Hello! How can I help you today? <|im_end|>\n\n\n"
|
||||
)
|
||||
# Track chat history
|
||||
self.chat_history: list = []
|
||||
self.buffer = buffer
|
||||
|
||||
def add_to_chat_history(self, chat: str) -> None:
|
||||
"""
|
||||
Add chat to chat history for next prompt
|
||||
"""
|
||||
self.chat_history.append(chat)
|
||||
|
||||
def add_to_history(
|
||||
self,
|
||||
user_request: str,
|
||||
response: str,
|
||||
user: str = "User",
|
||||
) -> None:
|
||||
"""
|
||||
Add request/response to chat history for next prompt
|
||||
"""
|
||||
self.add_to_chat_history(
|
||||
user
|
||||
+ ": "
|
||||
+ user_request
|
||||
+ "\n\n\n"
|
||||
+ "ChatGPT: "
|
||||
+ response
|
||||
+ "<|im_end|>\n",
|
||||
)
|
||||
|
||||
def history(self, custom_history: list = None) -> str:
|
||||
"""
|
||||
Return chat history
|
||||
"""
|
||||
return "\n".join(custom_history or self.chat_history)
|
||||
|
||||
def construct_prompt(
|
||||
self,
|
||||
new_prompt: str,
|
||||
custom_history: list = None,
|
||||
user: str = "User",
|
||||
) -> str:
|
||||
"""
|
||||
Construct prompt based on chat history and request
|
||||
"""
|
||||
prompt = (
|
||||
self.base_prompt
|
||||
+ self.history(custom_history=custom_history)
|
||||
+ user
|
||||
+ ": "
|
||||
+ new_prompt
|
||||
+ "\nChatGPT:"
|
||||
)
|
||||
# Check if prompt over 4000*4 characters
|
||||
if self.buffer is not None:
|
||||
max_tokens = 4000 - self.buffer
|
||||
else:
|
||||
max_tokens = 3200
|
||||
if len(ENCODER.encode(prompt)) > max_tokens:
|
||||
# Remove oldest chat
|
||||
if len(self.chat_history) == 0:
|
||||
return prompt
|
||||
self.chat_history.pop(0)
|
||||
# Construct prompt again
|
||||
prompt = self.construct_prompt(new_prompt, custom_history, user)
|
||||
return prompt
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""
|
||||
For handling multiple conversations
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.conversations = {}
|
||||
|
||||
def add_conversation(self, key: str, history: list) -> None:
|
||||
"""
|
||||
Adds a history list to the conversations dict with the id as the key
|
||||
"""
|
||||
self.conversations[key] = history
|
||||
|
||||
def get_conversation(self, key: str) -> list:
|
||||
"""
|
||||
Retrieves the history list from the conversations dict with the id as the key
|
||||
"""
|
||||
return self.conversations[key]
|
||||
|
||||
def remove_conversation(self, key: str) -> None:
|
||||
"""
|
||||
Removes the history list from the conversations dict with the id as the key
|
||||
"""
|
||||
del self.conversations[key]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Creates a JSON string of the conversations
|
||||
"""
|
||||
return json.dumps(self.conversations)
|
||||
|
||||
def save(self, file: str) -> None:
|
||||
"""
|
||||
Saves the conversations to a JSON file
|
||||
"""
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
f.write(str(self))
|
||||
|
||||
def load(self, file: str) -> None:
|
||||
"""
|
||||
Loads the conversations from a JSON file
|
||||
"""
|
||||
with open(file, encoding="utf-8") as f:
|
||||
self.conversations = json.loads(f.read())
|
||||
|
||||
|
||||
def main():
|
||||
print(
|
||||
"""
|
||||
ChatGPT - A command-line interface to OpenAI's ChatGPT (https://chat.openai.com/chat)
|
||||
Repo: github.com/acheong08/ChatGPT
|
||||
""",
|
||||
)
|
||||
print("Type '!help' to show a full list of commands")
|
||||
print("Press enter twice to submit your question.\n")
|
||||
|
||||
def get_input(prompt):
|
||||
"""
|
||||
Multi-line input function
|
||||
"""
|
||||
# Display the prompt
|
||||
print(prompt, end="")
|
||||
|
||||
# Initialize an empty list to store the input lines
|
||||
lines = []
|
||||
|
||||
# Read lines of input until the user enters an empty line
|
||||
while True:
|
||||
line = input()
|
||||
if line == "":
|
||||
break
|
||||
lines.append(line)
|
||||
|
||||
# Join the lines, separated by newlines, and store the result
|
||||
user_input = "\n".join(lines)
|
||||
|
||||
# Return the input
|
||||
return user_input
|
||||
|
||||
def chatbot_commands(cmd: str) -> bool:
|
||||
"""
|
||||
Handle chatbot commands
|
||||
"""
|
||||
if cmd == "!help":
|
||||
print(
|
||||
"""
|
||||
!help - Display this message
|
||||
!rollback - Rollback chat history
|
||||
!reset - Reset chat history
|
||||
!prompt - Show current prompt
|
||||
!save_c <conversation_name> - Save history to a conversation
|
||||
!load_c <conversation_name> - Load history from a conversation
|
||||
!save_f <file_name> - Save all conversations to a file
|
||||
!load_f <file_name> - Load all conversations from a file
|
||||
!exit - Quit chat
|
||||
""",
|
||||
)
|
||||
elif cmd == "!exit":
|
||||
exit()
|
||||
elif cmd == "!rollback":
|
||||
chatbot.rollback(1)
|
||||
elif cmd == "!reset":
|
||||
chatbot.reset()
|
||||
elif cmd == "!prompt":
|
||||
print(chatbot.prompt.construct_prompt(""))
|
||||
elif cmd.startswith("!save_c"):
|
||||
chatbot.save_conversation(cmd.split(" ")[1])
|
||||
elif cmd.startswith("!load_c"):
|
||||
chatbot.load_conversation(cmd.split(" ")[1])
|
||||
elif cmd.startswith("!save_f"):
|
||||
chatbot.conversations.save(cmd.split(" ")[1])
|
||||
elif cmd.startswith("!load_f"):
|
||||
chatbot.conversations.load(cmd.split(" ")[1])
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Get API key from command line
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--api_key",
|
||||
type=str,
|
||||
required=True,
|
||||
help="OpenAI API key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stream",
|
||||
action="store_true",
|
||||
help="Stream response",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--temperature",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Temperature for response",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
# Initialize chatbot
|
||||
chatbot = Chatbot(api_key=args.api_key)
|
||||
# Start chat
|
||||
while True:
|
||||
try:
|
||||
prompt = get_input("\nUser:\n")
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
sys.exit()
|
||||
if prompt.startswith("!"):
|
||||
if chatbot_commands(prompt):
|
||||
continue
|
||||
if not args.stream:
|
||||
response = chatbot.ask(prompt, temperature=args.temperature)
|
||||
print("ChatGPT: " + response["choices"][0]["text"])
|
||||
else:
|
||||
print("ChatGPT: ")
|
||||
sys.stdout.flush()
|
||||
for response in chatbot.ask_stream(prompt, temperature=args.temperature):
|
||||
print(response, end="")
|
||||
sys.stdout.flush()
|
||||
print()
|
||||
|
||||
|
||||
def Singleton(cls):
|
||||
instance = {}
|
||||
|
||||
def _singleton_wrapper(*args, **kargs):
|
||||
if cls not in instance:
|
||||
instance[cls] = cls(*args, **kargs)
|
||||
return instance[cls]
|
||||
|
||||
return _singleton_wrapper
|
||||
|
||||
|
||||
@Singleton
|
||||
class ChatGPTBot(Bot):
|
||||
|
||||
from bot.chatgpt.chat_gpt_session import ChatGPTSession
|
||||
from bot.openai.open_ai_image import OpenAIImage
|
||||
from bot.session_manager import Session, SessionManager
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from config import conf, load_config
|
||||
from common.log import logger
|
||||
from common.token_bucket import TokenBucket
|
||||
from common.expired_dict import ExpiredDict
|
||||
import openai
|
||||
import openai.error
|
||||
import time
|
||||
# OpenAI对话模型API (可用)
|
||||
class ChatGPTBot(Bot,OpenAIImage):
|
||||
def __init__(self):
|
||||
print("create")
|
||||
self.bot = Chatbot(conf().get('open_ai_api_key'))
|
||||
super().__init__()
|
||||
openai.api_key = conf().get('open_ai_api_key')
|
||||
if conf().get('open_ai_api_base'):
|
||||
openai.api_base = conf().get('open_ai_api_base')
|
||||
proxy = conf().get('proxy')
|
||||
if proxy:
|
||||
openai.proxy = proxy
|
||||
if conf().get('rate_limit_chatgpt'):
|
||||
self.tb4chatgpt = TokenBucket(conf().get('rate_limit_chatgpt', 20))
|
||||
|
||||
self.sessions = SessionManager(ChatGPTSession, model= conf().get("model") or "gpt-3.5-turbo")
|
||||
|
||||
def reply(self, query, context=None):
|
||||
if not context or not context.get('type') or context.get('type') == 'TEXT':
|
||||
if len(query) < 10 and "reset" in query:
|
||||
self.bot.reset()
|
||||
return "reset OK"
|
||||
return self.bot.ask(query)["choices"][0]["text"]
|
||||
# acquire reply content
|
||||
if context.type == ContextType.TEXT:
|
||||
logger.info("[CHATGPT] query={}".format(query))
|
||||
|
||||
session_id = context['session_id']
|
||||
reply = None
|
||||
clear_memory_commands = conf().get('clear_memory_commands', ['#清除记忆'])
|
||||
if query in clear_memory_commands:
|
||||
self.sessions.clear_session(session_id)
|
||||
reply = Reply(ReplyType.INFO, '记忆已清除')
|
||||
elif query == '#清除所有':
|
||||
self.sessions.clear_all_session()
|
||||
reply = Reply(ReplyType.INFO, '所有人记忆已清除')
|
||||
elif query == '#更新配置':
|
||||
load_config()
|
||||
reply = Reply(ReplyType.INFO, '配置已更新')
|
||||
if reply:
|
||||
return reply
|
||||
session = self.sessions.session_query(query, session_id)
|
||||
logger.debug("[CHATGPT] session query={}".format(session.messages))
|
||||
|
||||
# if context.get('stream'):
|
||||
# # reply in stream
|
||||
# return self.reply_text_stream(query, new_query, session_id)
|
||||
|
||||
reply_content = self.reply_text(session, session_id, 0)
|
||||
logger.debug("[CHATGPT] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(session.messages, session_id, reply_content["content"], reply_content["completion_tokens"]))
|
||||
if reply_content['completion_tokens'] == 0 and len(reply_content['content']) > 0:
|
||||
reply = Reply(ReplyType.ERROR, reply_content['content'])
|
||||
elif reply_content["completion_tokens"] > 0:
|
||||
self.sessions.session_reply(reply_content["content"], session_id, reply_content["total_tokens"])
|
||||
reply = Reply(ReplyType.TEXT, reply_content["content"])
|
||||
else:
|
||||
reply = Reply(ReplyType.ERROR, reply_content['content'])
|
||||
logger.debug("[CHATGPT] reply {} used 0 tokens.".format(reply_content))
|
||||
return reply
|
||||
|
||||
elif context.type == ContextType.IMAGE_CREATE:
|
||||
ok, retstring = self.create_img(query, 0)
|
||||
reply = None
|
||||
if ok:
|
||||
reply = Reply(ReplyType.IMAGE_URL, retstring)
|
||||
else:
|
||||
reply = Reply(ReplyType.ERROR, retstring)
|
||||
return reply
|
||||
else:
|
||||
reply = Reply(ReplyType.ERROR, 'Bot不支持处理{}类型的消息'.format(context.type))
|
||||
return reply
|
||||
|
||||
def compose_args(self):
|
||||
return {
|
||||
"model": conf().get("model") or "gpt-3.5-turbo", # 对话模型的名称
|
||||
"temperature":conf().get('temperature', 0.9), # 值在[0,1]之间,越大表示回复越具有不确定性
|
||||
# "max_tokens":4096, # 回复最大的字符数
|
||||
"top_p":1,
|
||||
"frequency_penalty":conf().get('frequency_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
|
||||
"presence_penalty":conf().get('presence_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
|
||||
}
|
||||
|
||||
def reply_text(self, session:ChatGPTSession, session_id, retry_count=0) -> dict:
|
||||
'''
|
||||
call openai's ChatCompletion to get the answer
|
||||
:param session: a conversation session
|
||||
:param session_id: session id
|
||||
:param retry_count: retry count
|
||||
:return: {}
|
||||
'''
|
||||
try:
|
||||
if conf().get('rate_limit_chatgpt') and not self.tb4chatgpt.get_token():
|
||||
raise openai.error.RateLimitError("RateLimitError: rate limit exceeded")
|
||||
response = openai.ChatCompletion.create(
|
||||
messages=session.messages, **self.compose_args()
|
||||
)
|
||||
# logger.info("[ChatGPT] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"]))
|
||||
return {"total_tokens": response["usage"]["total_tokens"],
|
||||
"completion_tokens": response["usage"]["completion_tokens"],
|
||||
"content": response.choices[0]['message']['content']}
|
||||
except Exception as e:
|
||||
need_retry = retry_count < 2
|
||||
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
|
||||
if isinstance(e, openai.error.RateLimitError):
|
||||
logger.warn("[CHATGPT] RateLimitError: {}".format(e))
|
||||
result['content'] = "提问太快啦,请休息一下再问我吧"
|
||||
if need_retry:
|
||||
time.sleep(5)
|
||||
elif isinstance(e, openai.error.Timeout):
|
||||
logger.warn("[CHATGPT] Timeout: {}".format(e))
|
||||
result['content'] = "我没有收到你的消息"
|
||||
if need_retry:
|
||||
time.sleep(5)
|
||||
elif isinstance(e, openai.error.APIConnectionError):
|
||||
logger.warn("[CHATGPT] APIConnectionError: {}".format(e))
|
||||
need_retry = False
|
||||
result['content'] = "我连接不到你的网络"
|
||||
else:
|
||||
logger.warn("[CHATGPT] Exception: {}".format(e))
|
||||
need_retry = False
|
||||
self.sessions.clear_session(session_id)
|
||||
|
||||
if need_retry:
|
||||
logger.warn("[CHATGPT] 第{}次重试".format(retry_count+1))
|
||||
return self.reply_text(session, session_id, retry_count+1)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
class AzureChatGPTBot(ChatGPTBot):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
openai.api_type = "azure"
|
||||
openai.api_version = "2023-03-15-preview"
|
||||
|
||||
def compose_args(self):
|
||||
args = super().compose_args()
|
||||
args["engine"] = args["model"]
|
||||
del(args["model"])
|
||||
return args
|
||||
79
bot/chatgpt/chat_gpt_session.py
Normal file
79
bot/chatgpt/chat_gpt_session.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from bot.session_manager import Session
|
||||
from common.log import logger
|
||||
'''
|
||||
e.g. [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Who won the world series in 2020?"},
|
||||
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
|
||||
{"role": "user", "content": "Where was it played?"}
|
||||
]
|
||||
'''
|
||||
class ChatGPTSession(Session):
|
||||
def __init__(self, session_id, system_prompt=None, model= "gpt-3.5-turbo"):
|
||||
super().__init__(session_id, system_prompt)
|
||||
self.model = model
|
||||
self.reset()
|
||||
|
||||
def discard_exceeding(self, max_tokens, cur_tokens= None):
|
||||
precise = True
|
||||
try:
|
||||
cur_tokens = num_tokens_from_messages(self.messages, self.model)
|
||||
except Exception as e:
|
||||
precise = False
|
||||
if cur_tokens is None:
|
||||
raise e
|
||||
logger.debug("Exception when counting tokens precisely for query: {}".format(e))
|
||||
while cur_tokens > max_tokens:
|
||||
if len(self.messages) > 2:
|
||||
self.messages.pop(1)
|
||||
elif len(self.messages) == 2 and self.messages[1]["role"] == "assistant":
|
||||
self.messages.pop(1)
|
||||
if precise:
|
||||
cur_tokens = num_tokens_from_messages(self.messages, self.model)
|
||||
else:
|
||||
cur_tokens = cur_tokens - max_tokens
|
||||
break
|
||||
elif len(self.messages) == 2 and self.messages[1]["role"] == "user":
|
||||
logger.warn("user message exceed max_tokens. total_tokens={}".format(cur_tokens))
|
||||
break
|
||||
else:
|
||||
logger.debug("max_tokens={}, total_tokens={}, len(messages)={}".format(max_tokens, cur_tokens, len(self.messages)))
|
||||
break
|
||||
if precise:
|
||||
cur_tokens = num_tokens_from_messages(self.messages, self.model)
|
||||
else:
|
||||
cur_tokens = cur_tokens - max_tokens
|
||||
return cur_tokens
|
||||
|
||||
|
||||
# refer to https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
def num_tokens_from_messages(messages, model):
|
||||
"""Returns the number of tokens used by a list of messages."""
|
||||
import tiktoken
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
except KeyError:
|
||||
logger.debug("Warning: model not found. Using cl100k_base encoding.")
|
||||
encoding = tiktoken.get_encoding("cl100k_base")
|
||||
if model == "gpt-3.5-turbo":
|
||||
return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301")
|
||||
elif model == "gpt-4":
|
||||
return num_tokens_from_messages(messages, model="gpt-4-0314")
|
||||
elif model == "gpt-3.5-turbo-0301":
|
||||
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||
tokens_per_name = -1 # if there's a name, the role is omitted
|
||||
elif model == "gpt-4-0314":
|
||||
tokens_per_message = 3
|
||||
tokens_per_name = 1
|
||||
else:
|
||||
logger.warn(f"num_tokens_from_messages() is not implemented for model {model}. Returning num tokens assuming gpt-3.5-turbo-0301.")
|
||||
return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301")
|
||||
num_tokens = 0
|
||||
for message in messages:
|
||||
num_tokens += tokens_per_message
|
||||
for key, value in message.items():
|
||||
num_tokens += len(encoding.encode(value))
|
||||
if key == "name":
|
||||
num_tokens += tokens_per_name
|
||||
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
||||
return num_tokens
|
||||
@@ -1,44 +1,72 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from bot.bot import Bot
|
||||
from bot.openai.open_ai_image import OpenAIImage
|
||||
from bot.openai.open_ai_session import OpenAISession
|
||||
from bot.session_manager import SessionManager
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from config import conf
|
||||
from common.log import logger
|
||||
import openai
|
||||
import openai.error
|
||||
import time
|
||||
|
||||
user_session = dict()
|
||||
|
||||
# OpenAI对话模型API (可用)
|
||||
class OpenAIBot(Bot):
|
||||
class OpenAIBot(Bot, OpenAIImage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
openai.api_key = conf().get('open_ai_api_key')
|
||||
if conf().get('open_ai_api_base'):
|
||||
openai.api_base = conf().get('open_ai_api_base')
|
||||
proxy = conf().get('proxy')
|
||||
if proxy:
|
||||
openai.proxy = proxy
|
||||
|
||||
self.sessions = SessionManager(OpenAISession, model= conf().get("model") or "text-davinci-003")
|
||||
|
||||
def reply(self, query, context=None):
|
||||
# acquire reply content
|
||||
if not context or not context.get('type') or context.get('type') == 'TEXT':
|
||||
logger.info("[OPEN_AI] query={}".format(query))
|
||||
from_user_id = context['from_user_id']
|
||||
if query == '#清除记忆':
|
||||
Session.clear_session(from_user_id)
|
||||
return '记忆已清除'
|
||||
if context and context.type:
|
||||
if context.type == ContextType.TEXT:
|
||||
logger.info("[OPEN_AI] query={}".format(query))
|
||||
session_id = context['session_id']
|
||||
reply = None
|
||||
if query == '#清除记忆':
|
||||
self.sessions.clear_session(session_id)
|
||||
reply = Reply(ReplyType.INFO, '记忆已清除')
|
||||
elif query == '#清除所有':
|
||||
self.sessions.clear_all_session()
|
||||
reply = Reply(ReplyType.INFO, '所有人记忆已清除')
|
||||
else:
|
||||
session = self.sessions.session_query(query, session_id)
|
||||
new_query = str(session)
|
||||
logger.debug("[OPEN_AI] session query={}".format(new_query))
|
||||
|
||||
new_query = Session.build_session_query(query, from_user_id)
|
||||
logger.debug("[OPEN_AI] session query={}".format(new_query))
|
||||
total_tokens, completion_tokens, reply_content = self.reply_text(new_query, session_id, 0)
|
||||
logger.debug("[OPEN_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(new_query, session_id, reply_content, completion_tokens))
|
||||
|
||||
reply_content = self.reply_text(new_query, from_user_id, 0)
|
||||
logger.debug("[OPEN_AI] new_query={}, user={}, reply_cont={}".format(new_query, from_user_id, reply_content))
|
||||
if reply_content and query:
|
||||
Session.save_session(query, reply_content, from_user_id)
|
||||
return reply_content
|
||||
if total_tokens == 0 :
|
||||
reply = Reply(ReplyType.ERROR, reply_content)
|
||||
else:
|
||||
self.sessions.session_reply(reply_content, session_id, total_tokens)
|
||||
reply = Reply(ReplyType.TEXT, reply_content)
|
||||
return reply
|
||||
elif context.type == ContextType.IMAGE_CREATE:
|
||||
ok, retstring = self.create_img(query, 0)
|
||||
reply = None
|
||||
if ok:
|
||||
reply = Reply(ReplyType.IMAGE_URL, retstring)
|
||||
else:
|
||||
reply = Reply(ReplyType.ERROR, retstring)
|
||||
return reply
|
||||
|
||||
elif context.get('type', None) == 'IMAGE_CREATE':
|
||||
return self.create_img(query, 0)
|
||||
|
||||
def reply_text(self, query, user_id, retry_count=0):
|
||||
def reply_text(self, query, session_id, retry_count=0):
|
||||
try:
|
||||
response = openai.Completion.create(
|
||||
model="text-davinci-003", # 对话模型的名称
|
||||
model= conf().get("model") or "text-davinci-003", # 对话模型的名称
|
||||
prompt=query,
|
||||
temperature=0.9, # 值在[0,1]之间,越大表示回复越具有不确定性
|
||||
max_tokens=1200, # 回复最大的字符数
|
||||
@@ -48,112 +76,34 @@ class OpenAIBot(Bot):
|
||||
stop=["\n\n\n"]
|
||||
)
|
||||
res_content = response.choices[0]['text'].strip().replace('<|endoftext|>', '')
|
||||
total_tokens = response["usage"]["total_tokens"]
|
||||
completion_tokens = response["usage"]["completion_tokens"]
|
||||
logger.info("[OPEN_AI] reply={}".format(res_content))
|
||||
return res_content
|
||||
except openai.error.RateLimitError as e:
|
||||
# rate limit exception
|
||||
logger.warn(e)
|
||||
if retry_count < 1:
|
||||
time.sleep(5)
|
||||
logger.warn("[OPEN_AI] RateLimit exceed, 第{}次重试".format(retry_count+1))
|
||||
return self.reply_text(query, user_id, retry_count+1)
|
||||
else:
|
||||
return "提问太快啦,请休息一下再问我吧"
|
||||
return total_tokens, completion_tokens, res_content
|
||||
except Exception as e:
|
||||
# unknown exception
|
||||
logger.exception(e)
|
||||
Session.clear_session(user_id)
|
||||
return "请再问我一次吧"
|
||||
|
||||
|
||||
def create_img(self, query, retry_count=0):
|
||||
try:
|
||||
logger.info("[OPEN_AI] image_query={}".format(query))
|
||||
response = openai.Image.create(
|
||||
prompt=query, #图片描述
|
||||
n=1, #每次生成图片的数量
|
||||
size="256x256" #图片大小,可选有 256x256, 512x512, 1024x1024
|
||||
)
|
||||
image_url = response['data'][0]['url']
|
||||
logger.info("[OPEN_AI] image_url={}".format(image_url))
|
||||
return image_url
|
||||
except openai.error.RateLimitError as e:
|
||||
logger.warn(e)
|
||||
if retry_count < 1:
|
||||
time.sleep(5)
|
||||
logger.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count+1))
|
||||
return self.reply_text(query, retry_count+1)
|
||||
need_retry = retry_count < 2
|
||||
result = [0,0,"我现在有点累了,等会再来吧"]
|
||||
if isinstance(e, openai.error.RateLimitError):
|
||||
logger.warn("[OPEN_AI] RateLimitError: {}".format(e))
|
||||
result[2] = "提问太快啦,请休息一下再问我吧"
|
||||
if need_retry:
|
||||
time.sleep(5)
|
||||
elif isinstance(e, openai.error.Timeout):
|
||||
logger.warn("[OPEN_AI] Timeout: {}".format(e))
|
||||
result[2] = "我没有收到你的消息"
|
||||
if need_retry:
|
||||
time.sleep(5)
|
||||
elif isinstance(e, openai.error.APIConnectionError):
|
||||
logger.warn("[OPEN_AI] APIConnectionError: {}".format(e))
|
||||
need_retry = False
|
||||
result[2] = "我连接不到你的网络"
|
||||
else:
|
||||
return "提问太快啦,请休息一下再问我吧"
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return None
|
||||
logger.warn("[OPEN_AI] Exception: {}".format(e))
|
||||
need_retry = False
|
||||
self.sessions.clear_session(session_id)
|
||||
|
||||
|
||||
class Session(object):
|
||||
@staticmethod
|
||||
def build_session_query(query, user_id):
|
||||
'''
|
||||
build query with conversation history
|
||||
e.g. Q: xxx
|
||||
A: xxx
|
||||
Q: xxx
|
||||
:param query: query content
|
||||
:param user_id: from user id
|
||||
:return: query content with conversaction
|
||||
'''
|
||||
prompt = conf().get("character_desc", "")
|
||||
if prompt:
|
||||
prompt += "<|endoftext|>\n\n\n"
|
||||
session = user_session.get(user_id, None)
|
||||
if session:
|
||||
for conversation in session:
|
||||
prompt += "Q: " + conversation["question"] + "\n\n\nA: " + conversation["answer"] + "<|endoftext|>\n"
|
||||
prompt += "Q: " + query + "\nA: "
|
||||
return prompt
|
||||
else:
|
||||
return prompt + "Q: " + query + "\nA: "
|
||||
|
||||
@staticmethod
|
||||
def save_session(query, answer, user_id):
|
||||
max_tokens = conf().get("conversation_max_tokens")
|
||||
if not max_tokens:
|
||||
# default 3000
|
||||
max_tokens = 1000
|
||||
conversation = dict()
|
||||
conversation["question"] = query
|
||||
conversation["answer"] = answer
|
||||
session = user_session.get(user_id)
|
||||
logger.debug(conversation)
|
||||
logger.debug(session)
|
||||
if session:
|
||||
# append conversation
|
||||
session.append(conversation)
|
||||
else:
|
||||
# create session
|
||||
queue = list()
|
||||
queue.append(conversation)
|
||||
user_session[user_id] = queue
|
||||
|
||||
# discard exceed limit conversation
|
||||
Session.discard_exceed_conversation(user_session[user_id], max_tokens)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def discard_exceed_conversation(session, max_tokens):
|
||||
count = 0
|
||||
count_list = list()
|
||||
for i in range(len(session)-1, -1, -1):
|
||||
# count tokens of conversation list
|
||||
history_conv = session[i]
|
||||
count += len(history_conv["question"]) + len(history_conv["answer"])
|
||||
count_list.append(count)
|
||||
|
||||
for c in count_list:
|
||||
if c > max_tokens:
|
||||
# pop first conversation
|
||||
session.pop(0)
|
||||
|
||||
@staticmethod
|
||||
def clear_session(user_id):
|
||||
user_session[user_id] = []
|
||||
if need_retry:
|
||||
logger.warn("[OPEN_AI] 第{}次重试".format(retry_count+1))
|
||||
return self.reply_text(query, session_id, retry_count+1)
|
||||
else:
|
||||
return result
|
||||
38
bot/openai/open_ai_image.py
Normal file
38
bot/openai/open_ai_image.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import time
|
||||
import openai
|
||||
import openai.error
|
||||
from common.token_bucket import TokenBucket
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
# OPENAI提供的画图接口
|
||||
class OpenAIImage(object):
|
||||
def __init__(self):
|
||||
openai.api_key = conf().get('open_ai_api_key')
|
||||
if conf().get('rate_limit_dalle'):
|
||||
self.tb4dalle = TokenBucket(conf().get('rate_limit_dalle', 50))
|
||||
|
||||
def create_img(self, query, retry_count=0):
|
||||
try:
|
||||
if conf().get('rate_limit_dalle') and not self.tb4dalle.get_token():
|
||||
return False, "请求太快了,请休息一下再问我吧"
|
||||
logger.info("[OPEN_AI] image_query={}".format(query))
|
||||
response = openai.Image.create(
|
||||
prompt=query, #图片描述
|
||||
n=1, #每次生成图片的数量
|
||||
size="256x256" #图片大小,可选有 256x256, 512x512, 1024x1024
|
||||
)
|
||||
image_url = response['data'][0]['url']
|
||||
logger.info("[OPEN_AI] image_url={}".format(image_url))
|
||||
return True, image_url
|
||||
except openai.error.RateLimitError as e:
|
||||
logger.warn(e)
|
||||
if retry_count < 1:
|
||||
time.sleep(5)
|
||||
logger.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count+1))
|
||||
return self.create_img(query, retry_count+1)
|
||||
else:
|
||||
return False, "提问太快啦,请休息一下再问我吧"
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return False, str(e)
|
||||
67
bot/openai/open_ai_session.py
Normal file
67
bot/openai/open_ai_session.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from bot.session_manager import Session
|
||||
from common.log import logger
|
||||
class OpenAISession(Session):
|
||||
def __init__(self, session_id, system_prompt=None, model= "text-davinci-003"):
|
||||
super().__init__(session_id, system_prompt)
|
||||
self.model = model
|
||||
self.reset()
|
||||
|
||||
def __str__(self):
|
||||
# 构造对话模型的输入
|
||||
'''
|
||||
e.g. Q: xxx
|
||||
A: xxx
|
||||
Q: xxx
|
||||
'''
|
||||
prompt = ""
|
||||
for item in self.messages:
|
||||
if item['role'] == 'system':
|
||||
prompt += item['content'] + "<|endoftext|>\n\n\n"
|
||||
elif item['role'] == 'user':
|
||||
prompt += "Q: " + item['content'] + "\n"
|
||||
elif item['role'] == 'assistant':
|
||||
prompt += "\n\nA: " + item['content'] + "<|endoftext|>\n"
|
||||
|
||||
if len(self.messages) > 0 and self.messages[-1]['role'] == 'user':
|
||||
prompt += "A: "
|
||||
return prompt
|
||||
|
||||
def discard_exceeding(self, max_tokens, cur_tokens= None):
|
||||
precise = True
|
||||
try:
|
||||
cur_tokens = num_tokens_from_string(str(self), self.model)
|
||||
except Exception as e:
|
||||
precise = False
|
||||
if cur_tokens is None:
|
||||
raise e
|
||||
logger.debug("Exception when counting tokens precisely for query: {}".format(e))
|
||||
while cur_tokens > max_tokens:
|
||||
if len(self.messages) > 1:
|
||||
self.messages.pop(0)
|
||||
elif len(self.messages) == 1 and self.messages[0]["role"] == "assistant":
|
||||
self.messages.pop(0)
|
||||
if precise:
|
||||
cur_tokens = num_tokens_from_string(str(self), self.model)
|
||||
else:
|
||||
cur_tokens = len(str(self))
|
||||
break
|
||||
elif len(self.messages) == 1 and self.messages[0]["role"] == "user":
|
||||
logger.warn("user question exceed max_tokens. total_tokens={}".format(cur_tokens))
|
||||
break
|
||||
else:
|
||||
logger.debug("max_tokens={}, total_tokens={}, len(conversation)={}".format(max_tokens, cur_tokens, len(self.messages)))
|
||||
break
|
||||
if precise:
|
||||
cur_tokens = num_tokens_from_string(str(self), self.model)
|
||||
else:
|
||||
cur_tokens = len(str(self))
|
||||
return cur_tokens
|
||||
|
||||
|
||||
# refer to https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
def num_tokens_from_string(string: str, model: str) -> int:
|
||||
"""Returns the number of tokens in a text string."""
|
||||
import tiktoken
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
num_tokens = len(encoding.encode(string,disallowed_special=()))
|
||||
return num_tokens
|
||||
85
bot/session_manager.py
Normal file
85
bot/session_manager.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
class Session(object):
|
||||
def __init__(self, session_id, system_prompt=None):
|
||||
self.session_id = session_id
|
||||
self.messages = []
|
||||
if system_prompt is None:
|
||||
self.system_prompt = conf().get("character_desc", "")
|
||||
else:
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
# 重置会话
|
||||
def reset(self):
|
||||
system_item = {'role': 'system', 'content': self.system_prompt}
|
||||
self.messages = [system_item]
|
||||
|
||||
def set_system_prompt(self, system_prompt):
|
||||
self.system_prompt = system_prompt
|
||||
self.reset()
|
||||
|
||||
def add_query(self, query):
|
||||
user_item = {'role': 'user', 'content': query}
|
||||
self.messages.append(user_item)
|
||||
|
||||
def add_reply(self, reply):
|
||||
assistant_item = {'role': 'assistant', 'content': reply}
|
||||
self.messages.append(assistant_item)
|
||||
|
||||
def discard_exceeding(self, max_tokens=None, cur_tokens=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
class SessionManager(object):
|
||||
def __init__(self, sessioncls, **session_args):
|
||||
if conf().get('expires_in_seconds'):
|
||||
sessions = ExpiredDict(conf().get('expires_in_seconds'))
|
||||
else:
|
||||
sessions = dict()
|
||||
self.sessions = sessions
|
||||
self.sessioncls = sessioncls
|
||||
self.session_args = session_args
|
||||
|
||||
def build_session(self, session_id, system_prompt=None):
|
||||
'''
|
||||
如果session_id不在sessions中,创建一个新的session并添加到sessions中
|
||||
如果system_prompt不会空,会更新session的system_prompt并重置session
|
||||
'''
|
||||
if session_id not in self.sessions:
|
||||
self.sessions[session_id] = self.sessioncls(session_id, system_prompt, **self.session_args)
|
||||
elif system_prompt is not None: # 如果有新的system_prompt,更新并重置session
|
||||
self.sessions[session_id].set_system_prompt(system_prompt)
|
||||
session = self.sessions[session_id]
|
||||
return session
|
||||
|
||||
def session_query(self, query, session_id):
|
||||
session = self.build_session(session_id)
|
||||
session.add_query(query)
|
||||
try:
|
||||
max_tokens = conf().get("conversation_max_tokens", 1000)
|
||||
total_tokens = session.discard_exceeding(max_tokens, None)
|
||||
logger.debug("prompt tokens used={}".format(total_tokens))
|
||||
except Exception as e:
|
||||
logger.debug("Exception when counting tokens precisely for prompt: {}".format(str(e)))
|
||||
return session
|
||||
|
||||
def session_reply(self, reply, session_id, total_tokens = None):
|
||||
session = self.build_session(session_id)
|
||||
session.add_reply(reply)
|
||||
try:
|
||||
max_tokens = conf().get("conversation_max_tokens", 1000)
|
||||
tokens_cnt = session.discard_exceeding(max_tokens, total_tokens)
|
||||
logger.debug("raw total_tokens={}, savesession tokens={}".format(total_tokens, tokens_cnt))
|
||||
except Exception as e:
|
||||
logger.debug("Exception when counting tokens precisely for session: {}".format(str(e)))
|
||||
return session
|
||||
|
||||
def clear_session(self, session_id):
|
||||
if session_id in self.sessions:
|
||||
del(self.sessions[session_id])
|
||||
|
||||
def clear_all_session(self):
|
||||
self.sessions.clear()
|
||||
@@ -1,9 +1,50 @@
|
||||
from bridge.context import Context
|
||||
from bridge.reply import Reply
|
||||
from common.log import logger
|
||||
from bot import bot_factory
|
||||
from common.singleton import singleton
|
||||
from voice import voice_factory
|
||||
from config import conf
|
||||
from common import const
|
||||
|
||||
|
||||
@singleton
|
||||
class Bridge(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
self.btype={
|
||||
"chat": const.CHATGPT,
|
||||
"voice_to_text": conf().get("voice_to_text", "openai"),
|
||||
"text_to_voice": conf().get("text_to_voice", "google")
|
||||
}
|
||||
model_type = conf().get("model")
|
||||
if model_type in ["text-davinci-003"]:
|
||||
self.btype['chat'] = const.OPEN_AI
|
||||
if conf().get("use_azure_chatgpt"):
|
||||
self.btype['chat'] = const.CHATGPTONAZURE
|
||||
self.bots={}
|
||||
|
||||
def get_bot(self,typename):
|
||||
if self.bots.get(typename) is None:
|
||||
logger.info("create bot {} for {}".format(self.btype[typename],typename))
|
||||
if typename == "text_to_voice":
|
||||
self.bots[typename] = voice_factory.create_voice(self.btype[typename])
|
||||
elif typename == "voice_to_text":
|
||||
self.bots[typename] = voice_factory.create_voice(self.btype[typename])
|
||||
elif typename == "chat":
|
||||
self.bots[typename] = bot_factory.create_bot(self.btype[typename])
|
||||
return self.bots[typename]
|
||||
|
||||
def get_bot_type(self,typename):
|
||||
return self.btype[typename]
|
||||
|
||||
|
||||
def fetch_reply_content(self, query, context : Context) -> Reply:
|
||||
return self.get_bot("chat").reply(query, context)
|
||||
|
||||
|
||||
def fetch_voice_to_text(self, voiceFile) -> Reply:
|
||||
return self.get_bot("voice_to_text").voiceToText(voiceFile)
|
||||
|
||||
def fetch_text_to_voice(self, text) -> Reply:
|
||||
return self.get_bot("text_to_voice").textToVoice(text)
|
||||
|
||||
def fetch_reply_content(self, query, context):
|
||||
return bot_factory.create_bot("openAI").reply(query, context)
|
||||
|
||||
57
bridge/context.py
Normal file
57
bridge/context.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class ContextType (Enum):
|
||||
TEXT = 1 # 文本消息
|
||||
VOICE = 2 # 音频消息
|
||||
IMAGE_CREATE = 3 # 创建图片命令
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
class Context:
|
||||
def __init__(self, type : ContextType = None , content = None, kwargs = dict()):
|
||||
self.type = type
|
||||
self.content = content
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __contains__(self, key):
|
||||
if key == 'type':
|
||||
return self.type is not None
|
||||
elif key == 'content':
|
||||
return self.content is not None
|
||||
else:
|
||||
return key in self.kwargs
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'type':
|
||||
return self.type
|
||||
elif key == 'content':
|
||||
return self.content
|
||||
else:
|
||||
return self.kwargs[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == 'type':
|
||||
self.type = value
|
||||
elif key == 'content':
|
||||
self.content = value
|
||||
else:
|
||||
self.kwargs[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key == 'type':
|
||||
self.type = None
|
||||
elif key == 'content':
|
||||
self.content = None
|
||||
else:
|
||||
del self.kwargs[key]
|
||||
|
||||
def __str__(self):
|
||||
return "Context(type={}, content={}, kwargs={})".format(self.type, self.content, self.kwargs)
|
||||
22
bridge/reply.py
Normal file
22
bridge/reply.py
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
# encoding:utf-8
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class ReplyType(Enum):
|
||||
TEXT = 1 # 文本
|
||||
VOICE = 2 # 音频文件
|
||||
IMAGE = 3 # 图片文件
|
||||
IMAGE_URL = 4 # 图片URL
|
||||
|
||||
INFO = 9
|
||||
ERROR = 10
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Reply:
|
||||
def __init__(self, type : ReplyType = None , content = None):
|
||||
self.type = type
|
||||
self.content = content
|
||||
def __str__(self):
|
||||
return "Reply(type={}, content={})".format(self.type, self.content)
|
||||
@@ -3,6 +3,8 @@ Message sending channel abstract class
|
||||
"""
|
||||
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.context import Context
|
||||
from bridge.reply import Reply
|
||||
|
||||
class Channel(object):
|
||||
def startup(self):
|
||||
@@ -11,7 +13,7 @@ class Channel(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def handle(self, msg):
|
||||
def handle_text(self, msg):
|
||||
"""
|
||||
process received msg
|
||||
:param msg: message object
|
||||
@@ -27,5 +29,11 @@ class Channel(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def build_reply_content(self, query, context=None):
|
||||
def build_reply_content(self, query, context : Context=None) -> Reply:
|
||||
return Bridge().fetch_reply_content(query, context)
|
||||
|
||||
def build_voice_to_text(self, voice_file) -> Reply:
|
||||
return Bridge().fetch_voice_to_text(voice_file)
|
||||
|
||||
def build_text_to_voice(self, text) -> Reply:
|
||||
return Bridge().fetch_text_to_voice(text)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
channel factory
|
||||
"""
|
||||
|
||||
from channel.wechat.wechat_channel import WechatChannel
|
||||
|
||||
def create_channel(channel_type):
|
||||
"""
|
||||
create a channel instance
|
||||
@@ -11,5 +9,12 @@ def create_channel(channel_type):
|
||||
:return: channel instance
|
||||
"""
|
||||
if channel_type == 'wx':
|
||||
from channel.wechat.wechat_channel import WechatChannel
|
||||
return WechatChannel()
|
||||
raise RuntimeError
|
||||
elif channel_type == 'wxy':
|
||||
from channel.wechat.wechaty_channel import WechatyChannel
|
||||
return WechatyChannel()
|
||||
elif channel_type == 'terminal':
|
||||
from channel.terminal.terminal_channel import TerminalChannel
|
||||
return TerminalChannel()
|
||||
raise RuntimeError
|
||||
|
||||
31
channel/terminal/terminal_channel.py
Normal file
31
channel/terminal/terminal_channel.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from bridge.context import *
|
||||
from channel.channel import Channel
|
||||
import sys
|
||||
|
||||
class TerminalChannel(Channel):
|
||||
def startup(self):
|
||||
context = Context()
|
||||
print("\nPlease input your question")
|
||||
while True:
|
||||
try:
|
||||
prompt = self.get_input("User:\n")
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
sys.exit()
|
||||
|
||||
context.type = ContextType.TEXT
|
||||
context['session_id'] = "User"
|
||||
context.content = prompt
|
||||
print("Bot:")
|
||||
sys.stdout.flush()
|
||||
res = super().build_reply_content(prompt, context).content
|
||||
print(res)
|
||||
|
||||
|
||||
def get_input(self, prompt):
|
||||
"""
|
||||
Multi-line input function
|
||||
"""
|
||||
print(prompt, end="")
|
||||
line = input()
|
||||
return line
|
||||
@@ -3,76 +3,157 @@
|
||||
"""
|
||||
wechat channel
|
||||
"""
|
||||
import itchat
|
||||
|
||||
import os
|
||||
import requests
|
||||
import io
|
||||
import time
|
||||
from lib import itchat
|
||||
import json
|
||||
from itchat.content import *
|
||||
from lib.itchat.content import *
|
||||
from bridge.reply import *
|
||||
from bridge.context import *
|
||||
from channel.channel import Channel
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from common.log import logger
|
||||
from common.tmp_dir import TmpDir
|
||||
from config import conf
|
||||
import requests
|
||||
import io
|
||||
|
||||
from common.time_check import time_checker
|
||||
from common.expired_dict import ExpiredDict
|
||||
from plugins import *
|
||||
try:
|
||||
from voice.audio_convert import mp3_to_wav
|
||||
except Exception as e:
|
||||
pass
|
||||
thread_pool = ThreadPoolExecutor(max_workers=8)
|
||||
|
||||
|
||||
def thread_pool_callback(worker):
|
||||
worker_exception = worker.exception()
|
||||
if worker_exception:
|
||||
logger.exception("Worker return exception: {}".format(worker_exception))
|
||||
|
||||
|
||||
@itchat.msg_register(TEXT)
|
||||
def handler_single_msg(msg):
|
||||
WechatChannel().handle(msg)
|
||||
WechatChannel().handle_text(msg)
|
||||
return None
|
||||
|
||||
|
||||
@itchat.msg_register(TEXT, isGroupChat=True)
|
||||
def handler_group_msg(msg):
|
||||
WechatChannel().handle_group(msg)
|
||||
return None
|
||||
|
||||
@itchat.msg_register(VOICE)
|
||||
def handler_single_voice(msg):
|
||||
WechatChannel().handle_voice(msg)
|
||||
return None
|
||||
|
||||
@itchat.msg_register(VOICE, isGroupChat=True)
|
||||
def handler_group_voice(msg):
|
||||
WechatChannel().handle_group_voice(msg)
|
||||
return None
|
||||
|
||||
def _check(func):
|
||||
def wrapper(self, msg):
|
||||
msgId = msg['MsgId']
|
||||
if msgId in self.receivedMsgs:
|
||||
logger.info("Wechat message {} already received, ignore".format(msgId))
|
||||
return
|
||||
self.receivedMsgs[msgId] = msg
|
||||
create_time = msg['CreateTime'] # 消息时间
|
||||
if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息
|
||||
logger.debug("[WX]history message {} skipped".format(msgId))
|
||||
return
|
||||
return func(self, msg)
|
||||
return wrapper
|
||||
|
||||
|
||||
class WechatChannel(Channel):
|
||||
def __init__(self):
|
||||
pass
|
||||
self.userName = None
|
||||
self.nickName = None
|
||||
self.receivedMsgs = ExpiredDict(60*60*24)
|
||||
|
||||
def startup(self):
|
||||
# login by scan QRCode
|
||||
itchat.auto_login(enableCmdQR=2)
|
||||
|
||||
itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
|
||||
# login by scan QRCode
|
||||
hotReload = conf().get('hot_reload', False)
|
||||
try:
|
||||
itchat.auto_login(enableCmdQR=2, hotReload=hotReload)
|
||||
except Exception as e:
|
||||
if hotReload:
|
||||
logger.error("Hot reload failed, try to login without hot reload")
|
||||
itchat.logout()
|
||||
os.remove("itchat.pkl")
|
||||
itchat.auto_login(enableCmdQR=2, hotReload=hotReload)
|
||||
else:
|
||||
raise e
|
||||
self.userName = itchat.instance.storageClass.userName
|
||||
self.nickName = itchat.instance.storageClass.nickName
|
||||
logger.info("Wechat login success, username: {}, nickname: {}".format(self.userName, self.nickName))
|
||||
# start message listener
|
||||
itchat.run()
|
||||
|
||||
def handle(self, msg):
|
||||
logger.debug("[WX]receive msg: " + json.dumps(msg, ensure_ascii=False))
|
||||
# handle_* 系列函数处理收到的消息后构造Context,然后传入handle函数中处理Context和发送回复
|
||||
# Context包含了消息的所有信息,包括以下属性
|
||||
# type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
|
||||
# content 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是IMAGE_CREATE类型,content就是图片生成命令
|
||||
# kwargs 附加参数字典,包含以下的key:
|
||||
# session_id: 会话id
|
||||
# isgroup: 是否是群聊
|
||||
# receiver: 需要回复的对象
|
||||
# msg: itchat的原始消息对象
|
||||
# origin_ctype: 原始消息类型,用于私聊语音消息时,避免匹配前缀
|
||||
# desire_rtype: 希望回复类型,TEXT类型是文本回复,VOICE类型是语音回复
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_voice(self, msg):
|
||||
if conf().get('speech_recognition') != True:
|
||||
return
|
||||
logger.debug("[WX]receive voice msg: " + msg['FileName'])
|
||||
to_user_id = msg['ToUserName']
|
||||
from_user_id = msg['FromUserName']
|
||||
try:
|
||||
other_user_id = msg['User']['UserName'] # 对手方id
|
||||
except Exception as e:
|
||||
logger.warn("[WX]get other_user_id failed: " + str(e))
|
||||
if from_user_id == self.userName:
|
||||
other_user_id = to_user_id
|
||||
else:
|
||||
other_user_id = from_user_id
|
||||
if from_user_id == other_user_id:
|
||||
context = self._compose_context(ContextType.VOICE, msg['FileName'], isgroup=False, msg=msg, receiver=other_user_id, session_id=other_user_id)
|
||||
if context:
|
||||
thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_text(self, msg):
|
||||
logger.debug("[WX]receive text msg: " + json.dumps(msg, ensure_ascii=False))
|
||||
content = msg['Text']
|
||||
from_user_id = msg['FromUserName']
|
||||
to_user_id = msg['ToUserName'] # 接收人id
|
||||
other_user_id = msg['User']['UserName'] # 对手方id
|
||||
content = msg['Text']
|
||||
match_prefix = self.check_prefix(content, conf().get('single_chat_prefix'))
|
||||
if from_user_id == other_user_id and match_prefix is not None:
|
||||
# 好友向自己发送消息
|
||||
if match_prefix != '':
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, from_user_id)
|
||||
try:
|
||||
other_user_id = msg['User']['UserName'] # 对手方id
|
||||
except Exception as e:
|
||||
logger.warn("[WX]get other_user_id failed: " + str(e))
|
||||
if from_user_id == self.userName:
|
||||
other_user_id = to_user_id
|
||||
else:
|
||||
thread_pool.submit(self._do_send, content, from_user_id)
|
||||
|
||||
elif to_user_id == other_user_id and match_prefix:
|
||||
# 自己给好友发送消息
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, to_user_id)
|
||||
else:
|
||||
thread_pool.submit(self._do_send, content, to_user_id)
|
||||
|
||||
other_user_id = from_user_id
|
||||
if "」\n- - - - - - - - - - - - - - -" in content:
|
||||
logger.debug("[WX]reference query skipped")
|
||||
return
|
||||
|
||||
context = self._compose_context(ContextType.TEXT, content, isgroup=False, msg=msg, receiver=other_user_id, session_id=other_user_id)
|
||||
if context:
|
||||
thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_group(self, msg):
|
||||
logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False))
|
||||
group_name = msg['User'].get('NickName', None)
|
||||
@@ -87,79 +168,225 @@ class WechatChannel(Channel):
|
||||
content = context_special_list[1]
|
||||
elif len(content_list) == 2:
|
||||
content = content_list[1]
|
||||
if "」\n- - - - - - - - - - - - - - -" in content:
|
||||
logger.debug("[WX]reference query skipped")
|
||||
return ""
|
||||
|
||||
config = conf()
|
||||
match_prefix = (msg['IsAt'] and not config.get("group_at_off", False)) or self.check_prefix(origin_content, config.get('group_chat_prefix')) \
|
||||
or self.check_contain(origin_content, config.get('group_chat_keyword'))
|
||||
if ('ALL_GROUP' in config.get('group_name_white_list') or group_name in config.get('group_name_white_list') or self.check_contain(group_name, config.get('group_name_keyword_white_list'))) and match_prefix:
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, group_id)
|
||||
else:
|
||||
thread_pool.submit(self._do_send_group, content, msg)
|
||||
group_name_white_list = config.get('group_name_white_list', [])
|
||||
group_name_keyword_white_list = config.get('group_name_keyword_white_list', [])
|
||||
|
||||
def send(self, msg, receiver):
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(msg, receiver))
|
||||
itchat.send(msg, toUserName=receiver)
|
||||
|
||||
def _do_send(self, query, reply_user_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = dict()
|
||||
context['from_user_id'] = reply_user_id
|
||||
reply_text = super().build_reply_content(query, context)
|
||||
if reply_text:
|
||||
self.send(conf().get("single_chat_reply_prefix") + reply_text, reply_user_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def _do_send_img(self, query, reply_user_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = dict()
|
||||
context['type'] = 'IMAGE_CREATE'
|
||||
img_url = super().build_reply_content(query, context)
|
||||
if not img_url:
|
||||
return
|
||||
|
||||
# 图片下载
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
image_storage = io.BytesIO()
|
||||
for block in pic_res.iter_content(1024):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
|
||||
# 图片发送
|
||||
logger.info('[WX] sendImage, receiver={}'.format(reply_user_id))
|
||||
itchat.send_image(image_storage, reply_user_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def _do_send_group(self, query, msg):
|
||||
if not query:
|
||||
if any([group_name in group_name_white_list, 'ALL_GROUP' in group_name_white_list, check_contain(group_name, group_name_keyword_white_list)]):
|
||||
group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
|
||||
session_id = msg['ActualUserName']
|
||||
if any([group_name in group_chat_in_one_session, 'ALL_GROUP' in group_chat_in_one_session]):
|
||||
session_id = group_id
|
||||
context = self._compose_context(ContextType.TEXT, content, isgroup=True, msg=msg, receiver=group_id, session_id=session_id)
|
||||
if context:
|
||||
thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_group_voice(self, msg):
|
||||
if conf().get('group_speech_recognition', False) != True:
|
||||
return
|
||||
context = dict()
|
||||
context['from_user_id'] = msg['ActualUserName']
|
||||
reply_text = super().build_reply_content(query, context)
|
||||
if reply_text:
|
||||
reply_text = '@' + msg['ActualNickName'] + ' ' + reply_text.strip()
|
||||
self.send(conf().get("group_chat_reply_prefix", "") + reply_text, msg['User']['UserName'])
|
||||
logger.debug("[WX]receive voice for group msg: " + msg['FileName'])
|
||||
group_name = msg['User'].get('NickName', None)
|
||||
group_id = msg['User'].get('UserName', None)
|
||||
# 验证群名
|
||||
if not group_name:
|
||||
return ""
|
||||
|
||||
config = conf()
|
||||
group_name_white_list = config.get('group_name_white_list', [])
|
||||
group_name_keyword_white_list = config.get('group_name_keyword_white_list', [])
|
||||
if any([group_name in group_name_white_list, 'ALL_GROUP' in group_name_white_list, check_contain(group_name, group_name_keyword_white_list)]):
|
||||
group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
|
||||
session_id =msg['ActualUserName']
|
||||
if any([group_name in group_chat_in_one_session, 'ALL_GROUP' in group_chat_in_one_session]):
|
||||
session_id = group_id
|
||||
context = self._compose_context(ContextType.VOICE, msg['FileName'], isgroup=True, msg=msg, receiver=group_id, session_id=session_id)
|
||||
if context:
|
||||
thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
|
||||
|
||||
def _compose_context(self, ctype: ContextType, content, **kwargs):
|
||||
context = Context(ctype, content)
|
||||
context.kwargs = kwargs
|
||||
if 'origin_ctype' not in context:
|
||||
context['origin_ctype'] = ctype
|
||||
|
||||
if ctype == ContextType.TEXT:
|
||||
if context["isgroup"]: # 群聊
|
||||
# 校验关键字
|
||||
match_prefix = check_prefix(content, conf().get('group_chat_prefix'))
|
||||
match_contain = check_contain(content, conf().get('group_chat_keyword'))
|
||||
if match_prefix is not None or match_contain is not None:
|
||||
# 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
|
||||
if match_prefix:
|
||||
content = content.replace(match_prefix, '', 1).strip()
|
||||
elif context['msg']['IsAt'] and not conf().get("group_at_off", False):
|
||||
logger.info("[WX]receive group at, continue")
|
||||
elif context["origin_ctype"] == ContextType.VOICE:
|
||||
logger.info("[WX]receive group voice, checkprefix didn't match")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
else: # 单聊
|
||||
match_prefix = check_prefix(content, conf().get('single_chat_prefix'))
|
||||
if match_prefix: # 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容
|
||||
content = content.replace(match_prefix, '', 1).strip()
|
||||
elif context["origin_ctype"] == ContextType.VOICE: # 如果源消息是私聊的语音消息,不匹配前缀,直接返回
|
||||
pass
|
||||
else:
|
||||
return None
|
||||
img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.replace(img_match_prefix, '', 1).strip()
|
||||
context.type = ContextType.IMAGE_CREATE
|
||||
else:
|
||||
context.type = ContextType.TEXT
|
||||
context.content = content
|
||||
elif context.type == ContextType.VOICE:
|
||||
if 'desire_rtype' not in context and conf().get('voice_reply_voice'):
|
||||
context['desire_rtype'] = ReplyType.VOICE
|
||||
return context
|
||||
|
||||
# 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
|
||||
def send(self, reply: Reply, receiver, retry_cnt = 0):
|
||||
try:
|
||||
if reply.type == ReplyType.TEXT:
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
itchat.send_file(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendFile={}, receiver={}'.format(reply.content, receiver))
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
image_storage = io.BytesIO()
|
||||
for block in pic_res.iter_content(1024):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
itchat.send_image(image_storage, toUserName=receiver)
|
||||
logger.info('[WX] sendImage url={}, receiver={}'.format(img_url,receiver))
|
||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||
image_storage = reply.content
|
||||
image_storage.seek(0)
|
||||
itchat.send_image(image_storage, toUserName=receiver)
|
||||
logger.info('[WX] sendImage, receiver={}'.format(receiver))
|
||||
except Exception as e:
|
||||
logger.error('[WX] sendMsg error: {}, receiver={}'.format(e, receiver))
|
||||
if retry_cnt < 2:
|
||||
time.sleep(3+3*retry_cnt)
|
||||
self.send(reply, receiver, retry_cnt + 1)
|
||||
|
||||
# 处理消息 TODO: 如果wechaty解耦,此处逻辑可以放置到父类
|
||||
def handle(self, context: Context):
|
||||
if context is None or not context.content:
|
||||
return
|
||||
logger.debug('[WX] ready to handle context: {}'.format(context))
|
||||
# reply的构建步骤
|
||||
reply = self._generate_reply(context)
|
||||
|
||||
logger.debug('[WX] ready to decorate reply: {}'.format(reply))
|
||||
# reply的包装步骤
|
||||
reply = self._decorate_reply(context, reply)
|
||||
|
||||
# reply的发送步骤
|
||||
self._send_reply(context, reply)
|
||||
|
||||
def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass():
|
||||
logger.debug('[WX] ready to handle context: type={}, content={}'.format(context.type, context.content))
|
||||
if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息
|
||||
reply = super().build_reply_content(context.content, context)
|
||||
elif context.type == ContextType.VOICE: # 语音消息
|
||||
msg = context['msg']
|
||||
mp3_path = TmpDir().path() + context.content
|
||||
msg.download(mp3_path)
|
||||
# mp3转wav
|
||||
wav_path = os.path.splitext(mp3_path)[0] + '.wav'
|
||||
try:
|
||||
mp3_to_wav(mp3_path=mp3_path, wav_path=wav_path)
|
||||
except Exception as e: # 转换失败,直接使用mp3,对于某些api,mp3也可以识别
|
||||
logger.warning("[WX]mp3 to wav error, use mp3 path. " + str(e))
|
||||
wav_path = mp3_path
|
||||
# 语音识别
|
||||
reply = super().build_voice_to_text(wav_path)
|
||||
# 删除临时文件
|
||||
try:
|
||||
os.remove(wav_path)
|
||||
os.remove(mp3_path)
|
||||
except Exception as e:
|
||||
logger.warning("[WX]delete temp file error: " + str(e))
|
||||
|
||||
if reply.type == ReplyType.TEXT:
|
||||
new_context = self._compose_context(
|
||||
ContextType.TEXT, reply.content, **context.kwargs)
|
||||
if new_context:
|
||||
reply = self._generate_reply(new_context)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
logger.error('[WX] unknown context type: {}'.format(context.type))
|
||||
return
|
||||
return reply
|
||||
|
||||
def _decorate_reply(self, context: Context, reply: Reply) -> Reply:
|
||||
if reply and reply.type:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
desire_rtype = context.get('desire_rtype')
|
||||
if not e_context.is_pass() and reply and reply.type:
|
||||
if reply.type == ReplyType.TEXT:
|
||||
reply_text = reply.content
|
||||
if desire_rtype == ReplyType.VOICE:
|
||||
reply = super().build_text_to_voice(reply.content)
|
||||
return self._decorate_reply(context, reply)
|
||||
if context['isgroup']:
|
||||
reply_text = '@' + context['msg']['ActualNickName'] + ' ' + reply_text.strip()
|
||||
reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
|
||||
else:
|
||||
reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
|
||||
reply.content = reply_text
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
reply.content = str(reply.type)+":\n" + reply.content
|
||||
elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE:
|
||||
pass
|
||||
else:
|
||||
logger.error('[WX] unknown reply type: {}'.format(reply.type))
|
||||
return
|
||||
if desire_rtype and desire_rtype != reply.type and reply.type not in [ReplyType.ERROR, ReplyType.INFO]:
|
||||
logger.warning('[WX] desire_rtype: {}, but reply type: {}'.format(context.get('desire_rtype'), reply.type))
|
||||
return reply
|
||||
|
||||
def _send_reply(self, context: Context, reply: Reply):
|
||||
if reply and reply.type:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass() and reply and reply.type:
|
||||
logger.debug('[WX] ready to send reply: {} to {}'.format(reply, context['receiver']))
|
||||
self.send(reply, context['receiver'])
|
||||
|
||||
|
||||
def check_prefix(self, content, prefix_list):
|
||||
for prefix in prefix_list:
|
||||
if content.startswith(prefix):
|
||||
return prefix
|
||||
return None
|
||||
|
||||
|
||||
def check_contain(self, content, keyword_list):
|
||||
if not keyword_list:
|
||||
return None
|
||||
for ky in keyword_list:
|
||||
if content.find(ky) != -1:
|
||||
return True
|
||||
def check_prefix(content, prefix_list):
|
||||
for prefix in prefix_list:
|
||||
if content.startswith(prefix):
|
||||
return prefix
|
||||
return None
|
||||
|
||||
def check_contain(content, keyword_list):
|
||||
if not keyword_list:
|
||||
return None
|
||||
for ky in keyword_list:
|
||||
if content.find(ky) != -1:
|
||||
return True
|
||||
return None
|
||||
|
||||
339
channel/wechat/wechaty_channel.py
Normal file
339
channel/wechat/wechaty_channel.py
Normal file
@@ -0,0 +1,339 @@
|
||||
# encoding:utf-8
|
||||
|
||||
"""
|
||||
wechaty channel
|
||||
Python Wechaty - https://github.com/wechaty/python-wechaty
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Optional, Union
|
||||
from bridge.context import Context, ContextType
|
||||
from wechaty_puppet import MessageType, FileBox, ScanStatus # type: ignore
|
||||
from wechaty import Wechaty, Contact
|
||||
from wechaty.user import Message, MiniProgram, UrlLink
|
||||
from channel.channel import Channel
|
||||
from common.log import logger
|
||||
from common.tmp_dir import TmpDir
|
||||
from config import conf
|
||||
from voice.audio_convert import sil_to_wav, mp3_to_sil
|
||||
|
||||
class WechatyChannel(Channel):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def startup(self):
|
||||
asyncio.run(self.main())
|
||||
|
||||
async def main(self):
|
||||
config = conf()
|
||||
# 使用PadLocal协议 比较稳定(免费web协议 os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:8080')
|
||||
token = config.get('wechaty_puppet_service_token')
|
||||
os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = token
|
||||
global bot
|
||||
bot = Wechaty()
|
||||
|
||||
bot.on('scan', self.on_scan)
|
||||
bot.on('login', self.on_login)
|
||||
bot.on('message', self.on_message)
|
||||
await bot.start()
|
||||
|
||||
async def on_login(self, contact: Contact):
|
||||
logger.info('[WX] login user={}'.format(contact))
|
||||
|
||||
async def on_scan(self, status: ScanStatus, qr_code: Optional[str] = None,
|
||||
data: Optional[str] = None):
|
||||
pass
|
||||
# contact = self.Contact.load(self.contact_id)
|
||||
# logger.info('[WX] scan user={}, scan status={}, scan qr_code={}'.format(contact, status.name, qr_code))
|
||||
# print(f'user <{contact}> scan status: {status.name} , 'f'qr_code: {qr_code}')
|
||||
|
||||
async def on_message(self, msg: Message):
|
||||
"""
|
||||
listen for message event
|
||||
"""
|
||||
from_contact = msg.talker() # 获取消息的发送者
|
||||
to_contact = msg.to() # 接收人
|
||||
room = msg.room() # 获取消息来自的群聊. 如果消息不是来自群聊, 则返回None
|
||||
from_user_id = from_contact.contact_id
|
||||
to_user_id = to_contact.contact_id # 接收人id
|
||||
# other_user_id = msg['User']['UserName'] # 对手方id
|
||||
content = msg.text()
|
||||
mention_content = await msg.mention_text() # 返回过滤掉@name后的消息
|
||||
match_prefix = self.check_prefix(content, conf().get('single_chat_prefix'))
|
||||
# conversation: Union[Room, Contact] = from_contact if room is None else room
|
||||
|
||||
if room is None and msg.type() == MessageType.MESSAGE_TYPE_TEXT:
|
||||
if not msg.is_self() and match_prefix is not None:
|
||||
# 好友向自己发送消息
|
||||
if match_prefix != '':
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
await self._do_send_img(content, from_user_id)
|
||||
else:
|
||||
await self._do_send(content, from_user_id)
|
||||
elif msg.is_self() and match_prefix:
|
||||
# 自己给好友发送消息
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
await self._do_send_img(content, to_user_id)
|
||||
else:
|
||||
await self._do_send(content, to_user_id)
|
||||
elif room is None and msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
|
||||
if not msg.is_self(): # 接收语音消息
|
||||
# 下载语音文件
|
||||
voice_file = await msg.to_file_box()
|
||||
silk_file = TmpDir().path() + voice_file.name
|
||||
await voice_file.to_file(silk_file)
|
||||
logger.info("[WX]receive voice file: " + silk_file)
|
||||
# 将文件转成wav格式音频
|
||||
wav_file = os.path.splitext(silk_file)[0] + '.wav'
|
||||
sil_to_wav(silk_file, wav_file)
|
||||
# 语音识别为文本
|
||||
query = super().build_voice_to_text(wav_file).content
|
||||
# 交验关键字
|
||||
match_prefix = self.check_prefix(query, conf().get('single_chat_prefix'))
|
||||
if match_prefix is not None:
|
||||
if match_prefix != '':
|
||||
str_list = query.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
query = str_list[1].strip()
|
||||
# 返回消息
|
||||
if conf().get('voice_reply_voice'):
|
||||
await self._do_send_voice(query, from_user_id)
|
||||
else:
|
||||
await self._do_send(query, from_user_id)
|
||||
else:
|
||||
logger.info("[WX]receive voice check prefix: " + 'False')
|
||||
# 清除缓存文件
|
||||
os.remove(wav_file)
|
||||
os.remove(silk_file)
|
||||
elif room and msg.type() == MessageType.MESSAGE_TYPE_TEXT:
|
||||
# 群组&文本消息
|
||||
room_id = room.room_id
|
||||
room_name = await room.topic()
|
||||
from_user_id = from_contact.contact_id
|
||||
from_user_name = from_contact.name
|
||||
is_at = await msg.mention_self()
|
||||
content = mention_content
|
||||
config = conf()
|
||||
match_prefix = (is_at and not config.get("group_at_off", False)) \
|
||||
or self.check_prefix(content, config.get('group_chat_prefix')) \
|
||||
or self.check_contain(content, config.get('group_chat_keyword'))
|
||||
# Wechaty判断is_at为True,返回的内容是过滤掉@之后的内容;而is_at为False,则会返回完整的内容
|
||||
# 故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
|
||||
prefixes = config.get('group_chat_prefix')
|
||||
for prefix in prefixes:
|
||||
if content.startswith(prefix):
|
||||
content = content.replace(prefix, '', 1).strip()
|
||||
break
|
||||
if ('ALL_GROUP' in config.get('group_name_white_list') or room_name in config.get(
|
||||
'group_name_white_list') or self.check_contain(room_name, config.get(
|
||||
'group_name_keyword_white_list'))) and match_prefix:
|
||||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
await self._do_send_group_img(content, room_id)
|
||||
else:
|
||||
await self._do_send_group(content, room_id, room_name, from_user_id, from_user_name)
|
||||
elif room and msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
|
||||
# 群组&语音消息
|
||||
room_id = room.room_id
|
||||
room_name = await room.topic()
|
||||
from_user_id = from_contact.contact_id
|
||||
from_user_name = from_contact.name
|
||||
is_at = await msg.mention_self()
|
||||
config = conf()
|
||||
# 是否开启语音识别、群消息响应功能、群名白名单符合等条件
|
||||
if config.get('group_speech_recognition') and (
|
||||
'ALL_GROUP' in config.get('group_name_white_list') or room_name in config.get(
|
||||
'group_name_white_list') or self.check_contain(room_name, config.get(
|
||||
'group_name_keyword_white_list'))):
|
||||
# 下载语音文件
|
||||
voice_file = await msg.to_file_box()
|
||||
silk_file = TmpDir().path() + voice_file.name
|
||||
await voice_file.to_file(silk_file)
|
||||
logger.info("[WX]receive voice file: " + silk_file)
|
||||
# 将文件转成wav格式音频
|
||||
wav_file = os.path.splitext(silk_file)[0] + '.wav'
|
||||
sil_to_wav(silk_file, wav_file)
|
||||
# 语音识别为文本
|
||||
query = super().build_voice_to_text(wav_file).content
|
||||
# 校验关键字
|
||||
match_prefix = self.check_prefix(query, config.get('group_chat_prefix')) \
|
||||
or self.check_contain(query, config.get('group_chat_keyword'))
|
||||
# Wechaty判断is_at为True,返回的内容是过滤掉@之后的内容;而is_at为False,则会返回完整的内容
|
||||
if match_prefix is not None:
|
||||
# 故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
|
||||
prefixes = config.get('group_chat_prefix')
|
||||
for prefix in prefixes:
|
||||
if query.startswith(prefix):
|
||||
query = query.replace(prefix, '', 1).strip()
|
||||
break
|
||||
# 返回消息
|
||||
img_match_prefix = self.check_prefix(query, conf().get('image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
query = query.split(img_match_prefix, 1)[1].strip()
|
||||
await self._do_send_group_img(query, room_id)
|
||||
elif config.get('voice_reply_voice'):
|
||||
await self._do_send_group_voice(query, room_id, room_name, from_user_id, from_user_name)
|
||||
else:
|
||||
await self._do_send_group(query, room_id, room_name, from_user_id, from_user_name)
|
||||
else:
|
||||
logger.info("[WX]receive voice check prefix: " + 'False')
|
||||
# 清除缓存文件
|
||||
os.remove(wav_file)
|
||||
os.remove(silk_file)
|
||||
|
||||
async def send(self, message: Union[str, Message, FileBox, Contact, UrlLink, MiniProgram], receiver):
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(message, receiver))
|
||||
if receiver:
|
||||
contact = await bot.Contact.find(receiver)
|
||||
await contact.say(message)
|
||||
|
||||
async def send_group(self, message: Union[str, Message, FileBox, Contact, UrlLink, MiniProgram], receiver):
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(message, receiver))
|
||||
if receiver:
|
||||
room = await bot.Room.find(receiver)
|
||||
await room.say(message)
|
||||
|
||||
async def _do_send(self, query, reply_user_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.TEXT, query)
|
||||
context['session_id'] = reply_user_id
|
||||
reply_text = super().build_reply_content(query, context).content
|
||||
if reply_text:
|
||||
await self.send(conf().get("single_chat_reply_prefix") + reply_text, reply_user_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
async def _do_send_voice(self, query, reply_user_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.TEXT, query)
|
||||
context['session_id'] = reply_user_id
|
||||
reply_text = super().build_reply_content(query, context).content
|
||||
if reply_text:
|
||||
# 转换 mp3 文件为 silk 格式
|
||||
mp3_file = super().build_text_to_voice(reply_text).content
|
||||
silk_file = os.path.splitext(mp3_file)[0] + '.sil'
|
||||
voiceLength = mp3_to_sil(mp3_file, silk_file)
|
||||
# 发送语音
|
||||
t = int(time.time())
|
||||
file_box = FileBox.from_file(silk_file, name=str(t) + '.sil')
|
||||
file_box.metadata = {'voiceLength': voiceLength}
|
||||
await self.send(file_box, reply_user_id)
|
||||
# 清除缓存文件
|
||||
os.remove(mp3_file)
|
||||
os.remove(silk_file)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
async def _do_send_img(self, query, reply_user_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.IMAGE_CREATE, query)
|
||||
img_url = super().build_reply_content(query, context).content
|
||||
if not img_url:
|
||||
return
|
||||
# 图片下载
|
||||
# pic_res = requests.get(img_url, stream=True)
|
||||
# image_storage = io.BytesIO()
|
||||
# for block in pic_res.iter_content(1024):
|
||||
# image_storage.write(block)
|
||||
# image_storage.seek(0)
|
||||
|
||||
# 图片发送
|
||||
logger.info('[WX] sendImage, receiver={}'.format(reply_user_id))
|
||||
t = int(time.time())
|
||||
file_box = FileBox.from_url(url=img_url, name=str(t) + '.png')
|
||||
await self.send(file_box, reply_user_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
async def _do_send_group(self, query, group_id, group_name, group_user_id, group_user_name):
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.TEXT, query)
|
||||
group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
|
||||
if ('ALL_GROUP' in group_chat_in_one_session or \
|
||||
group_name in group_chat_in_one_session or \
|
||||
self.check_contain(group_name, group_chat_in_one_session)):
|
||||
context['session_id'] = str(group_id)
|
||||
else:
|
||||
context['session_id'] = str(group_id) + '-' + str(group_user_id)
|
||||
reply_text = super().build_reply_content(query, context).content
|
||||
if reply_text:
|
||||
reply_text = '@' + group_user_name + ' ' + reply_text.strip()
|
||||
await self.send_group(conf().get("group_chat_reply_prefix", "") + reply_text, group_id)
|
||||
|
||||
async def _do_send_group_voice(self, query, group_id, group_name, group_user_id, group_user_name):
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.TEXT, query)
|
||||
group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
|
||||
if ('ALL_GROUP' in group_chat_in_one_session or \
|
||||
group_name in group_chat_in_one_session or \
|
||||
self.check_contain(group_name, group_chat_in_one_session)):
|
||||
context['session_id'] = str(group_id)
|
||||
else:
|
||||
context['session_id'] = str(group_id) + '-' + str(group_user_id)
|
||||
reply_text = super().build_reply_content(query, context).content
|
||||
if reply_text:
|
||||
reply_text = '@' + group_user_name + ' ' + reply_text.strip()
|
||||
# 转换 mp3 文件为 silk 格式
|
||||
mp3_file = super().build_text_to_voice(reply_text).content
|
||||
silk_file = os.path.splitext(mp3_file)[0] + '.sil'
|
||||
voiceLength = mp3_to_sil(mp3_file, silk_file)
|
||||
# 发送语音
|
||||
t = int(time.time())
|
||||
file_box = FileBox.from_file(silk_file, name=str(t) + '.silk')
|
||||
file_box.metadata = {'voiceLength': voiceLength}
|
||||
await self.send_group(file_box, group_id)
|
||||
# 清除缓存文件
|
||||
os.remove(mp3_file)
|
||||
os.remove(silk_file)
|
||||
|
||||
async def _do_send_group_img(self, query, reply_room_id):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = Context(ContextType.IMAGE_CREATE, query)
|
||||
img_url = super().build_reply_content(query, context).content
|
||||
if not img_url:
|
||||
return
|
||||
# 图片发送
|
||||
logger.info('[WX] sendImage, receiver={}'.format(reply_room_id))
|
||||
t = int(time.time())
|
||||
file_box = FileBox.from_url(url=img_url, name=str(t) + '.png')
|
||||
await self.send_group(file_box, reply_room_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def check_prefix(self, content, prefix_list):
|
||||
for prefix in prefix_list:
|
||||
if content.startswith(prefix):
|
||||
return prefix
|
||||
return None
|
||||
|
||||
def check_contain(self, content, keyword_list):
|
||||
if not keyword_list:
|
||||
return None
|
||||
for ky in keyword_list:
|
||||
if content.find(ky) != -1:
|
||||
return True
|
||||
return None
|
||||
5
common/const.py
Normal file
5
common/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# bot_type
|
||||
OPEN_AI = "openAI"
|
||||
CHATGPT = "chatGPT"
|
||||
BAIDU = "baidu"
|
||||
CHATGPTONAZURE = "chatGPTOnAzure"
|
||||
42
common/expired_dict.py
Normal file
42
common/expired_dict.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class ExpiredDict(dict):
|
||||
def __init__(self, expires_in_seconds):
|
||||
super().__init__()
|
||||
self.expires_in_seconds = expires_in_seconds
|
||||
|
||||
def __getitem__(self, key):
|
||||
value, expiry_time = super().__getitem__(key)
|
||||
if datetime.now() > expiry_time:
|
||||
del self[key]
|
||||
raise KeyError("expired {}".format(key))
|
||||
self.__setitem__(key, value)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
expiry_time = datetime.now() + timedelta(seconds=self.expires_in_seconds)
|
||||
super().__setitem__(key, (value, expiry_time))
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
self[key]
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def keys(self):
|
||||
keys = list(super().keys())
|
||||
return [key for key in keys if key in self]
|
||||
|
||||
def items(self):
|
||||
return [(key, self[key]) for key in self.keys()]
|
||||
|
||||
def __iter__(self):
|
||||
return self.keys().__iter__()
|
||||
9
common/singleton.py
Normal file
9
common/singleton.py
Normal file
@@ -0,0 +1,9 @@
|
||||
def singleton(cls):
|
||||
instances = {}
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
|
||||
return get_instance
|
||||
65
common/sorted_dict.py
Normal file
65
common/sorted_dict.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import heapq
|
||||
|
||||
|
||||
class SortedDict(dict):
|
||||
def __init__(self, sort_func=lambda k, v: k, init_dict=None, reverse=False):
|
||||
if init_dict is None:
|
||||
init_dict = []
|
||||
if isinstance(init_dict, dict):
|
||||
init_dict = init_dict.items()
|
||||
self.sort_func = sort_func
|
||||
self.sorted_keys = None
|
||||
self.reverse = reverse
|
||||
self.heap = []
|
||||
for k, v in init_dict:
|
||||
self[k] = v
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
super().__setitem__(key, value)
|
||||
for i, (priority, k) in enumerate(self.heap):
|
||||
if k == key:
|
||||
self.heap[i] = (self.sort_func(key, value), key)
|
||||
heapq.heapify(self.heap)
|
||||
break
|
||||
self.sorted_keys = None
|
||||
else:
|
||||
super().__setitem__(key, value)
|
||||
heapq.heappush(self.heap, (self.sort_func(key, value), key))
|
||||
self.sorted_keys = None
|
||||
|
||||
def __delitem__(self, key):
|
||||
super().__delitem__(key)
|
||||
for i, (priority, k) in enumerate(self.heap):
|
||||
if k == key:
|
||||
del self.heap[i]
|
||||
heapq.heapify(self.heap)
|
||||
break
|
||||
self.sorted_keys = None
|
||||
|
||||
def keys(self):
|
||||
if self.sorted_keys is None:
|
||||
self.sorted_keys = [k for _, k in sorted(self.heap, reverse=self.reverse)]
|
||||
return self.sorted_keys
|
||||
|
||||
def items(self):
|
||||
if self.sorted_keys is None:
|
||||
self.sorted_keys = [k for _, k in sorted(self.heap, reverse=self.reverse)]
|
||||
sorted_items = [(k, self[k]) for k in self.sorted_keys]
|
||||
return sorted_items
|
||||
|
||||
def _update_heap(self, key):
|
||||
for i, (priority, k) in enumerate(self.heap):
|
||||
if k == key:
|
||||
new_priority = self.sort_func(key, self[key])
|
||||
if new_priority != priority:
|
||||
self.heap[i] = (new_priority, key)
|
||||
heapq.heapify(self.heap)
|
||||
self.sorted_keys = None
|
||||
break
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.keys())
|
||||
|
||||
def __repr__(self):
|
||||
return f'{type(self).__name__}({dict(self)}, sort_func={self.sort_func.__name__}, reverse={self.reverse})'
|
||||
38
common/time_check.py
Normal file
38
common/time_check.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import time,re,hashlib
|
||||
import config
|
||||
from common.log import logger
|
||||
|
||||
def time_checker(f):
|
||||
def _time_checker(self, *args, **kwargs):
|
||||
_config = config.conf()
|
||||
chat_time_module = _config.get("chat_time_module", False)
|
||||
if chat_time_module:
|
||||
chat_start_time = _config.get("chat_start_time", "00:00")
|
||||
chat_stopt_time = _config.get("chat_stop_time", "24:00")
|
||||
time_regex = re.compile(r'^([01]?[0-9]|2[0-4])(:)([0-5][0-9])$') #时间匹配,包含24:00
|
||||
|
||||
starttime_format_check = time_regex.match(chat_start_time) # 检查停止时间格式
|
||||
stoptime_format_check = time_regex.match(chat_stopt_time) # 检查停止时间格式
|
||||
chat_time_check = chat_start_time < chat_stopt_time # 确定启动时间<停止时间
|
||||
|
||||
# 时间格式检查
|
||||
if not (starttime_format_check and stoptime_format_check and chat_time_check):
|
||||
logger.warn('时间格式不正确,请在config.json中修改您的CHAT_START_TIME/CHAT_STOP_TIME,否则可能会影响您正常使用,开始({})-结束({})'.format(starttime_format_check,stoptime_format_check))
|
||||
if chat_start_time>"23:59":
|
||||
logger.error('启动时间可能存在问题,请修改!')
|
||||
|
||||
# 服务时间检查
|
||||
now_time = time.strftime("%H:%M", time.localtime())
|
||||
if chat_start_time <= now_time <= chat_stopt_time: # 服务时间内,正常返回回答
|
||||
f(self, *args, **kwargs)
|
||||
return None
|
||||
else:
|
||||
if args[0]['Content'] == "#更新配置": # 不在服务时间内也可以更新配置
|
||||
f(self, *args, **kwargs)
|
||||
else:
|
||||
logger.info('非服务时间内,不接受访问')
|
||||
return None
|
||||
else:
|
||||
f(self, *args, **kwargs) # 未开启时间模块则直接回答
|
||||
return _time_checker
|
||||
|
||||
20
common/tmp_dir.py
Normal file
20
common/tmp_dir.py
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from config import conf
|
||||
|
||||
|
||||
class TmpDir(object):
|
||||
"""A temporary directory that is deleted when the object is destroyed.
|
||||
"""
|
||||
|
||||
tmpFilePath = pathlib.Path('./tmp/')
|
||||
|
||||
def __init__(self):
|
||||
pathExists = os.path.exists(self.tmpFilePath)
|
||||
if not pathExists and conf().get('speech_recognition') == True:
|
||||
os.makedirs(self.tmpFilePath)
|
||||
|
||||
def path(self):
|
||||
return str(self.tmpFilePath) + '/'
|
||||
|
||||
45
common/token_bucket.py
Normal file
45
common/token_bucket.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class TokenBucket:
|
||||
def __init__(self, tpm, timeout=None):
|
||||
self.capacity = int(tpm) # 令牌桶容量
|
||||
self.tokens = 0 # 初始令牌数为0
|
||||
self.rate = int(tpm) / 60 # 令牌每秒生成速率
|
||||
self.timeout = timeout # 等待令牌超时时间
|
||||
self.cond = threading.Condition() # 条件变量
|
||||
self.is_running = True
|
||||
# 开启令牌生成线程
|
||||
threading.Thread(target=self._generate_tokens).start()
|
||||
|
||||
def _generate_tokens(self):
|
||||
"""生成令牌"""
|
||||
while self.is_running:
|
||||
with self.cond:
|
||||
if self.tokens < self.capacity:
|
||||
self.tokens += 1
|
||||
self.cond.notify() # 通知获取令牌的线程
|
||||
time.sleep(1 / self.rate)
|
||||
|
||||
def get_token(self):
|
||||
"""获取令牌"""
|
||||
with self.cond:
|
||||
while self.tokens <= 0:
|
||||
flag = self.cond.wait(self.timeout)
|
||||
if not flag: # 超时
|
||||
return False
|
||||
self.tokens -= 1
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
self.is_running = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
token_bucket = TokenBucket(20, None) # 创建一个每分钟生产20个tokens的令牌桶
|
||||
# token_bucket = TokenBucket(20, 0.1)
|
||||
for i in range(3):
|
||||
if token_bucket.get_token():
|
||||
print(f"第{i+1}次请求成功")
|
||||
token_bucket.close()
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"open_ai_api_key": "YOUR API KEY",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"proxy": "",
|
||||
"use_azure_chatgpt": false,
|
||||
"single_chat_prefix": ["bot", "@bot"],
|
||||
"single_chat_reply_prefix": "[bot] ",
|
||||
"group_chat_prefix": ["@bot"],
|
||||
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"],
|
||||
"group_chat_in_one_session": ["ChatGPT测试群"],
|
||||
"image_create_prefix": ["画", "看", "找"],
|
||||
"speech_recognition": false,
|
||||
"group_speech_recognition": false,
|
||||
"voice_reply_voice": false,
|
||||
"conversation_max_tokens": 1000,
|
||||
"expires_in_seconds": 3600,
|
||||
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。"
|
||||
}
|
||||
}
|
||||
121
config.py
121
config.py
@@ -4,24 +4,133 @@ import json
|
||||
import os
|
||||
from common.log import logger
|
||||
|
||||
config = {}
|
||||
# 将所有可用的配置项写在字典里, 请使用小写字母
|
||||
available_setting = {
|
||||
# openai api配置
|
||||
"open_ai_api_key": "", # openai api key
|
||||
# openai apibase,当use_azure_chatgpt为true时,需要设置对应的api base
|
||||
"open_ai_api_base": "https://api.openai.com/v1",
|
||||
"proxy": "", # openai使用的代理
|
||||
# chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
|
||||
"model": "gpt-3.5-turbo",
|
||||
"use_azure_chatgpt": False, # 是否使用azure的chatgpt
|
||||
|
||||
# Bot触发配置
|
||||
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
|
||||
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
|
||||
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
|
||||
"group_chat_reply_prefix": "", # 群聊时自动回复的前缀
|
||||
"group_chat_keyword": [], # 群聊时包含该关键词则会触发机器人回复
|
||||
"group_at_off": False, # 是否关闭群聊时@bot的触发
|
||||
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
|
||||
"group_name_keyword_white_list": [], # 开启自动回复的群名称关键词列表
|
||||
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
|
||||
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
|
||||
|
||||
# chatgpt会话参数
|
||||
"expires_in_seconds": 3600, # 无操作会话的过期时间
|
||||
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
|
||||
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
|
||||
|
||||
# chatgpt限流配置
|
||||
"rate_limit_chatgpt": 20, # chatgpt的调用频率限制
|
||||
"rate_limit_dalle": 50, # openai dalle的调用频率限制
|
||||
|
||||
|
||||
# chatgpt api参数 参考https://platform.openai.com/docs/api-reference/chat/create
|
||||
"temperature": 0.9,
|
||||
"top_p": 1,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0,
|
||||
|
||||
# 语音设置
|
||||
"speech_recognition": False, # 是否开启语音识别
|
||||
"group_speech_recognition": False, # 是否开启群组语音识别
|
||||
"voice_reply_voice": False, # 是否使用语音回复语音,需要设置对应语音合成引擎的api key
|
||||
"voice_to_text": "openai", # 语音识别引擎,支持openai,google
|
||||
"text_to_voice": "baidu", # 语音合成引擎,支持baidu,google,pytts(offline)
|
||||
|
||||
# baidu api的配置, 使用百度语音识别和语音合成时需要
|
||||
"baidu_app_id": "",
|
||||
"baidu_api_key": "",
|
||||
"baidu_secret_key": "",
|
||||
# 1536普通话(支持简单的英文识别) 1737英语 1637粤语 1837四川话 1936普通话远场
|
||||
"baidu_dev_pid": "1536",
|
||||
|
||||
# 服务时间限制,目前支持itchat
|
||||
"chat_time_module": False, # 是否开启服务时间限制
|
||||
"chat_start_time": "00:00", # 服务开始时间
|
||||
"chat_stop_time": "24:00", # 服务结束时间
|
||||
|
||||
# itchat的配置
|
||||
"hot_reload": False, # 是否开启热重载
|
||||
|
||||
# wechaty的配置
|
||||
"wechaty_puppet_service_token": "", # wechaty的token
|
||||
|
||||
# chatgpt指令自定义触发词
|
||||
"clear_memory_commands": ['#清除记忆'], # 重置会话指令
|
||||
|
||||
# channel配置
|
||||
"channel_type": "wx", # 通道类型,支持wx,wxy和terminal
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __getitem__(self, key):
|
||||
if key not in available_setting:
|
||||
raise Exception("key {} not in available_setting".format(key))
|
||||
return super().__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in available_setting:
|
||||
raise Exception("key {} not in available_setting".format(key))
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError as e:
|
||||
return default
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
def load_config():
|
||||
global config
|
||||
config_path = "config.json"
|
||||
config_path = "./config.json"
|
||||
if not os.path.exists(config_path):
|
||||
raise Exception('配置文件不存在,请根据config-template.json模板创建config.json文件')
|
||||
logger.info('配置文件不存在,将使用config-template.json模板')
|
||||
config_path = "./config-template.json"
|
||||
|
||||
config_str = read_file(config_path)
|
||||
logger.debug("[INIT] config str: {}".format(config_str))
|
||||
|
||||
# 将json字符串反序列化为dict类型
|
||||
config = json.loads(config_str)
|
||||
config = Config(json.loads(config_str))
|
||||
|
||||
# override config with environment variables.
|
||||
# Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
|
||||
for name, value in os.environ.items():
|
||||
name = name.lower()
|
||||
if name in available_setting:
|
||||
logger.info(
|
||||
"[INIT] override config by environ args: {}={}".format(name, value))
|
||||
try:
|
||||
config[name] = eval(value)
|
||||
except:
|
||||
config[name] = value
|
||||
|
||||
logger.info("[INIT] load config: {}".format(config))
|
||||
|
||||
|
||||
|
||||
def get_root():
|
||||
return os.path.dirname(os.path.abspath( __file__ ))
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def read_file(path):
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
FROM python:3.7.9-alpine
|
||||
FROM python:3.10-alpine
|
||||
|
||||
LABEL maintainer="foo@bar.com"
|
||||
ARG TZ='Asia/Shanghai'
|
||||
|
||||
ARG CHATGPT_ON_WECHAT_VER=1.0.0
|
||||
ARG CHATGPT_ON_WECHAT_VER
|
||||
|
||||
ENV BUILD_PREFIX=/app \
|
||||
BUILD_OPEN_AI_API_KEY='YOUR OPEN AI KEY HERE'
|
||||
ENV BUILD_PREFIX=/app
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
wget \
|
||||
openssh
|
||||
|
||||
RUN wget -t 3 -T 30 -nv -O chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
https://github.com/zhayujie/chatgpt-on-wechat/archive/refs/tags/${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
&& tar -xzf chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
&& mv chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER} ${BUILD_PREFIX} \
|
||||
&& rm chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz
|
||||
&& export BUILD_GITHUB_TAG=${CHATGPT_ON_WECHAT_VER:-`curl -sL "https://api.github.com/repos/zhayujie/chatgpt-on-wechat/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"([^"]+)".*/\1/'`} \
|
||||
&& wget -t 3 -T 30 -nv -O chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
https://github.com/zhayujie/chatgpt-on-wechat/archive/refs/tags/${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& tar -xzf chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& mv chatgpt-on-wechat-${BUILD_GITHUB_TAG} ${BUILD_PREFIX} \
|
||||
&& rm chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& cd ${BUILD_PREFIX} \
|
||||
&& cp config-template.json ${BUILD_PREFIX}/config.json \
|
||||
&& /usr/local/bin/python -m pip install --no-cache --upgrade pip \
|
||||
&& pip install --no-cache -r requirements.txt \
|
||||
&& apk del curl wget
|
||||
|
||||
WORKDIR ${BUILD_PREFIX}
|
||||
|
||||
RUN cd ${BUILD_PREFIX} \
|
||||
&& cp config-template.json ${BUILD_PREFIX}/config.json \
|
||||
&& sed -i "2s/YOUR API KEY/${BUILD_OPEN_AI_API_KEY}/" ${BUILD_PREFIX}/config.json
|
||||
|
||||
RUN /usr/local/bin/python -m pip install --upgrade pip \
|
||||
&& pip install itchat-uos==1.5.0.dev0 \
|
||||
&& pip install --upgrade openai
|
||||
|
||||
ADD ./entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
RUN adduser -D -h /home/noroot -u 1000 -s /bin/bash noroot \
|
||||
&& chown noroot:noroot ${BUILD_PREFIX}
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& adduser -D -h /home/noroot -u 1000 -s /bin/bash noroot \
|
||||
&& chown -R noroot:noroot ${BUILD_PREFIX}
|
||||
|
||||
USER noroot
|
||||
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
FROM python:3.7.9
|
||||
FROM python:3.10
|
||||
|
||||
LABEL maintainer="foo@bar.com"
|
||||
ARG TZ='Asia/Shanghai'
|
||||
|
||||
ARG CHATGPT_ON_WECHAT_VER=1.0.0
|
||||
ARG CHATGPT_ON_WECHAT_VER
|
||||
|
||||
ENV BUILD_PREFIX=/app \
|
||||
BUILD_OPEN_AI_API_KEY='YOUR OPEN AI KEY HERE'
|
||||
ENV BUILD_PREFIX=/app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN wget -t 3 -T 30 -nv -O chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
https://github.com/zhayujie/chatgpt-on-wechat/archive/refs/tags/${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
&& tar -xzf chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz \
|
||||
&& mv chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER} ${BUILD_PREFIX} \
|
||||
&& rm chatgpt-on-wechat-${CHATGPT_ON_WECHAT_VER}.tar.gz
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& export BUILD_GITHUB_TAG=${CHATGPT_ON_WECHAT_VER:-`curl -sL "https://api.github.com/repos/zhayujie/chatgpt-on-wechat/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"([^"]+)".*/\1/'`} \
|
||||
&& wget -t 3 -T 30 -nv -O chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
https://github.com/zhayujie/chatgpt-on-wechat/archive/refs/tags/${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& tar -xzf chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& mv chatgpt-on-wechat-${BUILD_GITHUB_TAG} ${BUILD_PREFIX} \
|
||||
&& rm chatgpt-on-wechat-${BUILD_GITHUB_TAG}.tar.gz \
|
||||
&& cd ${BUILD_PREFIX} \
|
||||
&& cp config-template.json ${BUILD_PREFIX}/config.json \
|
||||
&& /usr/local/bin/python -m pip install --no-cache --upgrade pip \
|
||||
&& pip install --no-cache -r requirements.txt
|
||||
|
||||
WORKDIR ${BUILD_PREFIX}
|
||||
|
||||
RUN cd ${BUILD_PREFIX} \
|
||||
&& cp config-template.json ${BUILD_PREFIX}/config.json \
|
||||
&& sed -i "2s/YOUR API KEY/${BUILD_OPEN_AI_API_KEY}/" ${BUILD_PREFIX}/config.json
|
||||
|
||||
RUN /usr/local/bin/python -m pip install --upgrade pip \
|
||||
&& pip install itchat-uos==1.5.0.dev0 \
|
||||
&& pip install --upgrade openai
|
||||
|
||||
ADD ./entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
RUN groupadd -r noroot \
|
||||
&& useradd -r -g noroot -s /bin/bash -d /home/noroot noroot \
|
||||
&& chown -R noroot:noroot ${BUILD_PREFIX}
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& groupadd -r noroot \
|
||||
&& useradd -r -g noroot -s /bin/bash -d /home/noroot noroot \
|
||||
&& chown -R noroot:noroot ${BUILD_PREFIX}
|
||||
|
||||
USER noroot
|
||||
|
||||
|
||||
28
docker/Dockerfile.latest
Normal file
28
docker/Dockerfile.latest
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
LABEL maintainer="foo@bar.com"
|
||||
ARG TZ='Asia/Shanghai'
|
||||
|
||||
ARG CHATGPT_ON_WECHAT_VER
|
||||
|
||||
ENV BUILD_PREFIX=/app
|
||||
|
||||
ADD . ${BUILD_PREFIX}
|
||||
|
||||
RUN apk add --no-cache bash ffmpeg espeak \
|
||||
&& cd ${BUILD_PREFIX} \
|
||||
&& cp config-template.json config.json \
|
||||
&& /usr/local/bin/python -m pip install --no-cache --upgrade pip \
|
||||
&& pip install --no-cache -r requirements.txt
|
||||
|
||||
WORKDIR ${BUILD_PREFIX}
|
||||
|
||||
ADD docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& adduser -D -h /home/noroot -u 1000 -s /bin/bash noroot \
|
||||
&& chown -R noroot:noroot ${BUILD_PREFIX}
|
||||
|
||||
USER noroot
|
||||
|
||||
ENTRYPOINT ["docker/entrypoint.sh"]
|
||||
@@ -1,5 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# fetch latest release tag
|
||||
CHATGPT_ON_WECHAT_TAG=`curl -sL "https://api.github.com/repos/zhayujie/chatgpt-on-wechat/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"([^"]+)".*/\1/'`
|
||||
|
||||
# build image
|
||||
docker build -f Dockerfile.alpine \
|
||||
--build-arg CHATGPT_ON_WECHAT_VER=1.0.0\
|
||||
-t zhayujie/chatgpt-on-wechat:1.0.0-alpine .
|
||||
--build-arg CHATGPT_ON_WECHAT_VER=$CHATGPT_ON_WECHAT_TAG \
|
||||
-t zhayujie/chatgpt-on-wechat .
|
||||
|
||||
# tag image
|
||||
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:alpine
|
||||
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:$CHATGPT_ON_WECHAT_TAG-alpine
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# fetch latest release tag
|
||||
CHATGPT_ON_WECHAT_TAG=`curl -sL "https://api.github.com/repos/zhayujie/chatgpt-on-wechat/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"([^"]+)".*/\1/'`
|
||||
|
||||
# build image
|
||||
docker build -f Dockerfile.debian \
|
||||
--build-arg CHATGPT_ON_WECHAT_VER=1.0.0\
|
||||
-t zhayujie/chatgpt-on-wechat:1.0.0-debian .
|
||||
--build-arg CHATGPT_ON_WECHAT_VER=$CHATGPT_ON_WECHAT_TAG \
|
||||
-t zhayujie/chatgpt-on-wechat .
|
||||
|
||||
# tag image
|
||||
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:debian
|
||||
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:$CHATGPT_ON_WECHAT_TAG-debian
|
||||
4
docker/build.latest.sh
Normal file
4
docker/build.latest.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd .. && docker build -f Dockerfile \
|
||||
-t zhayujie/chatgpt-on-wechat .
|
||||
23
docker/chatgpt-on-wechat-voice-reply/Dockerfile.alpine
Normal file
23
docker/chatgpt-on-wechat-voice-reply/Dockerfile.alpine
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM zhayujie/chatgpt-on-wechat:alpine
|
||||
|
||||
LABEL maintainer="foo@bar.com"
|
||||
ARG TZ='Asia/Shanghai'
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ffmpeg \
|
||||
espeak \
|
||||
&& pip install --no-cache \
|
||||
baidu-aip \
|
||||
chardet \
|
||||
SpeechRecognition
|
||||
|
||||
# replace entrypoint
|
||||
ADD ./entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
USER noroot
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
24
docker/chatgpt-on-wechat-voice-reply/Dockerfile.debian
Normal file
24
docker/chatgpt-on-wechat-voice-reply/Dockerfile.debian
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM zhayujie/chatgpt-on-wechat:debian
|
||||
|
||||
LABEL maintainer="foo@bar.com"
|
||||
ARG TZ='Asia/Shanghai'
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
espeak \
|
||||
&& pip install --no-cache \
|
||||
baidu-aip \
|
||||
chardet \
|
||||
SpeechRecognition
|
||||
|
||||
# replace entrypoint
|
||||
ADD ./entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
USER noroot
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
24
docker/chatgpt-on-wechat-voice-reply/docker-compose.yaml
Normal file
24
docker/chatgpt-on-wechat-voice-reply/docker-compose.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: '2.0'
|
||||
services:
|
||||
chatgpt-on-wechat:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile.alpine
|
||||
image: zhayujie/chatgpt-on-wechat-voice-reply
|
||||
container_name: chatgpt-on-wechat-voice-reply
|
||||
environment:
|
||||
OPEN_AI_API_KEY: 'YOUR API KEY'
|
||||
OPEN_AI_PROXY: ''
|
||||
SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
|
||||
SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
|
||||
GROUP_CHAT_PREFIX: '["@bot"]'
|
||||
GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
|
||||
IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
|
||||
CONVERSATION_MAX_TOKENS: 1000
|
||||
SPEECH_RECOGNITION: 'true'
|
||||
CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
|
||||
EXPIRES_IN_SECONDS: 3600
|
||||
VOICE_REPLY_VOICE: 'true'
|
||||
BAIDU_APP_ID: 'YOUR BAIDU APP ID'
|
||||
BAIDU_API_KEY: 'YOUR BAIDU API KEY'
|
||||
BAIDU_SECRET_KEY: 'YOUR BAIDU SERVICE KEY'
|
||||
117
docker/chatgpt-on-wechat-voice-reply/entrypoint.sh
Executable file
117
docker/chatgpt-on-wechat-voice-reply/entrypoint.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# build prefix
|
||||
CHATGPT_ON_WECHAT_PREFIX=${CHATGPT_ON_WECHAT_PREFIX:-""}
|
||||
# path to config.json
|
||||
CHATGPT_ON_WECHAT_CONFIG_PATH=${CHATGPT_ON_WECHAT_CONFIG_PATH:-""}
|
||||
# execution command line
|
||||
CHATGPT_ON_WECHAT_EXEC=${CHATGPT_ON_WECHAT_EXEC:-""}
|
||||
|
||||
OPEN_AI_API_KEY=${OPEN_AI_API_KEY:-""}
|
||||
OPEN_AI_PROXY=${OPEN_AI_PROXY:-""}
|
||||
SINGLE_CHAT_PREFIX=${SINGLE_CHAT_PREFIX:-""}
|
||||
SINGLE_CHAT_REPLY_PREFIX=${SINGLE_CHAT_REPLY_PREFIX:-""}
|
||||
GROUP_CHAT_PREFIX=${GROUP_CHAT_PREFIX:-""}
|
||||
GROUP_NAME_WHITE_LIST=${GROUP_NAME_WHITE_LIST:-""}
|
||||
IMAGE_CREATE_PREFIX=${IMAGE_CREATE_PREFIX:-""}
|
||||
CONVERSATION_MAX_TOKENS=${CONVERSATION_MAX_TOKENS:-""}
|
||||
SPEECH_RECOGNITION=${SPEECH_RECOGNITION:-""}
|
||||
CHARACTER_DESC=${CHARACTER_DESC:-""}
|
||||
EXPIRES_IN_SECONDS=${EXPIRES_IN_SECONDS:-""}
|
||||
|
||||
VOICE_REPLY_VOICE=${VOICE_REPLY_VOICE:-""}
|
||||
BAIDU_APP_ID=${BAIDU_APP_ID:-""}
|
||||
BAIDU_API_KEY=${BAIDU_API_KEY:-""}
|
||||
BAIDU_SECRET_KEY=${BAIDU_SECRET_KEY:-""}
|
||||
|
||||
# CHATGPT_ON_WECHAT_PREFIX is empty, use /app
|
||||
if [ "$CHATGPT_ON_WECHAT_PREFIX" == "" ] ; then
|
||||
CHATGPT_ON_WECHAT_PREFIX=/app
|
||||
fi
|
||||
|
||||
# CHATGPT_ON_WECHAT_CONFIG_PATH is empty, use '/app/config.json'
|
||||
if [ "$CHATGPT_ON_WECHAT_CONFIG_PATH" == "" ] ; then
|
||||
CHATGPT_ON_WECHAT_CONFIG_PATH=$CHATGPT_ON_WECHAT_PREFIX/config.json
|
||||
fi
|
||||
|
||||
# CHATGPT_ON_WECHAT_EXEC is empty, use ‘python app.py’
|
||||
if [ "$CHATGPT_ON_WECHAT_EXEC" == "" ] ; then
|
||||
CHATGPT_ON_WECHAT_EXEC="python app.py"
|
||||
fi
|
||||
|
||||
# modify content in config.json
|
||||
if [ "$OPEN_AI_API_KEY" != "" ] ; then
|
||||
sed -i "s/\"open_ai_api_key\".*,$/\"open_ai_api_key\": \"$OPEN_AI_API_KEY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
else
|
||||
echo -e "\033[31m[Warning] You need to set OPEN_AI_API_KEY before running!\033[0m"
|
||||
fi
|
||||
|
||||
# use http_proxy as default
|
||||
if [ "$HTTP_PROXY" != "" ] ; then
|
||||
sed -i "s/\"proxy\".*,$/\"proxy\": \"$HTTP_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$OPEN_AI_PROXY" != "" ] ; then
|
||||
sed -i "s/\"proxy\".*,$/\"proxy\": \"$OPEN_AI_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$SINGLE_CHAT_PREFIX" != "" ] ; then
|
||||
sed -i "s/\"single_chat_prefix\".*,$/\"single_chat_prefix\": $SINGLE_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$SINGLE_CHAT_REPLY_PREFIX" != "" ] ; then
|
||||
sed -i "s/\"single_chat_reply_prefix\".*,$/\"single_chat_reply_prefix\": $SINGLE_CHAT_REPLY_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$GROUP_CHAT_PREFIX" != "" ] ; then
|
||||
sed -i "s/\"group_chat_prefix\".*,$/\"group_chat_prefix\": $GROUP_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$GROUP_NAME_WHITE_LIST" != "" ] ; then
|
||||
sed -i "s/\"group_name_white_list\".*,$/\"group_name_white_list\": $GROUP_NAME_WHITE_LIST,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$IMAGE_CREATE_PREFIX" != "" ] ; then
|
||||
sed -i "s/\"image_create_prefix\".*,$/\"image_create_prefix\": $IMAGE_CREATE_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$CONVERSATION_MAX_TOKENS" != "" ] ; then
|
||||
sed -i "s/\"conversation_max_tokens\".*,$/\"conversation_max_tokens\": $CONVERSATION_MAX_TOKENS,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$SPEECH_RECOGNITION" != "" ] ; then
|
||||
sed -i "s/\"speech_recognition\".*,$/\"speech_recognition\": $SPEECH_RECOGNITION,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$CHARACTER_DESC" != "" ] ; then
|
||||
sed -i "s/\"character_desc\".*,$/\"character_desc\": \"$CHARACTER_DESC\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$EXPIRES_IN_SECONDS" != "" ] ; then
|
||||
sed -i "s/\"expires_in_seconds\".*$/\"expires_in_seconds\": $EXPIRES_IN_SECONDS/" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
# append
|
||||
if [ "$BAIDU_SECRET_KEY" != "" ] ; then
|
||||
sed -i "1a \ \ \"baidu_secret_key\": \"$BAIDU_SECRET_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$BAIDU_API_KEY" != "" ] ; then
|
||||
sed -i "1a \ \ \"baidu_api_key\": \"$BAIDU_API_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$BAIDU_APP_ID" != "" ] ; then
|
||||
sed -i "1a \ \ \"baidu_app_id\": \"$BAIDU_APP_ID\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$VOICE_REPLY_VOICE" != "" ] ; then
|
||||
sed -i "1a \ \ \"voice_reply_voice\": $VOICE_REPLY_VOICE," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
# go to prefix dir
|
||||
cd $CHATGPT_ON_WECHAT_PREFIX
|
||||
# excute
|
||||
$CHATGPT_ON_WECHAT_EXEC
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ services:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile.alpine
|
||||
image: zhayujie/chatgpt-on-wechat:1.0.0-alpine
|
||||
image: zhayujie/chatgpt-on-wechat
|
||||
container_name: sample-chatgpt-on-wechat
|
||||
environment:
|
||||
OPEN_AI_API_KEY: 'YOUR API KEY'
|
||||
SINGLE_CHAT_PREFIX: '["BOT", "@BOT"]'
|
||||
SINGLE_CHAT_REPLY_PREFIX: '"[BOT] "'
|
||||
GROUP_CHAT_PREFIX: '["@BOT"]'
|
||||
GROUP_NAME_WHITE_LIST: '["CHATGPT测试群", "CHATGPT测试群2"]'
|
||||
OPEN_AI_PROXY: ''
|
||||
SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
|
||||
SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
|
||||
GROUP_CHAT_PREFIX: '["@bot"]'
|
||||
GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
|
||||
IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
|
||||
CONVERSATION_MAX_TOKENS: 1000
|
||||
CHARACTER_DESC: '你是CHATGPT, 一个由OPENAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
|
||||
SPEECH_RECOGNITION: "False"
|
||||
CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
|
||||
EXPIRES_IN_SECONDS: 3600
|
||||
@@ -8,21 +8,26 @@ CHATGPT_ON_WECHAT_CONFIG_PATH=${CHATGPT_ON_WECHAT_CONFIG_PATH:-""}
|
||||
# execution command line
|
||||
CHATGPT_ON_WECHAT_EXEC=${CHATGPT_ON_WECHAT_EXEC:-""}
|
||||
|
||||
OPEN_AI_API_KEY=${OPEN_AI_API_KEY:-""}
|
||||
SINGLE_CHAT_PREFIX=${SINGLE_CHAT_PREFIX:-""}
|
||||
SINGLE_CHAT_REPLY_PREFIX=${SINGLE_CHAT_REPLY_PREFIX:-""}
|
||||
GROUP_CHAT_PREFIX=${GROUP_CHAT_PREFIX:-""}
|
||||
GROUP_NAME_WHITE_LIST=${GROUP_NAME_WHITE_LIST:-""}
|
||||
IMAGE_CREATE_PREFIX=${IMAGE_CREATE_PREFIX:-""}
|
||||
CONVERSATION_MAX_TOKENS=${CONVERSATION_MAX_TOKENS:-""}
|
||||
CHARACTER_DESC=${CHARACTER_DESC:-""}
|
||||
# use environment variables to pass parameters
|
||||
# if you have not defined environment variables, set them below
|
||||
# export OPEN_AI_API_KEY=${OPEN_AI_API_KEY:-'YOUR API KEY'}
|
||||
# export OPEN_AI_PROXY=${OPEN_AI_PROXY:-""}
|
||||
# export SINGLE_CHAT_PREFIX=${SINGLE_CHAT_PREFIX:-'["bot", "@bot"]'}
|
||||
# export SINGLE_CHAT_REPLY_PREFIX=${SINGLE_CHAT_REPLY_PREFIX:-'"[bot] "'}
|
||||
# export GROUP_CHAT_PREFIX=${GROUP_CHAT_PREFIX:-'["@bot"]'}
|
||||
# export GROUP_NAME_WHITE_LIST=${GROUP_NAME_WHITE_LIST:-'["ChatGPT测试群", "ChatGPT测试群2"]'}
|
||||
# export IMAGE_CREATE_PREFIX=${IMAGE_CREATE_PREFIX:-'["画", "看", "找"]'}
|
||||
# export CONVERSATION_MAX_TOKENS=${CONVERSATION_MAX_TOKENS:-"1000"}
|
||||
# export SPEECH_RECOGNITION=${SPEECH_RECOGNITION:-"False"}
|
||||
# export CHARACTER_DESC=${CHARACTER_DESC:-"你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。"}
|
||||
# export EXPIRES_IN_SECONDS=${EXPIRES_IN_SECONDS:-"3600"}
|
||||
|
||||
# CHATGPT_ON_WECHAT_PREFIX is empty, use /app
|
||||
if [ "$CHATGPT_ON_WECHAT_PREFIX" == "" ] ; then
|
||||
CHATGPT_ON_WECHAT_PREFIX=/app
|
||||
fi
|
||||
|
||||
# APP_PREFIX is empty, use '/app/config.json'
|
||||
# CHATGPT_ON_WECHAT_CONFIG_PATH is empty, use '/app/config.json'
|
||||
if [ "$CHATGPT_ON_WECHAT_CONFIG_PATH" == "" ] ; then
|
||||
CHATGPT_ON_WECHAT_CONFIG_PATH=$CHATGPT_ON_WECHAT_PREFIX/config.json
|
||||
fi
|
||||
@@ -33,39 +38,10 @@ if [ "$CHATGPT_ON_WECHAT_EXEC" == "" ] ; then
|
||||
fi
|
||||
|
||||
# modify content in config.json
|
||||
if [ "$OPEN_AI_API_KEY" != "" ] ; then
|
||||
sed -i "2c \"open_ai_api_key\": \"$OPEN_AI_API_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
else
|
||||
if [ "$OPEN_AI_API_KEY" == "YOUR API KEY" ] || [ "$OPEN_AI_API_KEY" == "" ]; then
|
||||
echo -e "\033[31m[Warning] You need to set OPEN_AI_API_KEY before running!\033[0m"
|
||||
fi
|
||||
|
||||
if [ "$SINGLE_CHAT_PREFIX" != "" ] ; then
|
||||
sed -i "3c \"single_chat_prefix\": $SINGLE_CHAT_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$SINGLE_CHAT_REPLY_PREFIX" != "" ] ; then
|
||||
sed -i "4c \"single_chat_reply_prefix\": $SINGLE_CHAT_REPLY_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$GROUP_CHAT_PREFIX" != "" ] ; then
|
||||
sed -i "5c \"group_chat_prefix\": $GROUP_CHAT_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$GROUP_NAME_WHITE_LIST" != "" ] ; then
|
||||
sed -i "6c \"group_name_white_list\": $GROUP_NAME_WHITE_LIST," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$IMAGE_CREATE_PREFIX" != "" ] ; then
|
||||
sed -i "7c \"image_create_prefix\": $IMAGE_CREATE_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$CONVERSATION_MAX_TOKENS" != "" ] ; then
|
||||
sed -i "8c \"conversation_max_tokens\": $CONVERSATION_MAX_TOKENS," $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
if [ "$CHARACTER_DESC" != "" ] ; then
|
||||
sed -i "9c \"character_desc\": \"$CHARACTER_DESC\"" $CHATGPT_ON_WECHAT_CONFIG_PATH
|
||||
fi
|
||||
|
||||
# go to prefix dir
|
||||
cd $CHATGPT_ON_WECHAT_PREFIX
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
OPEN_AI_API_KEY=YOUR API KEY
|
||||
SINGLE_CHAT_PREFIX=["BOT", "@BOT"]
|
||||
SINGLE_CHAT_REPLY_PREFIX="[BOT] "
|
||||
GROUP_CHAT_PREFIX=["@BOT"]
|
||||
GROUP_NAME_WHITE_LIST=["CHATGPT测试群", "CHATGPT测试群2"]
|
||||
OPEN_AI_PROXY=
|
||||
SINGLE_CHAT_PREFIX=["bot", "@bot"]
|
||||
SINGLE_CHAT_REPLY_PREFIX="[bot] "
|
||||
GROUP_CHAT_PREFIX=["@bot"]
|
||||
GROUP_NAME_WHITE_LIST=["ChatGPT测试群", "ChatGPT测试群2"]
|
||||
IMAGE_CREATE_PREFIX=["画", "看", "找"]
|
||||
CONVERSATION_MAX_TOKENS=1000
|
||||
CHARACTER_DESC=你是CHATGPT, 一个由OPENAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。
|
||||
SPEECH_RECOGNITION=false
|
||||
CHARACTER_DESC=你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。
|
||||
EXPIRES_IN_SECONDS=3600
|
||||
|
||||
# Optional
|
||||
#CHATGPT_ON_WECHAT_PREFIX=/app
|
||||
|
||||
@@ -1 +1 @@
|
||||
zhayujie/chatgpt-on-wechat:1.0.0-alpine
|
||||
zhayujie/chatgpt-on-wechat
|
||||
|
||||
96
lib/itchat/__init__.py
Normal file
96
lib/itchat/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from .core import Core
|
||||
from .config import VERSION, ASYNC_COMPONENTS
|
||||
from .log import set_logging
|
||||
|
||||
if ASYNC_COMPONENTS:
|
||||
from .async_components import load_components
|
||||
else:
|
||||
from .components import load_components
|
||||
|
||||
|
||||
__version__ = VERSION
|
||||
|
||||
|
||||
instanceList = []
|
||||
|
||||
def load_async_itchat() -> Core:
|
||||
"""load async-based itchat instance
|
||||
|
||||
Returns:
|
||||
Core: the abstract interface of itchat
|
||||
"""
|
||||
from .async_components import load_components
|
||||
load_components(Core)
|
||||
return Core()
|
||||
|
||||
|
||||
def load_sync_itchat() -> Core:
|
||||
"""load sync-based itchat instance
|
||||
|
||||
Returns:
|
||||
Core: the abstract interface of itchat
|
||||
"""
|
||||
from .components import load_components
|
||||
load_components(Core)
|
||||
return Core()
|
||||
|
||||
|
||||
if ASYNC_COMPONENTS:
|
||||
instance = load_async_itchat()
|
||||
else:
|
||||
instance = load_sync_itchat()
|
||||
|
||||
|
||||
instanceList = [instance]
|
||||
|
||||
# I really want to use sys.modules[__name__] = originInstance
|
||||
# but it makes auto-fill a real mess, so forgive me for my following **
|
||||
# actually it toke me less than 30 seconds, god bless Uganda
|
||||
|
||||
# components.login
|
||||
login = instance.login
|
||||
get_QRuuid = instance.get_QRuuid
|
||||
get_QR = instance.get_QR
|
||||
check_login = instance.check_login
|
||||
web_init = instance.web_init
|
||||
show_mobile_login = instance.show_mobile_login
|
||||
start_receiving = instance.start_receiving
|
||||
get_msg = instance.get_msg
|
||||
logout = instance.logout
|
||||
# components.contact
|
||||
update_chatroom = instance.update_chatroom
|
||||
update_friend = instance.update_friend
|
||||
get_contact = instance.get_contact
|
||||
get_friends = instance.get_friends
|
||||
get_chatrooms = instance.get_chatrooms
|
||||
get_mps = instance.get_mps
|
||||
set_alias = instance.set_alias
|
||||
set_pinned = instance.set_pinned
|
||||
accept_friend = instance.accept_friend
|
||||
get_head_img = instance.get_head_img
|
||||
create_chatroom = instance.create_chatroom
|
||||
set_chatroom_name = instance.set_chatroom_name
|
||||
delete_member_from_chatroom = instance.delete_member_from_chatroom
|
||||
add_member_into_chatroom = instance.add_member_into_chatroom
|
||||
# components.messages
|
||||
send_raw_msg = instance.send_raw_msg
|
||||
send_msg = instance.send_msg
|
||||
upload_file = instance.upload_file
|
||||
send_file = instance.send_file
|
||||
send_image = instance.send_image
|
||||
send_video = instance.send_video
|
||||
send = instance.send
|
||||
revoke = instance.revoke
|
||||
# components.hotreload
|
||||
dump_login_status = instance.dump_login_status
|
||||
load_login_status = instance.load_login_status
|
||||
# components.register
|
||||
auto_login = instance.auto_login
|
||||
configured_reply = instance.configured_reply
|
||||
msg_register = instance.msg_register
|
||||
run = instance.run
|
||||
# other functions
|
||||
search_friends = instance.search_friends
|
||||
search_chatrooms = instance.search_chatrooms
|
||||
search_mps = instance.search_mps
|
||||
set_logging = set_logging
|
||||
12
lib/itchat/async_components/__init__.py
Normal file
12
lib/itchat/async_components/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .contact import load_contact
|
||||
from .hotreload import load_hotreload
|
||||
from .login import load_login
|
||||
from .messages import load_messages
|
||||
from .register import load_register
|
||||
|
||||
def load_components(core):
|
||||
load_contact(core)
|
||||
load_hotreload(core)
|
||||
load_login(core)
|
||||
load_messages(core)
|
||||
load_register(core)
|
||||
488
lib/itchat/async_components/contact.py
Normal file
488
lib/itchat/async_components/contact.py
Normal file
@@ -0,0 +1,488 @@
|
||||
import time, re, io
|
||||
import json, copy
|
||||
import logging
|
||||
|
||||
from .. import config, utils
|
||||
from ..components.contact import accept_friend
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import contact_change
|
||||
from ..utils import update_info_dict
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_contact(core):
|
||||
core.update_chatroom = update_chatroom
|
||||
core.update_friend = update_friend
|
||||
core.get_contact = get_contact
|
||||
core.get_friends = get_friends
|
||||
core.get_chatrooms = get_chatrooms
|
||||
core.get_mps = get_mps
|
||||
core.set_alias = set_alias
|
||||
core.set_pinned = set_pinned
|
||||
core.accept_friend = accept_friend
|
||||
core.get_head_img = get_head_img
|
||||
core.create_chatroom = create_chatroom
|
||||
core.set_chatroom_name = set_chatroom_name
|
||||
core.delete_member_from_chatroom = delete_member_from_chatroom
|
||||
core.add_member_into_chatroom = add_member_into_chatroom
|
||||
|
||||
def update_chatroom(self, userName, detailedMember=False):
|
||||
if not isinstance(userName, list):
|
||||
userName = [userName]
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(userName),
|
||||
'List': [{
|
||||
'UserName': u,
|
||||
'ChatRoomId': '', } for u in userName], }
|
||||
chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace')).get('ContactList')
|
||||
if not chatroomList:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No chatroom found',
|
||||
'Ret': -1001, }})
|
||||
|
||||
if detailedMember:
|
||||
def get_detailed_member_info(encryChatroomId, memberList):
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT, }
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(memberList),
|
||||
'List': [{
|
||||
'UserName': member['UserName'],
|
||||
'EncryChatRoomId': encryChatroomId} \
|
||||
for member in memberList], }
|
||||
return json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace'))['ContactList']
|
||||
MAX_GET_NUMBER = 50
|
||||
for chatroom in chatroomList:
|
||||
totalMemberList = []
|
||||
for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)):
|
||||
memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER]
|
||||
totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList)
|
||||
chatroom['MemberList'] = totalMemberList
|
||||
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
r = [self.storageClass.search_chatrooms(userName=c['UserName'])
|
||||
for c in chatroomList]
|
||||
return r if 1 < len(r) else r[0]
|
||||
|
||||
def update_friend(self, userName):
|
||||
if not isinstance(userName, list):
|
||||
userName = [userName]
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(userName),
|
||||
'List': [{
|
||||
'UserName': u,
|
||||
'EncryChatRoomId': '', } for u in userName], }
|
||||
friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace')).get('ContactList')
|
||||
|
||||
update_local_friends(self, friendList)
|
||||
r = [self.storageClass.search_friends(userName=f['UserName'])
|
||||
for f in friendList]
|
||||
return r if len(r) != 1 else r[0]
|
||||
|
||||
@contact_change
|
||||
def update_local_chatrooms(core, l):
|
||||
'''
|
||||
get a list of chatrooms for updating local chatrooms
|
||||
return a list of given chatrooms with updated info
|
||||
'''
|
||||
for chatroom in l:
|
||||
# format new chatrooms
|
||||
utils.emoji_formatter(chatroom, 'NickName')
|
||||
for member in chatroom['MemberList']:
|
||||
if 'NickName' in member:
|
||||
utils.emoji_formatter(member, 'NickName')
|
||||
if 'DisplayName' in member:
|
||||
utils.emoji_formatter(member, 'DisplayName')
|
||||
if 'RemarkName' in member:
|
||||
utils.emoji_formatter(member, 'RemarkName')
|
||||
# update it to old chatrooms
|
||||
oldChatroom = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||
if oldChatroom:
|
||||
update_info_dict(oldChatroom, chatroom)
|
||||
# - update other values
|
||||
memberList = chatroom.get('MemberList', [])
|
||||
oldMemberList = oldChatroom['MemberList']
|
||||
if memberList:
|
||||
for member in memberList:
|
||||
oldMember = utils.search_dict_list(
|
||||
oldMemberList, 'UserName', member['UserName'])
|
||||
if oldMember:
|
||||
update_info_dict(oldMember, member)
|
||||
else:
|
||||
oldMemberList.append(member)
|
||||
else:
|
||||
core.chatroomList.append(chatroom)
|
||||
oldChatroom = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||
# delete useless members
|
||||
if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \
|
||||
chatroom['MemberList']:
|
||||
existsUserNames = [member['UserName'] for member in chatroom['MemberList']]
|
||||
delList = []
|
||||
for i, member in enumerate(oldChatroom['MemberList']):
|
||||
if member['UserName'] not in existsUserNames:
|
||||
delList.append(i)
|
||||
delList.sort(reverse=True)
|
||||
for i in delList:
|
||||
del oldChatroom['MemberList'][i]
|
||||
# - update OwnerUin
|
||||
if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'):
|
||||
owner = utils.search_dict_list(oldChatroom['MemberList'],
|
||||
'UserName', oldChatroom['ChatRoomOwner'])
|
||||
oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0)
|
||||
# - update IsAdmin
|
||||
if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0:
|
||||
oldChatroom['IsAdmin'] = \
|
||||
oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin'])
|
||||
else:
|
||||
oldChatroom['IsAdmin'] = None
|
||||
# - update Self
|
||||
newSelf = utils.search_dict_list(oldChatroom['MemberList'],
|
||||
'UserName', core.storageClass.userName)
|
||||
oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User'])
|
||||
return {
|
||||
'Type' : 'System',
|
||||
'Text' : [chatroom['UserName'] for chatroom in l],
|
||||
'SystemInfo' : 'chatrooms',
|
||||
'FromUserName' : core.storageClass.userName,
|
||||
'ToUserName' : core.storageClass.userName, }
|
||||
|
||||
@contact_change
|
||||
def update_local_friends(core, l):
|
||||
'''
|
||||
get a list of friends or mps for updating local contact
|
||||
'''
|
||||
fullList = core.memberList + core.mpList
|
||||
for friend in l:
|
||||
if 'NickName' in friend:
|
||||
utils.emoji_formatter(friend, 'NickName')
|
||||
if 'DisplayName' in friend:
|
||||
utils.emoji_formatter(friend, 'DisplayName')
|
||||
if 'RemarkName' in friend:
|
||||
utils.emoji_formatter(friend, 'RemarkName')
|
||||
oldInfoDict = utils.search_dict_list(
|
||||
fullList, 'UserName', friend['UserName'])
|
||||
if oldInfoDict is None:
|
||||
oldInfoDict = copy.deepcopy(friend)
|
||||
if oldInfoDict['VerifyFlag'] & 8 == 0:
|
||||
core.memberList.append(oldInfoDict)
|
||||
else:
|
||||
core.mpList.append(oldInfoDict)
|
||||
else:
|
||||
update_info_dict(oldInfoDict, friend)
|
||||
|
||||
@contact_change
|
||||
def update_local_uin(core, msg):
|
||||
'''
|
||||
content contains uins and StatusNotifyUserName contains username
|
||||
they are in same order, so what I do is to pair them together
|
||||
|
||||
I caught an exception in this method while not knowing why
|
||||
but don't worry, it won't cause any problem
|
||||
'''
|
||||
uins = re.search('<username>([^<]*?)<', msg['Content'])
|
||||
usernameChangedList = []
|
||||
r = {
|
||||
'Type': 'System',
|
||||
'Text': usernameChangedList,
|
||||
'SystemInfo': 'uins', }
|
||||
if uins:
|
||||
uins = uins.group(1).split(',')
|
||||
usernames = msg['StatusNotifyUserName'].split(',')
|
||||
if 0 < len(uins) == len(usernames):
|
||||
for uin, username in zip(uins, usernames):
|
||||
if not '@' in username: continue
|
||||
fullContact = core.memberList + core.chatroomList + core.mpList
|
||||
userDicts = utils.search_dict_list(fullContact,
|
||||
'UserName', username)
|
||||
if userDicts:
|
||||
if userDicts.get('Uin', 0) == 0:
|
||||
userDicts['Uin'] = uin
|
||||
usernameChangedList.append(username)
|
||||
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||
else:
|
||||
if userDicts['Uin'] != uin:
|
||||
logger.debug('Uin changed: %s, %s' % (
|
||||
userDicts['Uin'], uin))
|
||||
else:
|
||||
if '@@' in username:
|
||||
core.storageClass.updateLock.release()
|
||||
update_chatroom(core, username)
|
||||
core.storageClass.updateLock.acquire()
|
||||
newChatroomDict = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', username)
|
||||
if newChatroomDict is None:
|
||||
newChatroomDict = utils.struct_friend_info({
|
||||
'UserName': username,
|
||||
'Uin': uin,
|
||||
'Self': copy.deepcopy(core.loginInfo['User'])})
|
||||
core.chatroomList.append(newChatroomDict)
|
||||
else:
|
||||
newChatroomDict['Uin'] = uin
|
||||
elif '@' in username:
|
||||
core.storageClass.updateLock.release()
|
||||
update_friend(core, username)
|
||||
core.storageClass.updateLock.acquire()
|
||||
newFriendDict = utils.search_dict_list(
|
||||
core.memberList, 'UserName', username)
|
||||
if newFriendDict is None:
|
||||
newFriendDict = utils.struct_friend_info({
|
||||
'UserName': username,
|
||||
'Uin': uin, })
|
||||
core.memberList.append(newFriendDict)
|
||||
else:
|
||||
newFriendDict['Uin'] = uin
|
||||
usernameChangedList.append(username)
|
||||
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||
else:
|
||||
logger.debug('Wrong length of uins & usernames: %s, %s' % (
|
||||
len(uins), len(usernames)))
|
||||
else:
|
||||
logger.debug('No uins in 51 message')
|
||||
logger.debug(msg['Content'])
|
||||
return r
|
||||
|
||||
def get_contact(self, update=False):
|
||||
if not update:
|
||||
return utils.contact_deep_copy(self, self.chatroomList)
|
||||
def _get_contact(seq=0):
|
||||
url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'],
|
||||
int(time.time()), seq, self.loginInfo['skey'])
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT, }
|
||||
try:
|
||||
r = self.s.get(url, headers=headers)
|
||||
except:
|
||||
logger.info('Failed to fetch contact, that may because of the amount of your chatrooms')
|
||||
for chatroom in self.get_chatrooms():
|
||||
self.update_chatroom(chatroom['UserName'], detailedMember=True)
|
||||
return 0, []
|
||||
j = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
return j.get('Seq', 0), j.get('MemberList')
|
||||
seq, memberList = 0, []
|
||||
while 1:
|
||||
seq, batchMemberList = _get_contact(seq)
|
||||
memberList.extend(batchMemberList)
|
||||
if seq == 0:
|
||||
break
|
||||
chatroomList, otherList = [], []
|
||||
for m in memberList:
|
||||
if m['Sex'] != 0:
|
||||
otherList.append(m)
|
||||
elif '@@' in m['UserName']:
|
||||
chatroomList.append(m)
|
||||
elif '@' in m['UserName']:
|
||||
# mp will be dealt in update_local_friends as well
|
||||
otherList.append(m)
|
||||
if chatroomList:
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
if otherList:
|
||||
update_local_friends(self, otherList)
|
||||
return utils.contact_deep_copy(self, chatroomList)
|
||||
|
||||
def get_friends(self, update=False):
|
||||
if update:
|
||||
self.get_contact(update=True)
|
||||
return utils.contact_deep_copy(self, self.memberList)
|
||||
|
||||
def get_chatrooms(self, update=False, contactOnly=False):
|
||||
if contactOnly:
|
||||
return self.get_contact(update=True)
|
||||
else:
|
||||
if update:
|
||||
self.get_contact(True)
|
||||
return utils.contact_deep_copy(self, self.chatroomList)
|
||||
|
||||
def get_mps(self, update=False):
|
||||
if update: self.get_contact(update=True)
|
||||
return utils.contact_deep_copy(self, self.mpList)
|
||||
|
||||
def set_alias(self, userName, alias):
|
||||
oldFriendInfo = utils.search_dict_list(
|
||||
self.memberList, 'UserName', userName)
|
||||
if oldFriendInfo is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1001, }})
|
||||
url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'UserName' : userName,
|
||||
'CmdId' : 2,
|
||||
'RemarkName' : alias,
|
||||
'BaseRequest' : self.loginInfo['BaseRequest'], }
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'),
|
||||
headers=headers)
|
||||
r = ReturnValue(rawResponse=r)
|
||||
if r:
|
||||
oldFriendInfo['RemarkName'] = alias
|
||||
return r
|
||||
|
||||
def set_pinned(self, userName, isPinned=True):
|
||||
url = '%s/webwxoplog?pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'UserName' : userName,
|
||||
'CmdId' : 3,
|
||||
'OP' : int(isPinned),
|
||||
'BaseRequest' : self.loginInfo['BaseRequest'], }
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.post(url, json=data, headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def accept_friend(self, userName, v4= '', autoUpdate=True):
|
||||
url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}"
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Opcode': 3, # 3
|
||||
'VerifyUserListSize': 1,
|
||||
'VerifyUserList': [{
|
||||
'Value': userName,
|
||||
'VerifyUserTicket': v4, }],
|
||||
'VerifyContent': '',
|
||||
'SceneListCount': 1,
|
||||
'SceneList': [33],
|
||||
'skey': self.loginInfo['skey'], }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace'))
|
||||
if autoUpdate:
|
||||
self.update_friend(userName)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||
''' get head image
|
||||
* if you want to get chatroom header: only set chatroomUserName
|
||||
* if you want to get friend header: only set userName
|
||||
* if you want to get chatroom member header: set both
|
||||
'''
|
||||
params = {
|
||||
'userName': userName or chatroomUserName or self.storageClass.userName,
|
||||
'skey': self.loginInfo['skey'],
|
||||
'type': 'big', }
|
||||
url = '%s/webwxgeticon' % self.loginInfo['url']
|
||||
if chatroomUserName is None:
|
||||
infoDict = self.storageClass.search_friends(userName=userName)
|
||||
if infoDict is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No friend found',
|
||||
'Ret': -1001, }})
|
||||
else:
|
||||
if userName is None:
|
||||
url = '%s/webwxgetheadimg' % self.loginInfo['url']
|
||||
else:
|
||||
chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||
if chatroomUserName is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No chatroom found',
|
||||
'Ret': -1001, }})
|
||||
if 'EncryChatRoomId' in chatroom:
|
||||
params['chatroomid'] = chatroom['EncryChatRoomId']
|
||||
params['chatroomid'] = params.get('chatroomid') or chatroom['UserName']
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, stream=True, headers=headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if picDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(picDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
tempStorage.seek(0)
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, },
|
||||
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||
|
||||
def create_chatroom(self, memberList, topic=''):
|
||||
url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time()))
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'MemberCount': len(memberList.split(',')),
|
||||
'MemberList': [{'UserName': member} for member in memberList.split(',')],
|
||||
'Topic': topic, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def set_chatroom_name(self, chatroomUserName, name):
|
||||
url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName': chatroomUserName,
|
||||
'NewTopic': name, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||
url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName': chatroomUserName,
|
||||
'DelMemberList': ','.join([member['UserName'] for member in memberList]), }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.post(url, data=json.dumps(data),headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||
useInvitation=False):
|
||||
''' add or invite member into chatroom
|
||||
* there are two ways to get members into chatroom: invite or directly add
|
||||
* but for chatrooms with more than 40 users, you can only use invite
|
||||
* but don't worry we will auto-force userInvitation for you when necessary
|
||||
'''
|
||||
if not useInvitation:
|
||||
chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||
if not chatroom: chatroom = self.update_chatroom(chatroomUserName)
|
||||
if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']:
|
||||
useInvitation = True
|
||||
if useInvitation:
|
||||
fun, memberKeyName = 'invitemember', 'InviteMemberList'
|
||||
else:
|
||||
fun, memberKeyName = 'addmember', 'AddMemberList'
|
||||
url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], fun, self.loginInfo['pass_ticket'])
|
||||
params = {
|
||||
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName' : chatroomUserName,
|
||||
memberKeyName : memberList, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.post(url, data=json.dumps(params),headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
102
lib/itchat/async_components/hotreload.py
Normal file
102
lib/itchat/async_components/hotreload.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pickle, os
|
||||
import logging
|
||||
|
||||
import requests # type: ignore
|
||||
|
||||
from ..config import VERSION
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import templates
|
||||
from .contact import update_local_chatrooms, update_local_friends
|
||||
from .messages import produce_msg
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_hotreload(core):
|
||||
core.dump_login_status = dump_login_status
|
||||
core.load_login_status = load_login_status
|
||||
|
||||
async def dump_login_status(self, fileDir=None):
|
||||
fileDir = fileDir or self.hotReloadDir
|
||||
try:
|
||||
with open(fileDir, 'w') as f:
|
||||
f.write('itchat - DELETE THIS')
|
||||
os.remove(fileDir)
|
||||
except:
|
||||
raise Exception('Incorrect fileDir')
|
||||
status = {
|
||||
'version' : VERSION,
|
||||
'loginInfo' : self.loginInfo,
|
||||
'cookies' : self.s.cookies.get_dict(),
|
||||
'storage' : self.storageClass.dumps()}
|
||||
with open(fileDir, 'wb') as f:
|
||||
pickle.dump(status, f)
|
||||
logger.debug('Dump login status for hot reload successfully.')
|
||||
|
||||
async def load_login_status(self, fileDir,
|
||||
loginCallback=None, exitCallback=None):
|
||||
try:
|
||||
with open(fileDir, 'rb') as f:
|
||||
j = pickle.load(f)
|
||||
except Exception as e:
|
||||
logger.debug('No such file, loading login status failed.')
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No such file, loading login status failed.',
|
||||
'Ret': -1002, }})
|
||||
|
||||
if j.get('version', '') != VERSION:
|
||||
logger.debug(('you have updated itchat from %s to %s, ' +
|
||||
'so cached status is ignored') % (
|
||||
j.get('version', 'old version'), VERSION))
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'cached status ignored because of version',
|
||||
'Ret': -1005, }})
|
||||
self.loginInfo = j['loginInfo']
|
||||
self.loginInfo['User'] = templates.User(self.loginInfo['User'])
|
||||
self.loginInfo['User'].core = self
|
||||
self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies'])
|
||||
self.storageClass.loads(j['storage'])
|
||||
try:
|
||||
msgList, contactList = self.get_msg()
|
||||
except:
|
||||
msgList = contactList = None
|
||||
if (msgList or contactList) is None:
|
||||
self.logout()
|
||||
await load_last_login_status(self.s, j['cookies'])
|
||||
logger.debug('server refused, loading login status failed.')
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'server refused, loading login status failed.',
|
||||
'Ret': -1003, }})
|
||||
else:
|
||||
if contactList:
|
||||
for contact in contactList:
|
||||
if '@@' in contact['UserName']:
|
||||
update_local_chatrooms(self, [contact])
|
||||
else:
|
||||
update_local_friends(self, [contact])
|
||||
if msgList:
|
||||
msgList = produce_msg(self, msgList)
|
||||
for msg in msgList: self.msgList.put(msg)
|
||||
await self.start_receiving(exitCallback)
|
||||
logger.debug('loading login status succeeded.')
|
||||
if hasattr(loginCallback, '__call__'):
|
||||
await loginCallback(self.storageClass.userName)
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'loading login status succeeded.',
|
||||
'Ret': 0, }})
|
||||
|
||||
async def load_last_login_status(session, cookiesDict):
|
||||
try:
|
||||
session.cookies = requests.utils.cookiejar_from_dict({
|
||||
'webwxuvid': cookiesDict['webwxuvid'],
|
||||
'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'],
|
||||
'login_frequency': '2',
|
||||
'last_wxuin': cookiesDict['wxuin'],
|
||||
'wxloadtime': cookiesDict['wxloadtime'] + '_expired',
|
||||
'wxpluginkey': cookiesDict['wxloadtime'],
|
||||
'wxuin': cookiesDict['wxuin'],
|
||||
'mm_lang': 'zh_CN',
|
||||
'MM_WX_NOTIFY_STATE': '1',
|
||||
'MM_WX_SOUND_STATE': '1', })
|
||||
except:
|
||||
logger.info('Load status for push login failed, we may have experienced a cookies change.')
|
||||
logger.info('If you are using the newest version of itchat, you may report a bug.')
|
||||
422
lib/itchat/async_components/login.py
Normal file
422
lib/itchat/async_components/login.py
Normal file
@@ -0,0 +1,422 @@
|
||||
import asyncio
|
||||
import os, time, re, io
|
||||
import threading
|
||||
import json
|
||||
import random
|
||||
import traceback
|
||||
import logging
|
||||
try:
|
||||
from httplib import BadStatusLine
|
||||
except ImportError:
|
||||
from http.client import BadStatusLine
|
||||
|
||||
import requests # type: ignore
|
||||
from pyqrcode import QRCode
|
||||
|
||||
from .. import config, utils
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage.templates import wrap_user_dict
|
||||
from .contact import update_local_chatrooms, update_local_friends
|
||||
from .messages import produce_msg
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
|
||||
def load_login(core):
|
||||
core.login = login
|
||||
core.get_QRuuid = get_QRuuid
|
||||
core.get_QR = get_QR
|
||||
core.check_login = check_login
|
||||
core.web_init = web_init
|
||||
core.show_mobile_login = show_mobile_login
|
||||
core.start_receiving = start_receiving
|
||||
core.get_msg = get_msg
|
||||
core.logout = logout
|
||||
|
||||
async def login(self, enableCmdQR=False, picDir=None, qrCallback=None, EventScanPayload=None,ScanStatus=None,event_stream=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
if self.alive or self.isLogging:
|
||||
logger.warning('itchat has already logged in.')
|
||||
return
|
||||
self.isLogging = True
|
||||
|
||||
while self.isLogging:
|
||||
uuid = await push_login(self)
|
||||
if uuid:
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Waiting,
|
||||
qrcode=f"qrcode/https://login.weixin.qq.com/l/{uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
logger.info('Getting uuid of QR code.')
|
||||
self.get_QRuuid()
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Waiting,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
print(f"https://wechaty.js.org/qrcode/https://login.weixin.qq.com/l/{self.uuid}")
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
# logger.info('Please scan the QR code to log in.')
|
||||
isLoggedIn = False
|
||||
while not isLoggedIn:
|
||||
status = await self.check_login()
|
||||
# if hasattr(qrCallback, '__call__'):
|
||||
# await qrCallback(uuid=self.uuid, status=status, qrcode=self.qrStorage.getvalue())
|
||||
if status == '200':
|
||||
isLoggedIn = True
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Scanned,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
elif status == '201':
|
||||
if isLoggedIn is not None:
|
||||
logger.info('Please press confirm on your phone.')
|
||||
isLoggedIn = None
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Waiting,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
elif status != '408':
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Cancel,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
break
|
||||
if isLoggedIn:
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Confirmed,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
break
|
||||
elif self.isLogging:
|
||||
logger.info('Log in time out, reloading QR code.')
|
||||
payload = EventScanPayload(
|
||||
status=ScanStatus.Timeout,
|
||||
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||
)
|
||||
event_stream.emit('scan', payload)
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
return
|
||||
logger.info('Loading the contact, this may take a little while.')
|
||||
await self.web_init()
|
||||
await self.show_mobile_login()
|
||||
self.get_contact(True)
|
||||
if hasattr(loginCallback, '__call__'):
|
||||
r = await loginCallback(self.storageClass.userName)
|
||||
else:
|
||||
utils.clear_screen()
|
||||
if os.path.exists(picDir or config.DEFAULT_QR):
|
||||
os.remove(picDir or config.DEFAULT_QR)
|
||||
logger.info('Login successfully as %s' % self.storageClass.nickName)
|
||||
await self.start_receiving(exitCallback)
|
||||
self.isLogging = False
|
||||
|
||||
async def push_login(core):
|
||||
cookiesDict = core.s.cookies.get_dict()
|
||||
if 'wxuin' in cookiesDict:
|
||||
url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % (
|
||||
config.BASE_URL, cookiesDict['wxuin'])
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = core.s.get(url, headers=headers).json()
|
||||
if 'uuid' in r and r.get('ret') in (0, '0'):
|
||||
core.uuid = r['uuid']
|
||||
return r['uuid']
|
||||
return False
|
||||
|
||||
def get_QRuuid(self):
|
||||
url = '%s/jslogin' % config.BASE_URL
|
||||
params = {
|
||||
'appid' : 'wx782c26e4c19acffb',
|
||||
'fun' : 'new',
|
||||
'redirect_uri' : 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
|
||||
'lang' : 'zh_CN' }
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, headers=headers)
|
||||
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";'
|
||||
data = re.search(regx, r.text)
|
||||
if data and data.group(1) == '200':
|
||||
self.uuid = data.group(2)
|
||||
return self.uuid
|
||||
|
||||
async def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||
uuid = uuid or self.uuid
|
||||
picDir = picDir or config.DEFAULT_QR
|
||||
qrStorage = io.BytesIO()
|
||||
qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid)
|
||||
qrCode.png(qrStorage, scale=10)
|
||||
if hasattr(qrCallback, '__call__'):
|
||||
await qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue())
|
||||
else:
|
||||
with open(picDir, 'wb') as f:
|
||||
f.write(qrStorage.getvalue())
|
||||
if enableCmdQR:
|
||||
utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR)
|
||||
else:
|
||||
utils.print_qr(picDir)
|
||||
return qrStorage
|
||||
|
||||
async def check_login(self, uuid=None):
|
||||
uuid = uuid or self.uuid
|
||||
url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL
|
||||
localTime = int(time.time())
|
||||
params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % (
|
||||
uuid, int(-localTime / 1579), localTime)
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, headers=headers)
|
||||
regx = r'window.code=(\d+)'
|
||||
data = re.search(regx, r.text)
|
||||
if data and data.group(1) == '200':
|
||||
if await process_login_info(self, r.text):
|
||||
return '200'
|
||||
else:
|
||||
return '400'
|
||||
elif data:
|
||||
return data.group(1)
|
||||
else:
|
||||
return '400'
|
||||
|
||||
async def process_login_info(core, loginContent):
|
||||
''' when finish login (scanning qrcode)
|
||||
* syncUrl and fileUploadingUrl will be fetched
|
||||
* deviceid and msgid will be generated
|
||||
* skey, wxsid, wxuin, pass_ticket will be fetched
|
||||
'''
|
||||
regx = r'window.redirect_uri="(\S+)";'
|
||||
core.loginInfo['url'] = re.search(regx, loginContent).group(1)
|
||||
headers = { 'User-Agent' : config.USER_AGENT,
|
||||
'client-version' : config.UOS_PATCH_CLIENT_VERSION,
|
||||
'extspam' : config.UOS_PATCH_EXTSPAM,
|
||||
'referer' : 'https://wx.qq.com/?&lang=zh_CN&target=t'
|
||||
}
|
||||
r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False)
|
||||
core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')]
|
||||
for indexUrl, detailedUrl in (
|
||||
("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")),
|
||||
("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")),
|
||||
("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")),
|
||||
("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")),
|
||||
("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))):
|
||||
fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl]
|
||||
if indexUrl in core.loginInfo['url']:
|
||||
core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \
|
||||
fileUrl, syncUrl
|
||||
break
|
||||
else:
|
||||
core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url']
|
||||
core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||
core.loginInfo['logintime'] = int(time.time() * 1e3)
|
||||
core.loginInfo['BaseRequest'] = {}
|
||||
cookies = core.s.cookies.get_dict()
|
||||
skey = re.findall('<skey>(.*?)</skey>', r.text, re.S)[0]
|
||||
pass_ticket = re.findall('<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)[0]
|
||||
core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
|
||||
core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
|
||||
core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
|
||||
core.loginInfo['pass_ticket'] = pass_ticket
|
||||
|
||||
# A question : why pass_ticket == DeviceID ?
|
||||
# deviceID is only a randomly generated number
|
||||
|
||||
# UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
|
||||
# for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
|
||||
# if node.nodeName == 'skey':
|
||||
# core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'wxsid':
|
||||
# core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'wxuin':
|
||||
# core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'pass_ticket':
|
||||
# core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
|
||||
if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
|
||||
logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
|
||||
core.isLogging = False
|
||||
return False
|
||||
return True
|
||||
|
||||
async def web_init(self):
|
||||
url = '%s/webwxinit' % self.loginInfo['url']
|
||||
params = {
|
||||
'r': int(-time.time() / 1579),
|
||||
'pass_ticket': self.loginInfo['pass_ticket'], }
|
||||
data = { 'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT, }
|
||||
r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
|
||||
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
# deal with login info
|
||||
utils.emoji_formatter(dic['User'], 'NickName')
|
||||
self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
|
||||
self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User']))
|
||||
self.memberList.append(self.loginInfo['User'])
|
||||
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||
for item in dic['SyncKey']['List']])
|
||||
self.storageClass.userName = dic['User']['UserName']
|
||||
self.storageClass.nickName = dic['User']['NickName']
|
||||
# deal with contact list returned when init
|
||||
contactList = dic.get('ContactList', [])
|
||||
chatroomList, otherList = [], []
|
||||
for m in contactList:
|
||||
if m['Sex'] != 0:
|
||||
otherList.append(m)
|
||||
elif '@@' in m['UserName']:
|
||||
m['MemberList'] = [] # don't let dirty info pollute the list
|
||||
chatroomList.append(m)
|
||||
elif '@' in m['UserName']:
|
||||
# mp will be dealt in update_local_friends as well
|
||||
otherList.append(m)
|
||||
if chatroomList:
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
if otherList:
|
||||
update_local_friends(self, otherList)
|
||||
return dic
|
||||
|
||||
async def show_mobile_login(self):
|
||||
url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||
'Code' : 3,
|
||||
'FromUserName' : self.storageClass.userName,
|
||||
'ToUserName' : self.storageClass.userName,
|
||||
'ClientMsgId' : int(time.time()), }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT, }
|
||||
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
async def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||
self.alive = True
|
||||
def maintain_loop():
|
||||
retryCount = 0
|
||||
while self.alive:
|
||||
try:
|
||||
i = sync_check(self)
|
||||
if i is None:
|
||||
self.alive = False
|
||||
elif i == '0':
|
||||
pass
|
||||
else:
|
||||
msgList, contactList = self.get_msg()
|
||||
if msgList:
|
||||
msgList = produce_msg(self, msgList)
|
||||
for msg in msgList:
|
||||
self.msgList.put(msg)
|
||||
if contactList:
|
||||
chatroomList, otherList = [], []
|
||||
for contact in contactList:
|
||||
if '@@' in contact['UserName']:
|
||||
chatroomList.append(contact)
|
||||
else:
|
||||
otherList.append(contact)
|
||||
chatroomMsg = update_local_chatrooms(self, chatroomList)
|
||||
chatroomMsg['User'] = self.loginInfo['User']
|
||||
self.msgList.put(chatroomMsg)
|
||||
update_local_friends(self, otherList)
|
||||
retryCount = 0
|
||||
except requests.exceptions.ReadTimeout:
|
||||
pass
|
||||
except:
|
||||
retryCount += 1
|
||||
logger.error(traceback.format_exc())
|
||||
if self.receivingRetryCount < retryCount:
|
||||
self.alive = False
|
||||
else:
|
||||
time.sleep(1)
|
||||
self.logout()
|
||||
if hasattr(exitCallback, '__call__'):
|
||||
exitCallback(self.storageClass.userName)
|
||||
else:
|
||||
logger.info('LOG OUT!')
|
||||
if getReceivingFnOnly:
|
||||
return maintain_loop
|
||||
else:
|
||||
maintainThread = threading.Thread(target=maintain_loop)
|
||||
maintainThread.setDaemon(True)
|
||||
maintainThread.start()
|
||||
|
||||
def sync_check(self):
|
||||
url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
|
||||
params = {
|
||||
'r' : int(time.time() * 1000),
|
||||
'skey' : self.loginInfo['skey'],
|
||||
'sid' : self.loginInfo['wxsid'],
|
||||
'uin' : self.loginInfo['wxuin'],
|
||||
'deviceid' : self.loginInfo['deviceid'],
|
||||
'synckey' : self.loginInfo['synckey'],
|
||||
'_' : self.loginInfo['logintime'], }
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
self.loginInfo['logintime'] += 1
|
||||
try:
|
||||
r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
try:
|
||||
if not isinstance(e.args[0].args[1], BadStatusLine):
|
||||
raise
|
||||
# will return a package with status '0 -'
|
||||
# and value like:
|
||||
# 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93
|
||||
# seems like status of typing, but before I make further achievement code will remain like this
|
||||
return '2'
|
||||
except:
|
||||
raise
|
||||
r.raise_for_status()
|
||||
regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
|
||||
pm = re.search(regx, r.text)
|
||||
if pm is None or pm.group(1) != '0':
|
||||
logger.debug('Unexpected sync check result: %s' % r.text)
|
||||
return None
|
||||
return pm.group(2)
|
||||
|
||||
def get_msg(self):
|
||||
self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||
url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['wxsid'],
|
||||
self.loginInfo['skey'],self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||
'SyncKey' : self.loginInfo['SyncKey'],
|
||||
'rr' : ~int(time.time()), }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT)
|
||||
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
if dic['BaseResponse']['Ret'] != 0: return None, None
|
||||
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||
for item in dic['SyncCheckKey']['List']])
|
||||
return dic['AddMsgList'], dic['ModContactList']
|
||||
|
||||
def logout(self):
|
||||
if self.alive:
|
||||
url = '%s/webwxlogout' % self.loginInfo['url']
|
||||
params = {
|
||||
'redirect' : 1,
|
||||
'type' : 1,
|
||||
'skey' : self.loginInfo['skey'], }
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
self.s.get(url, params=params, headers=headers)
|
||||
self.alive = False
|
||||
self.isLogging = False
|
||||
self.s.cookies.clear()
|
||||
del self.chatroomList[:]
|
||||
del self.memberList[:]
|
||||
del self.mpList[:]
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'logout successfully.',
|
||||
'Ret': 0, }})
|
||||
527
lib/itchat/async_components/messages.py
Normal file
527
lib/itchat/async_components/messages.py
Normal file
@@ -0,0 +1,527 @@
|
||||
import os, time, re, io
|
||||
import json
|
||||
import mimetypes, hashlib
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
from .. import config, utils
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import templates
|
||||
from .contact import update_local_uin
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_messages(core):
|
||||
core.send_raw_msg = send_raw_msg
|
||||
core.send_msg = send_msg
|
||||
core.upload_file = upload_file
|
||||
core.send_file = send_file
|
||||
core.send_image = send_image
|
||||
core.send_video = send_video
|
||||
core.send = send
|
||||
core.revoke = revoke
|
||||
|
||||
async def get_download_fn(core, url, msgId):
|
||||
async def download_fn(downloadDir=None):
|
||||
params = {
|
||||
'msgid': msgId,
|
||||
'skey': core.loginInfo['skey'],}
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = core.s.get(url, params=params, stream=True, headers = headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if downloadDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(downloadDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
tempStorage.seek(0)
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, },
|
||||
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||
return download_fn
|
||||
|
||||
def produce_msg(core, msgList):
|
||||
''' for messages types
|
||||
* 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg
|
||||
* 53 webwxvoipnotifymsg, 9999 sysnotice
|
||||
'''
|
||||
rl = []
|
||||
srl = [40, 43, 50, 52, 53, 9999]
|
||||
for m in msgList:
|
||||
# get actual opposite
|
||||
if m['FromUserName'] == core.storageClass.userName:
|
||||
actualOpposite = m['ToUserName']
|
||||
else:
|
||||
actualOpposite = m['FromUserName']
|
||||
# produce basic message
|
||||
if '@@' in m['FromUserName'] or '@@' in m['ToUserName']:
|
||||
produce_group_chat(core, m)
|
||||
else:
|
||||
utils.msg_formatter(m, 'Content')
|
||||
# set user of msg
|
||||
if '@@' in actualOpposite:
|
||||
m['User'] = core.search_chatrooms(userName=actualOpposite) or \
|
||||
templates.Chatroom({'UserName': actualOpposite})
|
||||
# we don't need to update chatroom here because we have
|
||||
# updated once when producing basic message
|
||||
elif actualOpposite in ('filehelper', 'fmessage'):
|
||||
m['User'] = templates.User({'UserName': actualOpposite})
|
||||
else:
|
||||
m['User'] = core.search_mps(userName=actualOpposite) or \
|
||||
core.search_friends(userName=actualOpposite) or \
|
||||
templates.User(userName=actualOpposite)
|
||||
# by default we think there may be a user missing not a mp
|
||||
m['User'].core = core
|
||||
if m['MsgType'] == 1: # words
|
||||
if m['Url']:
|
||||
regx = r'(.+?\(.+?\))'
|
||||
data = re.search(regx, m['Content'])
|
||||
data = 'Map' if data is None else data.group(1)
|
||||
msg = {
|
||||
'Type': 'Map',
|
||||
'Text': data,}
|
||||
else:
|
||||
msg = {
|
||||
'Type': 'Text',
|
||||
'Text': m['Content'],}
|
||||
elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type' : 'Picture',
|
||||
'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'png' if m['MsgType'] == 3 else 'gif'),
|
||||
'Text' : download_fn, }
|
||||
elif m['MsgType'] == 34: # voice
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type': 'Recording',
|
||||
'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'Text': download_fn,}
|
||||
elif m['MsgType'] == 37: # friends
|
||||
m['User']['UserName'] = m['RecommendInfo']['UserName']
|
||||
msg = {
|
||||
'Type': 'Friends',
|
||||
'Text': {
|
||||
'status' : m['Status'],
|
||||
'userName' : m['RecommendInfo']['UserName'],
|
||||
'verifyContent' : m['Ticket'],
|
||||
'autoUpdate' : m['RecommendInfo'], }, }
|
||||
m['User'].verifyDict = msg['Text']
|
||||
elif m['MsgType'] == 42: # name card
|
||||
msg = {
|
||||
'Type': 'Card',
|
||||
'Text': m['RecommendInfo'], }
|
||||
elif m['MsgType'] in (43, 62): # tiny video
|
||||
msgId = m['MsgId']
|
||||
async def download_video(videoDir=None):
|
||||
url = '%s/webwxgetvideo' % core.loginInfo['url']
|
||||
params = {
|
||||
'msgid': msgId,
|
||||
'skey': core.loginInfo['skey'],}
|
||||
headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT}
|
||||
r = core.s.get(url, params=params, headers=headers, stream=True)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if videoDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(videoDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, }})
|
||||
msg = {
|
||||
'Type': 'Video',
|
||||
'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'Text': download_video, }
|
||||
elif m['MsgType'] == 49: # sharing
|
||||
if m['AppMsgType'] == 0: # chat history
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['Content'], }
|
||||
elif m['AppMsgType'] == 6:
|
||||
rawMsg = m
|
||||
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||
async def download_atta(attaDir=None):
|
||||
url = core.loginInfo['fileUrl'] + '/webwxgetmedia'
|
||||
params = {
|
||||
'sender': rawMsg['FromUserName'],
|
||||
'mediaid': rawMsg['MediaId'],
|
||||
'filename': rawMsg['FileName'],
|
||||
'fromuser': core.loginInfo['wxuin'],
|
||||
'pass_ticket': 'undefined',
|
||||
'webwx_data_ticket': cookiesList['webwx_data_ticket'],}
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
r = core.s.get(url, params=params, stream=True, headers=headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if attaDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(attaDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, }})
|
||||
msg = {
|
||||
'Type': 'Attachment',
|
||||
'Text': download_atta, }
|
||||
elif m['AppMsgType'] == 8:
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type' : 'Picture',
|
||||
'FileName' : '%s.gif' % (
|
||||
time.strftime('%y%m%d-%H%M%S', time.localtime())),
|
||||
'Text' : download_fn, }
|
||||
elif m['AppMsgType'] == 17:
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['FileName'], }
|
||||
elif m['AppMsgType'] == 2000:
|
||||
regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]'
|
||||
data = re.search(regx, m['Content'])
|
||||
if data:
|
||||
data = data.group(2).split(u'\u3002')[0]
|
||||
else:
|
||||
data = 'You may found detailed info in Content key.'
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': data, }
|
||||
else:
|
||||
msg = {
|
||||
'Type': 'Sharing',
|
||||
'Text': m['FileName'], }
|
||||
elif m['MsgType'] == 51: # phone init
|
||||
msg = update_local_uin(core, m)
|
||||
elif m['MsgType'] == 10000:
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['Content'],}
|
||||
elif m['MsgType'] == 10002:
|
||||
regx = r'\[CDATA\[(.+?)\]\]'
|
||||
data = re.search(regx, m['Content'])
|
||||
data = 'System message' if data is None else data.group(1).replace('\\', '')
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': data, }
|
||||
elif m['MsgType'] in srl:
|
||||
msg = {
|
||||
'Type': 'Useless',
|
||||
'Text': 'UselessMsg', }
|
||||
else:
|
||||
logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m)))
|
||||
msg = {
|
||||
'Type': 'Useless',
|
||||
'Text': 'UselessMsg', }
|
||||
m = dict(m, **msg)
|
||||
rl.append(m)
|
||||
return rl
|
||||
|
||||
def produce_group_chat(core, msg):
|
||||
r = re.match('(@[0-9a-z]*?):<br/>(.*)$', msg['Content'])
|
||||
if r:
|
||||
actualUserName, content = r.groups()
|
||||
chatroomUserName = msg['FromUserName']
|
||||
elif msg['FromUserName'] == core.storageClass.userName:
|
||||
actualUserName = core.storageClass.userName
|
||||
content = msg['Content']
|
||||
chatroomUserName = msg['ToUserName']
|
||||
else:
|
||||
msg['ActualUserName'] = core.storageClass.userName
|
||||
msg['ActualNickName'] = core.storageClass.nickName
|
||||
msg['IsAt'] = False
|
||||
utils.msg_formatter(msg, 'Content')
|
||||
return
|
||||
chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||
member = utils.search_dict_list((chatroom or {}).get(
|
||||
'MemberList') or [], 'UserName', actualUserName)
|
||||
if member is None:
|
||||
chatroom = core.update_chatroom(chatroomUserName)
|
||||
member = utils.search_dict_list((chatroom or {}).get(
|
||||
'MemberList') or [], 'UserName', actualUserName)
|
||||
if member is None:
|
||||
logger.debug('chatroom member fetch failed with %s' % actualUserName)
|
||||
msg['ActualNickName'] = ''
|
||||
msg['IsAt'] = False
|
||||
else:
|
||||
msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName']
|
||||
atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName)
|
||||
msg['IsAt'] = (
|
||||
(atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' '))
|
||||
in msg['Content'] or msg['Content'].endswith(atFlag))
|
||||
msg['ActualUserName'] = actualUserName
|
||||
msg['Content'] = content
|
||||
utils.msg_formatter(msg, 'Content')
|
||||
|
||||
async def send_raw_msg(self, msgType, content, toUserName):
|
||||
url = '%s/webwxsendmsg' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': msgType,
|
||||
'Content': content,
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': (toUserName if toUserName else self.storageClass.userName),
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4),
|
||||
},
|
||||
'Scene': 0, }
|
||||
headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT}
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
async def send_msg(self, msg='Test Message', toUserName=None):
|
||||
logger.debug('Request to send a text message to %s: %s' % (toUserName, msg))
|
||||
r = await self.send_raw_msg(1, msg, toUserName)
|
||||
return r
|
||||
|
||||
def _prepare_file(fileDir, file_=None):
|
||||
fileDict = {}
|
||||
if file_:
|
||||
if hasattr(file_, 'read'):
|
||||
file_ = file_.read()
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'file_ param should be opened file',
|
||||
'Ret': -1005, }})
|
||||
else:
|
||||
if not utils.check_file(fileDir):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No file found in specific dir',
|
||||
'Ret': -1002, }})
|
||||
with open(fileDir, 'rb') as f:
|
||||
file_ = f.read()
|
||||
fileDict['fileSize'] = len(file_)
|
||||
fileDict['fileMd5'] = hashlib.md5(file_).hexdigest()
|
||||
fileDict['file_'] = io.BytesIO(file_)
|
||||
return fileDict
|
||||
|
||||
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||
toUserName='filehelper', file_=None, preparedFile=None):
|
||||
logger.debug('Request to upload a %s: %s' % (
|
||||
'picture' if isPicture else 'video' if isVideo else 'file', fileDir))
|
||||
if not preparedFile:
|
||||
preparedFile = _prepare_file(fileDir, file_)
|
||||
if not preparedFile:
|
||||
return preparedFile
|
||||
fileSize, fileMd5, file_ = \
|
||||
preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_']
|
||||
fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc'
|
||||
chunks = int((fileSize - 1) / 524288) + 1
|
||||
clientMediaId = int(time.time() * 1e4)
|
||||
uploadMediaRequest = json.dumps(OrderedDict([
|
||||
('UploadType', 2),
|
||||
('BaseRequest', self.loginInfo['BaseRequest']),
|
||||
('ClientMediaId', clientMediaId),
|
||||
('TotalLen', fileSize),
|
||||
('StartPos', 0),
|
||||
('DataLen', fileSize),
|
||||
('MediaType', 4),
|
||||
('FromUserName', self.storageClass.userName),
|
||||
('ToUserName', toUserName),
|
||||
('FileMd5', fileMd5)]
|
||||
), separators = (',', ':'))
|
||||
r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}}
|
||||
for chunk in range(chunks):
|
||||
r = upload_chunk_file(self, fileDir, fileSymbol, fileSize,
|
||||
file_, chunk, chunks, uploadMediaRequest)
|
||||
file_.close()
|
||||
if isinstance(r, dict):
|
||||
return ReturnValue(r)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def upload_chunk_file(core, fileDir, fileSymbol, fileSize,
|
||||
file_, chunk, chunks, uploadMediaRequest):
|
||||
url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \
|
||||
'/webwxuploadmedia?f=json'
|
||||
# save it on server
|
||||
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||
fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream'
|
||||
fileName = utils.quote(os.path.basename(fileDir))
|
||||
files = OrderedDict([
|
||||
('id', (None, 'WU_FILE_0')),
|
||||
('name', (None, fileName)),
|
||||
('type', (None, fileType)),
|
||||
('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))),
|
||||
('size', (None, str(fileSize))),
|
||||
('chunks', (None, None)),
|
||||
('chunk', (None, None)),
|
||||
('mediatype', (None, fileSymbol)),
|
||||
('uploadmediarequest', (None, uploadMediaRequest)),
|
||||
('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])),
|
||||
('pass_ticket', (None, core.loginInfo['pass_ticket'])),
|
||||
('filename' , (fileName, file_.read(524288), 'application/octet-stream'))])
|
||||
if chunks == 1:
|
||||
del files['chunk']; del files['chunks']
|
||||
else:
|
||||
files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks))
|
||||
headers = { 'User-Agent' : config.USER_AGENT}
|
||||
return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT)
|
||||
|
||||
async def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a file(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if hasattr(fileDir, 'read'):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'fileDir param should not be an opened file in send_file',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
preparedFile = _prepare_file(fileDir, file_)
|
||||
if not preparedFile:
|
||||
return preparedFile
|
||||
fileSize = preparedFile['fileSize']
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, preparedFile=preparedFile)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': 6,
|
||||
'Content': ("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>%s</title>" % os.path.basename(fileDir) +
|
||||
"<des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
|
||||
"<appattach><totallen>%s</totallen><attachid>%s</attachid>" % (str(fileSize), mediaId) +
|
||||
"<fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>" % os.path.splitext(fileDir)[1].replace('.','')),
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': toUserName,
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
headers = {
|
||||
'User-Agent': config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
async def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a image(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if fileDir or file_:
|
||||
if hasattr(fileDir, 'read'):
|
||||
file_, fileDir = fileDir, None
|
||||
if fileDir is None:
|
||||
fileDir = 'tmp.jpg' # specific fileDir to send gifs
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': 3,
|
||||
'MediaId': mediaId,
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': toUserName,
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
if fileDir[-4:] == '.gif':
|
||||
url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url']
|
||||
data['Msg']['Type'] = 47
|
||||
data['Msg']['EmojiFlag'] = 2
|
||||
headers = {
|
||||
'User-Agent': config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
async def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a video(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if fileDir or file_:
|
||||
if hasattr(fileDir, 'read'):
|
||||
file_, fileDir = fileDir, None
|
||||
if fileDir is None:
|
||||
fileDir = 'tmp.mp4' # specific fileDir to send other formats
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, isVideo=True, file_=file_)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type' : 43,
|
||||
'MediaId' : mediaId,
|
||||
'FromUserName' : self.storageClass.userName,
|
||||
'ToUserName' : toUserName,
|
||||
'LocalID' : int(time.time() * 1e4),
|
||||
'ClientMsgId' : int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
headers = {
|
||||
'User-Agent' : config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
async def send(self, msg, toUserName=None, mediaId=None):
|
||||
if not msg:
|
||||
r = ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No message.',
|
||||
'Ret': -1005, }})
|
||||
elif msg[:5] == '@fil@':
|
||||
if mediaId is None:
|
||||
r = await self.send_file(msg[5:], toUserName)
|
||||
else:
|
||||
r = await self.send_file(msg[5:], toUserName, mediaId)
|
||||
elif msg[:5] == '@img@':
|
||||
if mediaId is None:
|
||||
r = await self.send_image(msg[5:], toUserName)
|
||||
else:
|
||||
r = await self.send_image(msg[5:], toUserName, mediaId)
|
||||
elif msg[:5] == '@msg@':
|
||||
r = await self.send_msg(msg[5:], toUserName)
|
||||
elif msg[:5] == '@vid@':
|
||||
if mediaId is None:
|
||||
r = await self.send_video(msg[5:], toUserName)
|
||||
else:
|
||||
r = await self.send_video(msg[5:], toUserName, mediaId)
|
||||
else:
|
||||
r = await self.send_msg(msg, toUserName)
|
||||
return r
|
||||
|
||||
async def revoke(self, msgId, toUserName, localId=None):
|
||||
url = '%s/webwxrevokemsg' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
"ClientMsgId": localId or str(time.time() * 1e3),
|
||||
"SvrMsgId": msgId,
|
||||
"ToUserName": toUserName}
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
106
lib/itchat/async_components/register.py
Normal file
106
lib/itchat/async_components/register.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import logging, traceback, sys, threading
|
||||
try:
|
||||
import Queue
|
||||
except ImportError:
|
||||
import queue as Queue # type: ignore
|
||||
|
||||
from ..log import set_logging
|
||||
from ..utils import test_connect
|
||||
from ..storage import templates
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_register(core):
|
||||
core.auto_login = auto_login
|
||||
core.configured_reply = configured_reply
|
||||
core.msg_register = msg_register
|
||||
core.run = run
|
||||
|
||||
async def auto_login(self, EventScanPayload=None,ScanStatus=None,event_stream=None,
|
||||
hotReload=True, statusStorageDir='itchat.pkl',
|
||||
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
if not test_connect():
|
||||
logger.info("You can't get access to internet or wechat domain, so exit.")
|
||||
sys.exit()
|
||||
self.useHotReload = hotReload
|
||||
self.hotReloadDir = statusStorageDir
|
||||
if hotReload:
|
||||
if await self.load_login_status(statusStorageDir,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback):
|
||||
return
|
||||
await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||
await self.dump_login_status(statusStorageDir)
|
||||
else:
|
||||
await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||
|
||||
async def configured_reply(self, event_stream, payload, message_container):
|
||||
''' determine the type of message and reply if its method is defined
|
||||
however, I use a strange way to determine whether a msg is from massive platform
|
||||
I haven't found a better solution here
|
||||
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||
'''
|
||||
try:
|
||||
msg = self.msgList.get(timeout=1)
|
||||
if 'MsgId' in msg.keys():
|
||||
message_container[msg['MsgId']] = msg
|
||||
except Queue.Empty:
|
||||
pass
|
||||
else:
|
||||
if isinstance(msg['User'], templates.User):
|
||||
replyFn = self.functionDict['FriendChat'].get(msg['Type'])
|
||||
elif isinstance(msg['User'], templates.MassivePlatform):
|
||||
replyFn = self.functionDict['MpChat'].get(msg['Type'])
|
||||
elif isinstance(msg['User'], templates.Chatroom):
|
||||
replyFn = self.functionDict['GroupChat'].get(msg['Type'])
|
||||
if replyFn is None:
|
||||
r = None
|
||||
else:
|
||||
try:
|
||||
r = await replyFn(msg)
|
||||
if r is not None:
|
||||
await self.send(r, msg.get('FromUserName'))
|
||||
except:
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||
''' a decorator constructor
|
||||
return a specific decorator based on information given '''
|
||||
if not (isinstance(msgType, list) or isinstance(msgType, tuple)):
|
||||
msgType = [msgType]
|
||||
def _msg_register(fn):
|
||||
for _msgType in msgType:
|
||||
if isFriendChat:
|
||||
self.functionDict['FriendChat'][_msgType] = fn
|
||||
if isGroupChat:
|
||||
self.functionDict['GroupChat'][_msgType] = fn
|
||||
if isMpChat:
|
||||
self.functionDict['MpChat'][_msgType] = fn
|
||||
if not any((isFriendChat, isGroupChat, isMpChat)):
|
||||
self.functionDict['FriendChat'][_msgType] = fn
|
||||
return fn
|
||||
return _msg_register
|
||||
|
||||
async def run(self, debug=False, blockThread=True):
|
||||
logger.info('Start auto replying.')
|
||||
if debug:
|
||||
set_logging(loggingLevel=logging.DEBUG)
|
||||
async def reply_fn():
|
||||
try:
|
||||
while self.alive:
|
||||
await self.configured_reply()
|
||||
except KeyboardInterrupt:
|
||||
if self.useHotReload:
|
||||
await self.dump_login_status()
|
||||
self.alive = False
|
||||
logger.debug('itchat received an ^C and exit.')
|
||||
logger.info('Bye~')
|
||||
if blockThread:
|
||||
await reply_fn()
|
||||
else:
|
||||
replyThread = threading.Thread(target=reply_fn)
|
||||
replyThread.setDaemon(True)
|
||||
replyThread.start()
|
||||
12
lib/itchat/components/__init__.py
Normal file
12
lib/itchat/components/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .contact import load_contact
|
||||
from .hotreload import load_hotreload
|
||||
from .login import load_login
|
||||
from .messages import load_messages
|
||||
from .register import load_register
|
||||
|
||||
def load_components(core):
|
||||
load_contact(core)
|
||||
load_hotreload(core)
|
||||
load_login(core)
|
||||
load_messages(core)
|
||||
load_register(core)
|
||||
519
lib/itchat/components/contact.py
Normal file
519
lib/itchat/components/contact.py
Normal file
@@ -0,0 +1,519 @@
|
||||
import time
|
||||
import re
|
||||
import io
|
||||
import json
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .. import config, utils
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import contact_change
|
||||
from ..utils import update_info_dict
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
|
||||
def load_contact(core):
|
||||
core.update_chatroom = update_chatroom
|
||||
core.update_friend = update_friend
|
||||
core.get_contact = get_contact
|
||||
core.get_friends = get_friends
|
||||
core.get_chatrooms = get_chatrooms
|
||||
core.get_mps = get_mps
|
||||
core.set_alias = set_alias
|
||||
core.set_pinned = set_pinned
|
||||
core.accept_friend = accept_friend
|
||||
core.get_head_img = get_head_img
|
||||
core.create_chatroom = create_chatroom
|
||||
core.set_chatroom_name = set_chatroom_name
|
||||
core.delete_member_from_chatroom = delete_member_from_chatroom
|
||||
core.add_member_into_chatroom = add_member_into_chatroom
|
||||
|
||||
|
||||
def update_chatroom(self, userName, detailedMember=False):
|
||||
if not isinstance(userName, list):
|
||||
userName = [userName]
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(userName),
|
||||
'List': [{
|
||||
'UserName': u,
|
||||
'ChatRoomId': '', } for u in userName], }
|
||||
chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace')).get('ContactList')
|
||||
if not chatroomList:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No chatroom found',
|
||||
'Ret': -1001, }})
|
||||
|
||||
if detailedMember:
|
||||
def get_detailed_member_info(encryChatroomId, memberList):
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT, }
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(memberList),
|
||||
'List': [{
|
||||
'UserName': member['UserName'],
|
||||
'EncryChatRoomId': encryChatroomId}
|
||||
for member in memberList], }
|
||||
return json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace'))['ContactList']
|
||||
MAX_GET_NUMBER = 50
|
||||
for chatroom in chatroomList:
|
||||
totalMemberList = []
|
||||
for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)):
|
||||
memberList = chatroom['MemberList'][i *
|
||||
MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER]
|
||||
totalMemberList += get_detailed_member_info(
|
||||
chatroom['EncryChatRoomId'], memberList)
|
||||
chatroom['MemberList'] = totalMemberList
|
||||
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
r = [self.storageClass.search_chatrooms(userName=c['UserName'])
|
||||
for c in chatroomList]
|
||||
return r if 1 < len(r) else r[0]
|
||||
|
||||
|
||||
def update_friend(self, userName):
|
||||
if not isinstance(userName, list):
|
||||
userName = [userName]
|
||||
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||
self.loginInfo['url'], int(time.time()))
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Count': len(userName),
|
||||
'List': [{
|
||||
'UserName': u,
|
||||
'EncryChatRoomId': '', } for u in userName], }
|
||||
friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||
).content.decode('utf8', 'replace')).get('ContactList')
|
||||
|
||||
update_local_friends(self, friendList)
|
||||
r = [self.storageClass.search_friends(userName=f['UserName'])
|
||||
for f in friendList]
|
||||
return r if len(r) != 1 else r[0]
|
||||
|
||||
|
||||
@contact_change
|
||||
def update_local_chatrooms(core, l):
|
||||
'''
|
||||
get a list of chatrooms for updating local chatrooms
|
||||
return a list of given chatrooms with updated info
|
||||
'''
|
||||
for chatroom in l:
|
||||
# format new chatrooms
|
||||
utils.emoji_formatter(chatroom, 'NickName')
|
||||
for member in chatroom['MemberList']:
|
||||
if 'NickName' in member:
|
||||
utils.emoji_formatter(member, 'NickName')
|
||||
if 'DisplayName' in member:
|
||||
utils.emoji_formatter(member, 'DisplayName')
|
||||
if 'RemarkName' in member:
|
||||
utils.emoji_formatter(member, 'RemarkName')
|
||||
# update it to old chatrooms
|
||||
oldChatroom = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||
if oldChatroom:
|
||||
update_info_dict(oldChatroom, chatroom)
|
||||
# - update other values
|
||||
memberList = chatroom.get('MemberList', [])
|
||||
oldMemberList = oldChatroom['MemberList']
|
||||
if memberList:
|
||||
for member in memberList:
|
||||
oldMember = utils.search_dict_list(
|
||||
oldMemberList, 'UserName', member['UserName'])
|
||||
if oldMember:
|
||||
update_info_dict(oldMember, member)
|
||||
else:
|
||||
oldMemberList.append(member)
|
||||
else:
|
||||
core.chatroomList.append(chatroom)
|
||||
oldChatroom = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||
# delete useless members
|
||||
if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \
|
||||
chatroom['MemberList']:
|
||||
existsUserNames = [member['UserName']
|
||||
for member in chatroom['MemberList']]
|
||||
delList = []
|
||||
for i, member in enumerate(oldChatroom['MemberList']):
|
||||
if member['UserName'] not in existsUserNames:
|
||||
delList.append(i)
|
||||
delList.sort(reverse=True)
|
||||
for i in delList:
|
||||
del oldChatroom['MemberList'][i]
|
||||
# - update OwnerUin
|
||||
if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'):
|
||||
owner = utils.search_dict_list(oldChatroom['MemberList'],
|
||||
'UserName', oldChatroom['ChatRoomOwner'])
|
||||
oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0)
|
||||
# - update IsAdmin
|
||||
if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0:
|
||||
oldChatroom['IsAdmin'] = \
|
||||
oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin'])
|
||||
else:
|
||||
oldChatroom['IsAdmin'] = None
|
||||
# - update Self
|
||||
newSelf = utils.search_dict_list(oldChatroom['MemberList'],
|
||||
'UserName', core.storageClass.userName)
|
||||
oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User'])
|
||||
return {
|
||||
'Type': 'System',
|
||||
'Text': [chatroom['UserName'] for chatroom in l],
|
||||
'SystemInfo': 'chatrooms',
|
||||
'FromUserName': core.storageClass.userName,
|
||||
'ToUserName': core.storageClass.userName, }
|
||||
|
||||
|
||||
@contact_change
|
||||
def update_local_friends(core, l):
|
||||
'''
|
||||
get a list of friends or mps for updating local contact
|
||||
'''
|
||||
fullList = core.memberList + core.mpList
|
||||
for friend in l:
|
||||
if 'NickName' in friend:
|
||||
utils.emoji_formatter(friend, 'NickName')
|
||||
if 'DisplayName' in friend:
|
||||
utils.emoji_formatter(friend, 'DisplayName')
|
||||
if 'RemarkName' in friend:
|
||||
utils.emoji_formatter(friend, 'RemarkName')
|
||||
oldInfoDict = utils.search_dict_list(
|
||||
fullList, 'UserName', friend['UserName'])
|
||||
if oldInfoDict is None:
|
||||
oldInfoDict = copy.deepcopy(friend)
|
||||
if oldInfoDict['VerifyFlag'] & 8 == 0:
|
||||
core.memberList.append(oldInfoDict)
|
||||
else:
|
||||
core.mpList.append(oldInfoDict)
|
||||
else:
|
||||
update_info_dict(oldInfoDict, friend)
|
||||
|
||||
|
||||
@contact_change
|
||||
def update_local_uin(core, msg):
|
||||
'''
|
||||
content contains uins and StatusNotifyUserName contains username
|
||||
they are in same order, so what I do is to pair them together
|
||||
|
||||
I caught an exception in this method while not knowing why
|
||||
but don't worry, it won't cause any problem
|
||||
'''
|
||||
uins = re.search('<username>([^<]*?)<', msg['Content'])
|
||||
usernameChangedList = []
|
||||
r = {
|
||||
'Type': 'System',
|
||||
'Text': usernameChangedList,
|
||||
'SystemInfo': 'uins', }
|
||||
if uins:
|
||||
uins = uins.group(1).split(',')
|
||||
usernames = msg['StatusNotifyUserName'].split(',')
|
||||
if 0 < len(uins) == len(usernames):
|
||||
for uin, username in zip(uins, usernames):
|
||||
if not '@' in username:
|
||||
continue
|
||||
fullContact = core.memberList + core.chatroomList + core.mpList
|
||||
userDicts = utils.search_dict_list(fullContact,
|
||||
'UserName', username)
|
||||
if userDicts:
|
||||
if userDicts.get('Uin', 0) == 0:
|
||||
userDicts['Uin'] = uin
|
||||
usernameChangedList.append(username)
|
||||
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||
else:
|
||||
if userDicts['Uin'] != uin:
|
||||
logger.debug('Uin changed: %s, %s' % (
|
||||
userDicts['Uin'], uin))
|
||||
else:
|
||||
if '@@' in username:
|
||||
core.storageClass.updateLock.release()
|
||||
update_chatroom(core, username)
|
||||
core.storageClass.updateLock.acquire()
|
||||
newChatroomDict = utils.search_dict_list(
|
||||
core.chatroomList, 'UserName', username)
|
||||
if newChatroomDict is None:
|
||||
newChatroomDict = utils.struct_friend_info({
|
||||
'UserName': username,
|
||||
'Uin': uin,
|
||||
'Self': copy.deepcopy(core.loginInfo['User'])})
|
||||
core.chatroomList.append(newChatroomDict)
|
||||
else:
|
||||
newChatroomDict['Uin'] = uin
|
||||
elif '@' in username:
|
||||
core.storageClass.updateLock.release()
|
||||
update_friend(core, username)
|
||||
core.storageClass.updateLock.acquire()
|
||||
newFriendDict = utils.search_dict_list(
|
||||
core.memberList, 'UserName', username)
|
||||
if newFriendDict is None:
|
||||
newFriendDict = utils.struct_friend_info({
|
||||
'UserName': username,
|
||||
'Uin': uin, })
|
||||
core.memberList.append(newFriendDict)
|
||||
else:
|
||||
newFriendDict['Uin'] = uin
|
||||
usernameChangedList.append(username)
|
||||
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||
else:
|
||||
logger.debug('Wrong length of uins & usernames: %s, %s' % (
|
||||
len(uins), len(usernames)))
|
||||
else:
|
||||
logger.debug('No uins in 51 message')
|
||||
logger.debug(msg['Content'])
|
||||
return r
|
||||
|
||||
|
||||
def get_contact(self, update=False):
|
||||
if not update:
|
||||
return utils.contact_deep_copy(self, self.chatroomList)
|
||||
|
||||
def _get_contact(seq=0):
|
||||
url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'],
|
||||
int(time.time()), seq, self.loginInfo['skey'])
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT, }
|
||||
try:
|
||||
r = self.s.get(url, headers=headers)
|
||||
except:
|
||||
logger.info(
|
||||
'Failed to fetch contact, that may because of the amount of your chatrooms')
|
||||
for chatroom in self.get_chatrooms():
|
||||
self.update_chatroom(chatroom['UserName'], detailedMember=True)
|
||||
return 0, []
|
||||
j = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
return j.get('Seq', 0), j.get('MemberList')
|
||||
seq, memberList = 0, []
|
||||
while 1:
|
||||
seq, batchMemberList = _get_contact(seq)
|
||||
memberList.extend(batchMemberList)
|
||||
if seq == 0:
|
||||
break
|
||||
chatroomList, otherList = [], []
|
||||
for m in memberList:
|
||||
if m['Sex'] != 0:
|
||||
otherList.append(m)
|
||||
elif '@@' in m['UserName']:
|
||||
chatroomList.append(m)
|
||||
elif '@' in m['UserName']:
|
||||
# mp will be dealt in update_local_friends as well
|
||||
otherList.append(m)
|
||||
if chatroomList:
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
if otherList:
|
||||
update_local_friends(self, otherList)
|
||||
return utils.contact_deep_copy(self, chatroomList)
|
||||
|
||||
|
||||
def get_friends(self, update=False):
|
||||
if update:
|
||||
self.get_contact(update=True)
|
||||
return utils.contact_deep_copy(self, self.memberList)
|
||||
|
||||
|
||||
def get_chatrooms(self, update=False, contactOnly=False):
|
||||
if contactOnly:
|
||||
return self.get_contact(update=True)
|
||||
else:
|
||||
if update:
|
||||
self.get_contact(True)
|
||||
return utils.contact_deep_copy(self, self.chatroomList)
|
||||
|
||||
|
||||
def get_mps(self, update=False):
|
||||
if update:
|
||||
self.get_contact(update=True)
|
||||
return utils.contact_deep_copy(self, self.mpList)
|
||||
|
||||
|
||||
def set_alias(self, userName, alias):
|
||||
oldFriendInfo = utils.search_dict_list(
|
||||
self.memberList, 'UserName', userName)
|
||||
if oldFriendInfo is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1001, }})
|
||||
url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'UserName': userName,
|
||||
'CmdId': 2,
|
||||
'RemarkName': alias,
|
||||
'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'),
|
||||
headers=headers)
|
||||
r = ReturnValue(rawResponse=r)
|
||||
if r:
|
||||
oldFriendInfo['RemarkName'] = alias
|
||||
return r
|
||||
|
||||
|
||||
def set_pinned(self, userName, isPinned=True):
|
||||
url = '%s/webwxoplog?pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'UserName': userName,
|
||||
'CmdId': 3,
|
||||
'OP': int(isPinned),
|
||||
'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, json=data, headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def accept_friend(self, userName, v4='', autoUpdate=True):
|
||||
url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}"
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Opcode': 3, # 3
|
||||
'VerifyUserListSize': 1,
|
||||
'VerifyUserList': [{
|
||||
'Value': userName,
|
||||
'VerifyUserTicket': v4, }],
|
||||
'VerifyContent': '',
|
||||
'SceneListCount': 1,
|
||||
'SceneList': [33],
|
||||
'skey': self.loginInfo['skey'], }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace'))
|
||||
if autoUpdate:
|
||||
self.update_friend(userName)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||
''' get head image
|
||||
* if you want to get chatroom header: only set chatroomUserName
|
||||
* if you want to get friend header: only set userName
|
||||
* if you want to get chatroom member header: set both
|
||||
'''
|
||||
params = {
|
||||
'userName': userName or chatroomUserName or self.storageClass.userName,
|
||||
'skey': self.loginInfo['skey'],
|
||||
'type': 'big', }
|
||||
url = '%s/webwxgeticon' % self.loginInfo['url']
|
||||
if chatroomUserName is None:
|
||||
infoDict = self.storageClass.search_friends(userName=userName)
|
||||
if infoDict is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No friend found',
|
||||
'Ret': -1001, }})
|
||||
else:
|
||||
if userName is None:
|
||||
url = '%s/webwxgetheadimg' % self.loginInfo['url']
|
||||
else:
|
||||
chatroom = self.storageClass.search_chatrooms(
|
||||
userName=chatroomUserName)
|
||||
if chatroomUserName is None:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No chatroom found',
|
||||
'Ret': -1001, }})
|
||||
if 'EncryChatRoomId' in chatroom:
|
||||
params['chatroomid'] = chatroom['EncryChatRoomId']
|
||||
params['chatroomid'] = params.get(
|
||||
'chatroomid') or chatroom['UserName']
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, stream=True, headers=headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if picDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(picDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
tempStorage.seek(0)
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, },
|
||||
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||
|
||||
|
||||
def create_chatroom(self, memberList, topic=''):
|
||||
url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time()))
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'MemberCount': len(memberList.split(',')),
|
||||
'MemberList': [{'UserName': member} for member in memberList.split(',')],
|
||||
'Topic': topic, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def set_chatroom_name(self, chatroomUserName, name):
|
||||
url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName': chatroomUserName,
|
||||
'NewTopic': name, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||
url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName': chatroomUserName,
|
||||
'DelMemberList': ','.join([member['UserName'] for member in memberList]), }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||
useInvitation=False):
|
||||
''' add or invite member into chatroom
|
||||
* there are two ways to get members into chatroom: invite or directly add
|
||||
* but for chatrooms with more than 40 users, you can only use invite
|
||||
* but don't worry we will auto-force userInvitation for you when necessary
|
||||
'''
|
||||
if not useInvitation:
|
||||
chatroom = self.storageClass.search_chatrooms(
|
||||
userName=chatroomUserName)
|
||||
if not chatroom:
|
||||
chatroom = self.update_chatroom(chatroomUserName)
|
||||
if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']:
|
||||
useInvitation = True
|
||||
if useInvitation:
|
||||
fun, memberKeyName = 'invitemember', 'InviteMemberList'
|
||||
else:
|
||||
fun, memberKeyName = 'addmember', 'AddMemberList'
|
||||
url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], fun, self.loginInfo['pass_ticket'])
|
||||
params = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'ChatRoomName': chatroomUserName,
|
||||
memberKeyName: memberList, }
|
||||
headers = {
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, data=json.dumps(params), headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
102
lib/itchat/components/hotreload.py
Normal file
102
lib/itchat/components/hotreload.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pickle, os
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from ..config import VERSION
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import templates
|
||||
from .contact import update_local_chatrooms, update_local_friends
|
||||
from .messages import produce_msg
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_hotreload(core):
|
||||
core.dump_login_status = dump_login_status
|
||||
core.load_login_status = load_login_status
|
||||
|
||||
def dump_login_status(self, fileDir=None):
|
||||
fileDir = fileDir or self.hotReloadDir
|
||||
try:
|
||||
with open(fileDir, 'w') as f:
|
||||
f.write('itchat - DELETE THIS')
|
||||
os.remove(fileDir)
|
||||
except:
|
||||
raise Exception('Incorrect fileDir')
|
||||
status = {
|
||||
'version' : VERSION,
|
||||
'loginInfo' : self.loginInfo,
|
||||
'cookies' : self.s.cookies.get_dict(),
|
||||
'storage' : self.storageClass.dumps()}
|
||||
with open(fileDir, 'wb') as f:
|
||||
pickle.dump(status, f)
|
||||
logger.debug('Dump login status for hot reload successfully.')
|
||||
|
||||
def load_login_status(self, fileDir,
|
||||
loginCallback=None, exitCallback=None):
|
||||
try:
|
||||
with open(fileDir, 'rb') as f:
|
||||
j = pickle.load(f)
|
||||
except Exception as e:
|
||||
logger.debug('No such file, loading login status failed.')
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No such file, loading login status failed.',
|
||||
'Ret': -1002, }})
|
||||
|
||||
if j.get('version', '') != VERSION:
|
||||
logger.debug(('you have updated itchat from %s to %s, ' +
|
||||
'so cached status is ignored') % (
|
||||
j.get('version', 'old version'), VERSION))
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'cached status ignored because of version',
|
||||
'Ret': -1005, }})
|
||||
self.loginInfo = j['loginInfo']
|
||||
self.loginInfo['User'] = templates.User(self.loginInfo['User'])
|
||||
self.loginInfo['User'].core = self
|
||||
self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies'])
|
||||
self.storageClass.loads(j['storage'])
|
||||
try:
|
||||
msgList, contactList = self.get_msg()
|
||||
except:
|
||||
msgList = contactList = None
|
||||
if (msgList or contactList) is None:
|
||||
self.logout()
|
||||
load_last_login_status(self.s, j['cookies'])
|
||||
logger.debug('server refused, loading login status failed.')
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'server refused, loading login status failed.',
|
||||
'Ret': -1003, }})
|
||||
else:
|
||||
if contactList:
|
||||
for contact in contactList:
|
||||
if '@@' in contact['UserName']:
|
||||
update_local_chatrooms(self, [contact])
|
||||
else:
|
||||
update_local_friends(self, [contact])
|
||||
if msgList:
|
||||
msgList = produce_msg(self, msgList)
|
||||
for msg in msgList: self.msgList.put(msg)
|
||||
self.start_receiving(exitCallback)
|
||||
logger.debug('loading login status succeeded.')
|
||||
if hasattr(loginCallback, '__call__'):
|
||||
loginCallback()
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'loading login status succeeded.',
|
||||
'Ret': 0, }})
|
||||
|
||||
def load_last_login_status(session, cookiesDict):
|
||||
try:
|
||||
session.cookies = requests.utils.cookiejar_from_dict({
|
||||
'webwxuvid': cookiesDict['webwxuvid'],
|
||||
'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'],
|
||||
'login_frequency': '2',
|
||||
'last_wxuin': cookiesDict['wxuin'],
|
||||
'wxloadtime': cookiesDict['wxloadtime'] + '_expired',
|
||||
'wxpluginkey': cookiesDict['wxloadtime'],
|
||||
'wxuin': cookiesDict['wxuin'],
|
||||
'mm_lang': 'zh_CN',
|
||||
'MM_WX_NOTIFY_STATE': '1',
|
||||
'MM_WX_SOUND_STATE': '1', })
|
||||
except:
|
||||
logger.info('Load status for push login failed, we may have experienced a cookies change.')
|
||||
logger.info('If you are using the newest version of itchat, you may report a bug.')
|
||||
411
lib/itchat/components/login.py
Normal file
411
lib/itchat/components/login.py
Normal file
@@ -0,0 +1,411 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import io
|
||||
import threading
|
||||
import json
|
||||
import xml.dom.minidom
|
||||
import random
|
||||
import traceback
|
||||
import logging
|
||||
try:
|
||||
from httplib import BadStatusLine
|
||||
except ImportError:
|
||||
from http.client import BadStatusLine
|
||||
|
||||
import requests
|
||||
from pyqrcode import QRCode
|
||||
|
||||
from .. import config, utils
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage.templates import wrap_user_dict
|
||||
from .contact import update_local_chatrooms, update_local_friends
|
||||
from .messages import produce_msg
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
|
||||
def load_login(core):
|
||||
core.login = login
|
||||
core.get_QRuuid = get_QRuuid
|
||||
core.get_QR = get_QR
|
||||
core.check_login = check_login
|
||||
core.web_init = web_init
|
||||
core.show_mobile_login = show_mobile_login
|
||||
core.start_receiving = start_receiving
|
||||
core.get_msg = get_msg
|
||||
core.logout = logout
|
||||
|
||||
|
||||
def login(self, enableCmdQR=False, picDir=None, qrCallback=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
if self.alive or self.isLogging:
|
||||
logger.warning('itchat has already logged in.')
|
||||
return
|
||||
self.isLogging = True
|
||||
while self.isLogging:
|
||||
uuid = push_login(self)
|
||||
if uuid:
|
||||
qrStorage = io.BytesIO()
|
||||
else:
|
||||
logger.info('Getting uuid of QR code.')
|
||||
while not self.get_QRuuid():
|
||||
time.sleep(1)
|
||||
logger.info('Downloading QR code.')
|
||||
qrStorage = self.get_QR(enableCmdQR=enableCmdQR,
|
||||
picDir=picDir, qrCallback=qrCallback)
|
||||
# logger.info('Please scan the QR code to log in.')
|
||||
isLoggedIn = False
|
||||
while not isLoggedIn:
|
||||
status = self.check_login()
|
||||
if hasattr(qrCallback, '__call__'):
|
||||
qrCallback(uuid=self.uuid, status=status,
|
||||
qrcode=qrStorage.getvalue())
|
||||
if status == '200':
|
||||
isLoggedIn = True
|
||||
elif status == '201':
|
||||
if isLoggedIn is not None:
|
||||
logger.info('Please press confirm on your phone.')
|
||||
isLoggedIn = None
|
||||
time.sleep(7)
|
||||
time.sleep(0.5)
|
||||
elif status != '408':
|
||||
break
|
||||
if isLoggedIn:
|
||||
break
|
||||
elif self.isLogging:
|
||||
logger.info('Log in time out, reloading QR code.')
|
||||
else:
|
||||
return # log in process is stopped by user
|
||||
logger.info('Loading the contact, this may take a little while.')
|
||||
self.web_init()
|
||||
self.show_mobile_login()
|
||||
self.get_contact(True)
|
||||
if hasattr(loginCallback, '__call__'):
|
||||
r = loginCallback()
|
||||
else:
|
||||
utils.clear_screen()
|
||||
if os.path.exists(picDir or config.DEFAULT_QR):
|
||||
os.remove(picDir or config.DEFAULT_QR)
|
||||
logger.info('Login successfully as %s' % self.storageClass.nickName)
|
||||
self.start_receiving(exitCallback)
|
||||
self.isLogging = False
|
||||
|
||||
|
||||
def push_login(core):
|
||||
cookiesDict = core.s.cookies.get_dict()
|
||||
if 'wxuin' in cookiesDict:
|
||||
url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % (
|
||||
config.BASE_URL, cookiesDict['wxuin'])
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = core.s.get(url, headers=headers).json()
|
||||
if 'uuid' in r and r.get('ret') in (0, '0'):
|
||||
core.uuid = r['uuid']
|
||||
return r['uuid']
|
||||
return False
|
||||
|
||||
|
||||
def get_QRuuid(self):
|
||||
url = '%s/jslogin' % config.BASE_URL
|
||||
params = {
|
||||
'appid': 'wx782c26e4c19acffb',
|
||||
'fun': 'new',
|
||||
'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
|
||||
'lang': 'zh_CN'}
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, headers=headers)
|
||||
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";'
|
||||
data = re.search(regx, r.text)
|
||||
if data and data.group(1) == '200':
|
||||
self.uuid = data.group(2)
|
||||
return self.uuid
|
||||
|
||||
|
||||
def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||
uuid = uuid or self.uuid
|
||||
picDir = picDir or config.DEFAULT_QR
|
||||
qrStorage = io.BytesIO()
|
||||
qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid)
|
||||
qrCode.png(qrStorage, scale=10)
|
||||
if hasattr(qrCallback, '__call__'):
|
||||
qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue())
|
||||
else:
|
||||
with open(picDir, 'wb') as f:
|
||||
f.write(qrStorage.getvalue())
|
||||
if enableCmdQR:
|
||||
utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR)
|
||||
else:
|
||||
utils.print_qr(picDir)
|
||||
return qrStorage
|
||||
|
||||
|
||||
def check_login(self, uuid=None):
|
||||
uuid = uuid or self.uuid
|
||||
url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL
|
||||
localTime = int(time.time())
|
||||
params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % (
|
||||
uuid, int(-localTime / 1579), localTime)
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
r = self.s.get(url, params=params, headers=headers)
|
||||
regx = r'window.code=(\d+)'
|
||||
data = re.search(regx, r.text)
|
||||
if data and data.group(1) == '200':
|
||||
if process_login_info(self, r.text):
|
||||
return '200'
|
||||
else:
|
||||
return '400'
|
||||
elif data:
|
||||
return data.group(1)
|
||||
else:
|
||||
return '400'
|
||||
|
||||
|
||||
def process_login_info(core, loginContent):
|
||||
''' when finish login (scanning qrcode)
|
||||
* syncUrl and fileUploadingUrl will be fetched
|
||||
* deviceid and msgid will be generated
|
||||
* skey, wxsid, wxuin, pass_ticket will be fetched
|
||||
'''
|
||||
regx = r'window.redirect_uri="(\S+)";'
|
||||
core.loginInfo['url'] = re.search(regx, loginContent).group(1)
|
||||
headers = {'User-Agent': config.USER_AGENT,
|
||||
'client-version': config.UOS_PATCH_CLIENT_VERSION,
|
||||
'extspam': config.UOS_PATCH_EXTSPAM,
|
||||
'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t'
|
||||
}
|
||||
r = core.s.get(core.loginInfo['url'],
|
||||
headers=headers, allow_redirects=False)
|
||||
core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind(
|
||||
'/')]
|
||||
for indexUrl, detailedUrl in (
|
||||
("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")),
|
||||
("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")),
|
||||
("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")),
|
||||
("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")),
|
||||
("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))):
|
||||
fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' %
|
||||
url for url in detailedUrl]
|
||||
if indexUrl in core.loginInfo['url']:
|
||||
core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \
|
||||
fileUrl, syncUrl
|
||||
break
|
||||
else:
|
||||
core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url']
|
||||
core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||
core.loginInfo['logintime'] = int(time.time() * 1e3)
|
||||
core.loginInfo['BaseRequest'] = {}
|
||||
cookies = core.s.cookies.get_dict()
|
||||
skey = re.findall('<skey>(.*?)</skey>', r.text, re.S)[0]
|
||||
pass_ticket = re.findall(
|
||||
'<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)[0]
|
||||
core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
|
||||
core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
|
||||
core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
|
||||
core.loginInfo['pass_ticket'] = pass_ticket
|
||||
# A question : why pass_ticket == DeviceID ?
|
||||
# deviceID is only a randomly generated number
|
||||
|
||||
# UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
|
||||
# for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
|
||||
# if node.nodeName == 'skey':
|
||||
# core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'wxsid':
|
||||
# core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'wxuin':
|
||||
# core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
|
||||
# elif node.nodeName == 'pass_ticket':
|
||||
# core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
|
||||
if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
|
||||
logger.error(
|
||||
'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
|
||||
core.isLogging = False
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def web_init(self):
|
||||
url = '%s/webwxinit' % self.loginInfo['url']
|
||||
params = {
|
||||
'r': int(-time.time() / 1579),
|
||||
'pass_ticket': self.loginInfo['pass_ticket'], }
|
||||
data = {'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT, }
|
||||
r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
|
||||
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
# deal with login info
|
||||
utils.emoji_formatter(dic['User'], 'NickName')
|
||||
self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
|
||||
self.loginInfo['User'] = wrap_user_dict(
|
||||
utils.struct_friend_info(dic['User']))
|
||||
self.memberList.append(self.loginInfo['User'])
|
||||
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||
for item in dic['SyncKey']['List']])
|
||||
self.storageClass.userName = dic['User']['UserName']
|
||||
self.storageClass.nickName = dic['User']['NickName']
|
||||
# deal with contact list returned when init
|
||||
contactList = dic.get('ContactList', [])
|
||||
chatroomList, otherList = [], []
|
||||
for m in contactList:
|
||||
if m['Sex'] != 0:
|
||||
otherList.append(m)
|
||||
elif '@@' in m['UserName']:
|
||||
m['MemberList'] = [] # don't let dirty info pollute the list
|
||||
chatroomList.append(m)
|
||||
elif '@' in m['UserName']:
|
||||
# mp will be dealt in update_local_friends as well
|
||||
otherList.append(m)
|
||||
if chatroomList:
|
||||
update_local_chatrooms(self, chatroomList)
|
||||
if otherList:
|
||||
update_local_friends(self, otherList)
|
||||
return dic
|
||||
|
||||
|
||||
def show_mobile_login(self):
|
||||
url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Code': 3,
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': self.storageClass.userName,
|
||||
'ClientMsgId': int(time.time()), }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT, }
|
||||
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
|
||||
def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||
self.alive = True
|
||||
|
||||
def maintain_loop():
|
||||
retryCount = 0
|
||||
while self.alive:
|
||||
try:
|
||||
i = sync_check(self)
|
||||
if i is None:
|
||||
self.alive = False
|
||||
elif i == '0':
|
||||
pass
|
||||
else:
|
||||
msgList, contactList = self.get_msg()
|
||||
if msgList:
|
||||
msgList = produce_msg(self, msgList)
|
||||
for msg in msgList:
|
||||
self.msgList.put(msg)
|
||||
if contactList:
|
||||
chatroomList, otherList = [], []
|
||||
for contact in contactList:
|
||||
if '@@' in contact['UserName']:
|
||||
chatroomList.append(contact)
|
||||
else:
|
||||
otherList.append(contact)
|
||||
chatroomMsg = update_local_chatrooms(
|
||||
self, chatroomList)
|
||||
chatroomMsg['User'] = self.loginInfo['User']
|
||||
self.msgList.put(chatroomMsg)
|
||||
update_local_friends(self, otherList)
|
||||
retryCount = 0
|
||||
except requests.exceptions.ReadTimeout:
|
||||
pass
|
||||
except:
|
||||
retryCount += 1
|
||||
logger.error(traceback.format_exc())
|
||||
if self.receivingRetryCount < retryCount:
|
||||
self.alive = False
|
||||
else:
|
||||
time.sleep(1)
|
||||
self.logout()
|
||||
if hasattr(exitCallback, '__call__'):
|
||||
exitCallback()
|
||||
else:
|
||||
logger.info('LOG OUT!')
|
||||
if getReceivingFnOnly:
|
||||
return maintain_loop
|
||||
else:
|
||||
maintainThread = threading.Thread(target=maintain_loop)
|
||||
maintainThread.setDaemon(True)
|
||||
maintainThread.start()
|
||||
|
||||
|
||||
def sync_check(self):
|
||||
url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
|
||||
params = {
|
||||
'r': int(time.time() * 1000),
|
||||
'skey': self.loginInfo['skey'],
|
||||
'sid': self.loginInfo['wxsid'],
|
||||
'uin': self.loginInfo['wxuin'],
|
||||
'deviceid': self.loginInfo['deviceid'],
|
||||
'synckey': self.loginInfo['synckey'],
|
||||
'_': self.loginInfo['logintime'], }
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
self.loginInfo['logintime'] += 1
|
||||
try:
|
||||
r = self.s.get(url, params=params, headers=headers,
|
||||
timeout=config.TIMEOUT)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
try:
|
||||
if not isinstance(e.args[0].args[1], BadStatusLine):
|
||||
raise
|
||||
# will return a package with status '0 -'
|
||||
# and value like:
|
||||
# 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93
|
||||
# seems like status of typing, but before I make further achievement code will remain like this
|
||||
return '2'
|
||||
except:
|
||||
raise
|
||||
r.raise_for_status()
|
||||
regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
|
||||
pm = re.search(regx, r.text)
|
||||
if pm is None or pm.group(1) != '0':
|
||||
logger.debug('Unexpected sync check result: %s' % r.text)
|
||||
return None
|
||||
return pm.group(2)
|
||||
|
||||
|
||||
def get_msg(self):
|
||||
self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||
url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['wxsid'],
|
||||
self.loginInfo['skey'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'SyncKey': self.loginInfo['SyncKey'],
|
||||
'rr': ~int(time.time()), }
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent': config.USER_AGENT}
|
||||
r = self.s.post(url, data=json.dumps(data),
|
||||
headers=headers, timeout=config.TIMEOUT)
|
||||
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||
if dic['BaseResponse']['Ret'] != 0:
|
||||
return None, None
|
||||
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||
for item in dic['SyncCheckKey']['List']])
|
||||
return dic['AddMsgList'], dic['ModContactList']
|
||||
|
||||
|
||||
def logout(self):
|
||||
if self.alive:
|
||||
url = '%s/webwxlogout' % self.loginInfo['url']
|
||||
params = {
|
||||
'redirect': 1,
|
||||
'type': 1,
|
||||
'skey': self.loginInfo['skey'], }
|
||||
headers = {'User-Agent': config.USER_AGENT}
|
||||
self.s.get(url, params=params, headers=headers)
|
||||
self.alive = False
|
||||
self.isLogging = False
|
||||
self.s.cookies.clear()
|
||||
del self.chatroomList[:]
|
||||
del self.memberList[:]
|
||||
del self.mpList[:]
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'logout successfully.',
|
||||
'Ret': 0, }})
|
||||
528
lib/itchat/components/messages.py
Normal file
528
lib/itchat/components/messages.py
Normal file
@@ -0,0 +1,528 @@
|
||||
import os, time, re, io
|
||||
import json
|
||||
import mimetypes, hashlib
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
import requests
|
||||
|
||||
from .. import config, utils
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..storage import templates
|
||||
from .contact import update_local_uin
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_messages(core):
|
||||
core.send_raw_msg = send_raw_msg
|
||||
core.send_msg = send_msg
|
||||
core.upload_file = upload_file
|
||||
core.send_file = send_file
|
||||
core.send_image = send_image
|
||||
core.send_video = send_video
|
||||
core.send = send
|
||||
core.revoke = revoke
|
||||
|
||||
def get_download_fn(core, url, msgId):
|
||||
def download_fn(downloadDir=None):
|
||||
params = {
|
||||
'msgid': msgId,
|
||||
'skey': core.loginInfo['skey'],}
|
||||
headers = { 'User-Agent' : config.USER_AGENT }
|
||||
r = core.s.get(url, params=params, stream=True, headers = headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if downloadDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(downloadDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
tempStorage.seek(0)
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, },
|
||||
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||
return download_fn
|
||||
|
||||
def produce_msg(core, msgList):
|
||||
''' for messages types
|
||||
* 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg
|
||||
* 53 webwxvoipnotifymsg, 9999 sysnotice
|
||||
'''
|
||||
rl = []
|
||||
srl = [40, 43, 50, 52, 53, 9999]
|
||||
for m in msgList:
|
||||
# get actual opposite
|
||||
if m['FromUserName'] == core.storageClass.userName:
|
||||
actualOpposite = m['ToUserName']
|
||||
else:
|
||||
actualOpposite = m['FromUserName']
|
||||
# produce basic message
|
||||
if '@@' in m['FromUserName'] or '@@' in m['ToUserName']:
|
||||
produce_group_chat(core, m)
|
||||
else:
|
||||
utils.msg_formatter(m, 'Content')
|
||||
# set user of msg
|
||||
if '@@' in actualOpposite:
|
||||
m['User'] = core.search_chatrooms(userName=actualOpposite) or \
|
||||
templates.Chatroom({'UserName': actualOpposite})
|
||||
# we don't need to update chatroom here because we have
|
||||
# updated once when producing basic message
|
||||
elif actualOpposite in ('filehelper', 'fmessage'):
|
||||
m['User'] = templates.User({'UserName': actualOpposite})
|
||||
else:
|
||||
m['User'] = core.search_mps(userName=actualOpposite) or \
|
||||
core.search_friends(userName=actualOpposite) or \
|
||||
templates.User(userName=actualOpposite)
|
||||
# by default we think there may be a user missing not a mp
|
||||
m['User'].core = core
|
||||
if m['MsgType'] == 1: # words
|
||||
if m['Url']:
|
||||
regx = r'(.+?\(.+?\))'
|
||||
data = re.search(regx, m['Content'])
|
||||
data = 'Map' if data is None else data.group(1)
|
||||
msg = {
|
||||
'Type': 'Map',
|
||||
'Text': data,}
|
||||
else:
|
||||
msg = {
|
||||
'Type': 'Text',
|
||||
'Text': m['Content'],}
|
||||
elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type' : 'Picture',
|
||||
'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'png' if m['MsgType'] == 3 else 'gif'),
|
||||
'Text' : download_fn, }
|
||||
elif m['MsgType'] == 34: # voice
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type': 'Recording',
|
||||
'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'Text': download_fn,}
|
||||
elif m['MsgType'] == 37: # friends
|
||||
m['User']['UserName'] = m['RecommendInfo']['UserName']
|
||||
msg = {
|
||||
'Type': 'Friends',
|
||||
'Text': {
|
||||
'status' : m['Status'],
|
||||
'userName' : m['RecommendInfo']['UserName'],
|
||||
'verifyContent' : m['Ticket'],
|
||||
'autoUpdate' : m['RecommendInfo'], }, }
|
||||
m['User'].verifyDict = msg['Text']
|
||||
elif m['MsgType'] == 42: # name card
|
||||
msg = {
|
||||
'Type': 'Card',
|
||||
'Text': m['RecommendInfo'], }
|
||||
elif m['MsgType'] in (43, 62): # tiny video
|
||||
msgId = m['MsgId']
|
||||
def download_video(videoDir=None):
|
||||
url = '%s/webwxgetvideo' % core.loginInfo['url']
|
||||
params = {
|
||||
'msgid': msgId,
|
||||
'skey': core.loginInfo['skey'],}
|
||||
headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT }
|
||||
r = core.s.get(url, params=params, headers=headers, stream=True)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if videoDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(videoDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, }})
|
||||
msg = {
|
||||
'Type': 'Video',
|
||||
'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||
'Text': download_video, }
|
||||
elif m['MsgType'] == 49: # sharing
|
||||
if m['AppMsgType'] == 0: # chat history
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['Content'], }
|
||||
elif m['AppMsgType'] == 6:
|
||||
rawMsg = m
|
||||
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||
def download_atta(attaDir=None):
|
||||
url = core.loginInfo['fileUrl'] + '/webwxgetmedia'
|
||||
params = {
|
||||
'sender': rawMsg['FromUserName'],
|
||||
'mediaid': rawMsg['MediaId'],
|
||||
'filename': rawMsg['FileName'],
|
||||
'fromuser': core.loginInfo['wxuin'],
|
||||
'pass_ticket': 'undefined',
|
||||
'webwx_data_ticket': cookiesList['webwx_data_ticket'],}
|
||||
headers = { 'User-Agent' : config.USER_AGENT }
|
||||
r = core.s.get(url, params=params, stream=True, headers=headers)
|
||||
tempStorage = io.BytesIO()
|
||||
for block in r.iter_content(1024):
|
||||
tempStorage.write(block)
|
||||
if attaDir is None:
|
||||
return tempStorage.getvalue()
|
||||
with open(attaDir, 'wb') as f:
|
||||
f.write(tempStorage.getvalue())
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Successfully downloaded',
|
||||
'Ret': 0, }})
|
||||
msg = {
|
||||
'Type': 'Attachment',
|
||||
'Text': download_atta, }
|
||||
elif m['AppMsgType'] == 8:
|
||||
download_fn = get_download_fn(core,
|
||||
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||
msg = {
|
||||
'Type' : 'Picture',
|
||||
'FileName' : '%s.gif' % (
|
||||
time.strftime('%y%m%d-%H%M%S', time.localtime())),
|
||||
'Text' : download_fn, }
|
||||
elif m['AppMsgType'] == 17:
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['FileName'], }
|
||||
elif m['AppMsgType'] == 2000:
|
||||
regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]'
|
||||
data = re.search(regx, m['Content'])
|
||||
if data:
|
||||
data = data.group(2).split(u'\u3002')[0]
|
||||
else:
|
||||
data = 'You may found detailed info in Content key.'
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': data, }
|
||||
else:
|
||||
msg = {
|
||||
'Type': 'Sharing',
|
||||
'Text': m['FileName'], }
|
||||
elif m['MsgType'] == 51: # phone init
|
||||
msg = update_local_uin(core, m)
|
||||
elif m['MsgType'] == 10000:
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': m['Content'],}
|
||||
elif m['MsgType'] == 10002:
|
||||
regx = r'\[CDATA\[(.+?)\]\]'
|
||||
data = re.search(regx, m['Content'])
|
||||
data = 'System message' if data is None else data.group(1).replace('\\', '')
|
||||
msg = {
|
||||
'Type': 'Note',
|
||||
'Text': data, }
|
||||
elif m['MsgType'] in srl:
|
||||
msg = {
|
||||
'Type': 'Useless',
|
||||
'Text': 'UselessMsg', }
|
||||
else:
|
||||
logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m)))
|
||||
msg = {
|
||||
'Type': 'Useless',
|
||||
'Text': 'UselessMsg', }
|
||||
m = dict(m, **msg)
|
||||
rl.append(m)
|
||||
return rl
|
||||
|
||||
def produce_group_chat(core, msg):
|
||||
r = re.match('(@[0-9a-z]*?):<br/>(.*)$', msg['Content'])
|
||||
if r:
|
||||
actualUserName, content = r.groups()
|
||||
chatroomUserName = msg['FromUserName']
|
||||
elif msg['FromUserName'] == core.storageClass.userName:
|
||||
actualUserName = core.storageClass.userName
|
||||
content = msg['Content']
|
||||
chatroomUserName = msg['ToUserName']
|
||||
else:
|
||||
msg['ActualUserName'] = core.storageClass.userName
|
||||
msg['ActualNickName'] = core.storageClass.nickName
|
||||
msg['IsAt'] = False
|
||||
utils.msg_formatter(msg, 'Content')
|
||||
return
|
||||
chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||
member = utils.search_dict_list((chatroom or {}).get(
|
||||
'MemberList') or [], 'UserName', actualUserName)
|
||||
if member is None:
|
||||
chatroom = core.update_chatroom(chatroomUserName)
|
||||
member = utils.search_dict_list((chatroom or {}).get(
|
||||
'MemberList') or [], 'UserName', actualUserName)
|
||||
if member is None:
|
||||
logger.debug('chatroom member fetch failed with %s' % actualUserName)
|
||||
msg['ActualNickName'] = ''
|
||||
msg['IsAt'] = False
|
||||
else:
|
||||
msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName']
|
||||
atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName)
|
||||
msg['IsAt'] = (
|
||||
(atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' '))
|
||||
in msg['Content'] or msg['Content'].endswith(atFlag))
|
||||
msg['ActualUserName'] = actualUserName
|
||||
msg['Content'] = content
|
||||
utils.msg_formatter(msg, 'Content')
|
||||
|
||||
def send_raw_msg(self, msgType, content, toUserName):
|
||||
url = '%s/webwxsendmsg' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': msgType,
|
||||
'Content': content,
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': (toUserName if toUserName else self.storageClass.userName),
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4),
|
||||
},
|
||||
'Scene': 0, }
|
||||
headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def send_msg(self, msg='Test Message', toUserName=None):
|
||||
logger.debug('Request to send a text message to %s: %s' % (toUserName, msg))
|
||||
r = self.send_raw_msg(1, msg, toUserName)
|
||||
return r
|
||||
|
||||
def _prepare_file(fileDir, file_=None):
|
||||
fileDict = {}
|
||||
if file_:
|
||||
if hasattr(file_, 'read'):
|
||||
file_ = file_.read()
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'file_ param should be opened file',
|
||||
'Ret': -1005, }})
|
||||
else:
|
||||
if not utils.check_file(fileDir):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No file found in specific dir',
|
||||
'Ret': -1002, }})
|
||||
with open(fileDir, 'rb') as f:
|
||||
file_ = f.read()
|
||||
fileDict['fileSize'] = len(file_)
|
||||
fileDict['fileMd5'] = hashlib.md5(file_).hexdigest()
|
||||
fileDict['file_'] = io.BytesIO(file_)
|
||||
return fileDict
|
||||
|
||||
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||
toUserName='filehelper', file_=None, preparedFile=None):
|
||||
logger.debug('Request to upload a %s: %s' % (
|
||||
'picture' if isPicture else 'video' if isVideo else 'file', fileDir))
|
||||
if not preparedFile:
|
||||
preparedFile = _prepare_file(fileDir, file_)
|
||||
if not preparedFile:
|
||||
return preparedFile
|
||||
fileSize, fileMd5, file_ = \
|
||||
preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_']
|
||||
fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc'
|
||||
chunks = int((fileSize - 1) / 524288) + 1
|
||||
clientMediaId = int(time.time() * 1e4)
|
||||
uploadMediaRequest = json.dumps(OrderedDict([
|
||||
('UploadType', 2),
|
||||
('BaseRequest', self.loginInfo['BaseRequest']),
|
||||
('ClientMediaId', clientMediaId),
|
||||
('TotalLen', fileSize),
|
||||
('StartPos', 0),
|
||||
('DataLen', fileSize),
|
||||
('MediaType', 4),
|
||||
('FromUserName', self.storageClass.userName),
|
||||
('ToUserName', toUserName),
|
||||
('FileMd5', fileMd5)]
|
||||
), separators = (',', ':'))
|
||||
r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}}
|
||||
for chunk in range(chunks):
|
||||
r = upload_chunk_file(self, fileDir, fileSymbol, fileSize,
|
||||
file_, chunk, chunks, uploadMediaRequest)
|
||||
file_.close()
|
||||
if isinstance(r, dict):
|
||||
return ReturnValue(r)
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def upload_chunk_file(core, fileDir, fileSymbol, fileSize,
|
||||
file_, chunk, chunks, uploadMediaRequest):
|
||||
url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \
|
||||
'/webwxuploadmedia?f=json'
|
||||
# save it on server
|
||||
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||
fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream'
|
||||
fileName = utils.quote(os.path.basename(fileDir))
|
||||
files = OrderedDict([
|
||||
('id', (None, 'WU_FILE_0')),
|
||||
('name', (None, fileName)),
|
||||
('type', (None, fileType)),
|
||||
('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))),
|
||||
('size', (None, str(fileSize))),
|
||||
('chunks', (None, None)),
|
||||
('chunk', (None, None)),
|
||||
('mediatype', (None, fileSymbol)),
|
||||
('uploadmediarequest', (None, uploadMediaRequest)),
|
||||
('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])),
|
||||
('pass_ticket', (None, core.loginInfo['pass_ticket'])),
|
||||
('filename' , (fileName, file_.read(524288), 'application/octet-stream'))])
|
||||
if chunks == 1:
|
||||
del files['chunk']; del files['chunks']
|
||||
else:
|
||||
files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks))
|
||||
headers = { 'User-Agent' : config.USER_AGENT }
|
||||
return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT)
|
||||
|
||||
def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a file(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if hasattr(fileDir, 'read'):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'fileDir param should not be an opened file in send_file',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
preparedFile = _prepare_file(fileDir, file_)
|
||||
if not preparedFile:
|
||||
return preparedFile
|
||||
fileSize = preparedFile['fileSize']
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, preparedFile=preparedFile)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': 6,
|
||||
'Content': ("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>%s</title>" % os.path.basename(fileDir) +
|
||||
"<des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
|
||||
"<appattach><totallen>%s</totallen><attachid>%s</attachid>" % (str(fileSize), mediaId) +
|
||||
"<fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>" % os.path.splitext(fileDir)[1].replace('.','')),
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': toUserName,
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
headers = {
|
||||
'User-Agent': config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a image(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if fileDir or file_:
|
||||
if hasattr(fileDir, 'read'):
|
||||
file_, fileDir = fileDir, None
|
||||
if fileDir is None:
|
||||
fileDir = 'tmp.jpg' # specific fileDir to send gifs
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type': 3,
|
||||
'MediaId': mediaId,
|
||||
'FromUserName': self.storageClass.userName,
|
||||
'ToUserName': toUserName,
|
||||
'LocalID': int(time.time() * 1e4),
|
||||
'ClientMsgId': int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
if fileDir[-4:] == '.gif':
|
||||
url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url']
|
||||
data['Msg']['Type'] = 47
|
||||
data['Msg']['EmojiFlag'] = 2
|
||||
headers = {
|
||||
'User-Agent': config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
logger.debug('Request to send a video(mediaId: %s) to %s: %s' % (
|
||||
mediaId, toUserName, fileDir))
|
||||
if fileDir or file_:
|
||||
if hasattr(fileDir, 'read'):
|
||||
file_, fileDir = fileDir, None
|
||||
if fileDir is None:
|
||||
fileDir = 'tmp.mp4' # specific fileDir to send other formats
|
||||
else:
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||
'Ret': -1005, }})
|
||||
if toUserName is None:
|
||||
toUserName = self.storageClass.userName
|
||||
if mediaId is None:
|
||||
r = self.upload_file(fileDir, isVideo=True, file_=file_)
|
||||
if r:
|
||||
mediaId = r['MediaId']
|
||||
else:
|
||||
return r
|
||||
url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % (
|
||||
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
'Msg': {
|
||||
'Type' : 43,
|
||||
'MediaId' : mediaId,
|
||||
'FromUserName' : self.storageClass.userName,
|
||||
'ToUserName' : toUserName,
|
||||
'LocalID' : int(time.time() * 1e4),
|
||||
'ClientMsgId' : int(time.time() * 1e4), },
|
||||
'Scene': 0, }
|
||||
headers = {
|
||||
'User-Agent' : config.USER_AGENT,
|
||||
'Content-Type': 'application/json;charset=UTF-8', }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
|
||||
def send(self, msg, toUserName=None, mediaId=None):
|
||||
if not msg:
|
||||
r = ReturnValue({'BaseResponse': {
|
||||
'ErrMsg': 'No message.',
|
||||
'Ret': -1005, }})
|
||||
elif msg[:5] == '@fil@':
|
||||
if mediaId is None:
|
||||
r = self.send_file(msg[5:], toUserName)
|
||||
else:
|
||||
r = self.send_file(msg[5:], toUserName, mediaId)
|
||||
elif msg[:5] == '@img@':
|
||||
if mediaId is None:
|
||||
r = self.send_image(msg[5:], toUserName)
|
||||
else:
|
||||
r = self.send_image(msg[5:], toUserName, mediaId)
|
||||
elif msg[:5] == '@msg@':
|
||||
r = self.send_msg(msg[5:], toUserName)
|
||||
elif msg[:5] == '@vid@':
|
||||
if mediaId is None:
|
||||
r = self.send_video(msg[5:], toUserName)
|
||||
else:
|
||||
r = self.send_video(msg[5:], toUserName, mediaId)
|
||||
else:
|
||||
r = self.send_msg(msg, toUserName)
|
||||
return r
|
||||
|
||||
def revoke(self, msgId, toUserName, localId=None):
|
||||
url = '%s/webwxrevokemsg' % self.loginInfo['url']
|
||||
data = {
|
||||
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||
"ClientMsgId": localId or str(time.time() * 1e3),
|
||||
"SvrMsgId": msgId,
|
||||
"ToUserName": toUserName}
|
||||
headers = {
|
||||
'ContentType': 'application/json; charset=UTF-8',
|
||||
'User-Agent' : config.USER_AGENT }
|
||||
r = self.s.post(url, headers=headers,
|
||||
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||
return ReturnValue(rawResponse=r)
|
||||
103
lib/itchat/components/register.py
Normal file
103
lib/itchat/components/register.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import logging, traceback, sys, threading
|
||||
try:
|
||||
import Queue
|
||||
except ImportError:
|
||||
import queue as Queue
|
||||
|
||||
from ..log import set_logging
|
||||
from ..utils import test_connect
|
||||
from ..storage import templates
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
def load_register(core):
|
||||
core.auto_login = auto_login
|
||||
core.configured_reply = configured_reply
|
||||
core.msg_register = msg_register
|
||||
core.run = run
|
||||
|
||||
def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl',
|
||||
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
if not test_connect():
|
||||
logger.info("You can't get access to internet or wechat domain, so exit.")
|
||||
sys.exit()
|
||||
self.useHotReload = hotReload
|
||||
self.hotReloadDir = statusStorageDir
|
||||
if hotReload:
|
||||
if self.load_login_status(statusStorageDir,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback):
|
||||
return
|
||||
self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||
self.dump_login_status(statusStorageDir)
|
||||
else:
|
||||
self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback,
|
||||
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||
|
||||
def configured_reply(self):
|
||||
''' determine the type of message and reply if its method is defined
|
||||
however, I use a strange way to determine whether a msg is from massive platform
|
||||
I haven't found a better solution here
|
||||
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||
'''
|
||||
try:
|
||||
msg = self.msgList.get(timeout=1)
|
||||
except Queue.Empty:
|
||||
pass
|
||||
else:
|
||||
if isinstance(msg['User'], templates.User):
|
||||
replyFn = self.functionDict['FriendChat'].get(msg['Type'])
|
||||
elif isinstance(msg['User'], templates.MassivePlatform):
|
||||
replyFn = self.functionDict['MpChat'].get(msg['Type'])
|
||||
elif isinstance(msg['User'], templates.Chatroom):
|
||||
replyFn = self.functionDict['GroupChat'].get(msg['Type'])
|
||||
if replyFn is None:
|
||||
r = None
|
||||
else:
|
||||
try:
|
||||
r = replyFn(msg)
|
||||
if r is not None:
|
||||
self.send(r, msg.get('FromUserName'))
|
||||
except:
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||
''' a decorator constructor
|
||||
return a specific decorator based on information given '''
|
||||
if not (isinstance(msgType, list) or isinstance(msgType, tuple)):
|
||||
msgType = [msgType]
|
||||
def _msg_register(fn):
|
||||
for _msgType in msgType:
|
||||
if isFriendChat:
|
||||
self.functionDict['FriendChat'][_msgType] = fn
|
||||
if isGroupChat:
|
||||
self.functionDict['GroupChat'][_msgType] = fn
|
||||
if isMpChat:
|
||||
self.functionDict['MpChat'][_msgType] = fn
|
||||
if not any((isFriendChat, isGroupChat, isMpChat)):
|
||||
self.functionDict['FriendChat'][_msgType] = fn
|
||||
return fn
|
||||
return _msg_register
|
||||
|
||||
def run(self, debug=False, blockThread=True):
|
||||
logger.info('Start auto replying.')
|
||||
if debug:
|
||||
set_logging(loggingLevel=logging.DEBUG)
|
||||
def reply_fn():
|
||||
try:
|
||||
while self.alive:
|
||||
self.configured_reply()
|
||||
except KeyboardInterrupt:
|
||||
if self.useHotReload:
|
||||
self.dump_login_status()
|
||||
self.alive = False
|
||||
logger.debug('itchat received an ^C and exit.')
|
||||
logger.info('Bye~')
|
||||
if blockThread:
|
||||
reply_fn()
|
||||
else:
|
||||
replyThread = threading.Thread(target=reply_fn)
|
||||
replyThread.setDaemon(True)
|
||||
replyThread.start()
|
||||
17
lib/itchat/config.py
Normal file
17
lib/itchat/config.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import os, platform
|
||||
|
||||
VERSION = '1.5.0.dev'
|
||||
|
||||
# use this envrionment to initialize the async & sync componment
|
||||
ASYNC_COMPONENTS = os.environ.get('ITCHAT_UOS_ASYNC', False)
|
||||
|
||||
BASE_URL = 'https://login.weixin.qq.com'
|
||||
OS = platform.system() # Windows, Linux, Darwin
|
||||
DIR = os.getcwd()
|
||||
DEFAULT_QR = 'QR.png'
|
||||
TIMEOUT = (10, 60)
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
|
||||
|
||||
UOS_PATCH_CLIENT_VERSION = '2.0.0'
|
||||
UOS_PATCH_EXTSPAM = 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA=='
|
||||
14
lib/itchat/content.py
Normal file
14
lib/itchat/content.py
Normal file
@@ -0,0 +1,14 @@
|
||||
TEXT = 'Text'
|
||||
MAP = 'Map'
|
||||
CARD = 'Card'
|
||||
NOTE = 'Note'
|
||||
SHARING = 'Sharing'
|
||||
PICTURE = 'Picture'
|
||||
RECORDING = VOICE = 'Recording'
|
||||
ATTACHMENT = 'Attachment'
|
||||
VIDEO = 'Video'
|
||||
FRIENDS = 'Friends'
|
||||
SYSTEM = 'System'
|
||||
|
||||
INCOME_MSG = [TEXT, MAP, CARD, NOTE, SHARING, PICTURE,
|
||||
RECORDING, VOICE, ATTACHMENT, VIDEO, FRIENDS, SYSTEM]
|
||||
456
lib/itchat/core.py
Normal file
456
lib/itchat/core.py
Normal file
@@ -0,0 +1,456 @@
|
||||
import requests
|
||||
|
||||
from . import storage
|
||||
|
||||
class Core(object):
|
||||
def __init__(self):
|
||||
''' init is the only method defined in core.py
|
||||
alive is value showing whether core is running
|
||||
- you should call logout method to change it
|
||||
- after logout, a core object can login again
|
||||
storageClass only uses basic python types
|
||||
- so for advanced uses, inherit it yourself
|
||||
receivingRetryCount is for receiving loop retry
|
||||
- it's 5 now, but actually even 1 is enough
|
||||
- failing is failing
|
||||
'''
|
||||
self.alive, self.isLogging = False, False
|
||||
self.storageClass = storage.Storage(self)
|
||||
self.memberList = self.storageClass.memberList
|
||||
self.mpList = self.storageClass.mpList
|
||||
self.chatroomList = self.storageClass.chatroomList
|
||||
self.msgList = self.storageClass.msgList
|
||||
self.loginInfo = {}
|
||||
self.s = requests.Session()
|
||||
self.uuid = None
|
||||
self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}}
|
||||
self.useHotReload, self.hotReloadDir = False, 'itchat.pkl'
|
||||
self.receivingRetryCount = 5
|
||||
def login(self, enableCmdQR=False, picDir=None, qrCallback=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
''' log in like web wechat does
|
||||
for log in
|
||||
- a QR code will be downloaded and opened
|
||||
- then scanning status is logged, it paused for you confirm
|
||||
- finally it logged in and show your nickName
|
||||
for options
|
||||
- enableCmdQR: show qrcode in command line
|
||||
- integers can be used to fit strange char length
|
||||
- picDir: place for storing qrcode
|
||||
- qrCallback: method that should accept uuid, status, qrcode
|
||||
- loginCallback: callback after successfully logged in
|
||||
- if not set, screen is cleared and qrcode is deleted
|
||||
- exitCallback: callback after logged out
|
||||
- it contains calling of logout
|
||||
for usage
|
||||
..code::python
|
||||
|
||||
import itchat
|
||||
itchat.login()
|
||||
|
||||
it is defined in components/login.py
|
||||
and of course every single move in login can be called outside
|
||||
- you may scan source code to see how
|
||||
- and modified according to your own demand
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_QRuuid(self):
|
||||
''' get uuid for qrcode
|
||||
uuid is the symbol of qrcode
|
||||
- for logging in, you need to get a uuid first
|
||||
- for downloading qrcode, you need to pass uuid to it
|
||||
- for checking login status, uuid is also required
|
||||
if uuid has timed out, just get another
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||
''' download and show qrcode
|
||||
for options
|
||||
- uuid: if uuid is not set, latest uuid you fetched will be used
|
||||
- enableCmdQR: show qrcode in cmd
|
||||
- picDir: where to store qrcode
|
||||
- qrCallback: method that should accept uuid, status, qrcode
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def check_login(self, uuid=None):
|
||||
''' check login status
|
||||
for options:
|
||||
- uuid: if uuid is not set, latest uuid you fetched will be used
|
||||
for return values:
|
||||
- a string will be returned
|
||||
- for meaning of return values
|
||||
- 200: log in successfully
|
||||
- 201: waiting for press confirm
|
||||
- 408: uuid timed out
|
||||
- 0 : unknown error
|
||||
for processing:
|
||||
- syncUrl and fileUrl is set
|
||||
- BaseRequest is set
|
||||
blocks until reaches any of above status
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def web_init(self):
|
||||
''' get info necessary for initializing
|
||||
for processing:
|
||||
- own account info is set
|
||||
- inviteStartCount is set
|
||||
- syncKey is set
|
||||
- part of contact is fetched
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def show_mobile_login(self):
|
||||
''' show web wechat login sign
|
||||
the sign is on the top of mobile phone wechat
|
||||
sign will be added after sometime even without calling this function
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||
''' open a thread for heart loop and receiving messages
|
||||
for options:
|
||||
- exitCallback: callback after logged out
|
||||
- it contains calling of logout
|
||||
- getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned.
|
||||
for processing:
|
||||
- messages: msgs are formatted and passed on to registered fns
|
||||
- contact : chatrooms are updated when related info is received
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_msg(self):
|
||||
''' fetch messages
|
||||
for fetching
|
||||
- method blocks for sometime until
|
||||
- new messages are to be received
|
||||
- or anytime they like
|
||||
- synckey is updated with returned synccheckkey
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def logout(self):
|
||||
''' logout
|
||||
if core is now alive
|
||||
logout will tell wechat backstage to logout
|
||||
and core gets ready for another login
|
||||
it is defined in components/login.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def update_chatroom(self, userName, detailedMember=False):
|
||||
''' update chatroom
|
||||
for chatroom contact
|
||||
- a chatroom contact need updating to be detailed
|
||||
- detailed means members, encryid, etc
|
||||
- auto updating of heart loop is a more detailed updating
|
||||
- member uin will also be filled
|
||||
- once called, updated info will be stored
|
||||
for options
|
||||
- userName: 'UserName' key of chatroom or a list of it
|
||||
- detailedMember: whether to get members of contact
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def update_friend(self, userName):
|
||||
''' update chatroom
|
||||
for friend contact
|
||||
- once called, updated info will be stored
|
||||
for options
|
||||
- userName: 'UserName' key of a friend or a list of it
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_contact(self, update=False):
|
||||
''' fetch part of contact
|
||||
for part
|
||||
- all the massive platforms and friends are fetched
|
||||
- if update, only starred chatrooms are fetched
|
||||
for options
|
||||
- update: if not set, local value will be returned
|
||||
for results
|
||||
- chatroomList will be returned
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_friends(self, update=False):
|
||||
''' fetch friends list
|
||||
for options
|
||||
- update: if not set, local value will be returned
|
||||
for results
|
||||
- a list of friends' info dicts will be returned
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_chatrooms(self, update=False, contactOnly=False):
|
||||
''' fetch chatrooms list
|
||||
for options
|
||||
- update: if not set, local value will be returned
|
||||
- contactOnly: if set, only starred chatrooms will be returned
|
||||
for results
|
||||
- a list of chatrooms' info dicts will be returned
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_mps(self, update=False):
|
||||
''' fetch massive platforms list
|
||||
for options
|
||||
- update: if not set, local value will be returned
|
||||
for results
|
||||
- a list of platforms' info dicts will be returned
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def set_alias(self, userName, alias):
|
||||
''' set alias for a friend
|
||||
for options
|
||||
- userName: 'UserName' key of info dict
|
||||
- alias: new alias
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def set_pinned(self, userName, isPinned=True):
|
||||
''' set pinned for a friend or a chatroom
|
||||
for options
|
||||
- userName: 'UserName' key of info dict
|
||||
- isPinned: whether to pin
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def accept_friend(self, userName, v4,autoUpdate=True):
|
||||
''' accept a friend or accept a friend
|
||||
for options
|
||||
- userName: 'UserName' for friend's info dict
|
||||
- status:
|
||||
- for adding status should be 2
|
||||
- for accepting status should be 3
|
||||
- ticket: greeting message
|
||||
- userInfo: friend's other info for adding into local storage
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||
''' place for docs
|
||||
for options
|
||||
- if you want to get chatroom header: only set chatroomUserName
|
||||
- if you want to get friend header: only set userName
|
||||
- if you want to get chatroom member header: set both
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def create_chatroom(self, memberList, topic=''):
|
||||
''' create a chatroom
|
||||
for creating
|
||||
- its calling frequency is strictly limited
|
||||
for options
|
||||
- memberList: list of member info dict
|
||||
- topic: topic of new chatroom
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def set_chatroom_name(self, chatroomUserName, name):
|
||||
''' set chatroom name
|
||||
for setting
|
||||
- it makes an updating of chatroom
|
||||
- which means detailed info will be returned in heart loop
|
||||
for options
|
||||
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||
- name: new chatroom name
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||
''' deletes members from chatroom
|
||||
for deleting
|
||||
- you can't delete yourself
|
||||
- if so, no one will be deleted
|
||||
- strict-limited frequency
|
||||
for options
|
||||
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||
- memberList: list of members' info dict
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||
useInvitation=False):
|
||||
''' add members into chatroom
|
||||
for adding
|
||||
- you can't add yourself or member already in chatroom
|
||||
- if so, no one will be added
|
||||
- if member will over 40 after adding, invitation must be used
|
||||
- strict-limited frequency
|
||||
for options
|
||||
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||
- memberList: list of members' info dict
|
||||
- useInvitation: if invitation is not required, set this to use
|
||||
it is defined in components/contact.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send_raw_msg(self, msgType, content, toUserName):
|
||||
''' many messages are sent in a common way
|
||||
for demo
|
||||
.. code:: python
|
||||
|
||||
@itchat.msg_register(itchat.content.CARD)
|
||||
def reply(msg):
|
||||
itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName'])
|
||||
|
||||
there are some little tricks here, you may discover them yourself
|
||||
but remember they are tricks
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send_msg(self, msg='Test Message', toUserName=None):
|
||||
''' send plain text message
|
||||
for options
|
||||
- msg: should be unicode if there's non-ascii words in msg
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||
toUserName='filehelper', file_=None, preparedFile=None):
|
||||
''' upload file to server and get mediaId
|
||||
for options
|
||||
- fileDir: dir for file ready for upload
|
||||
- isPicture: whether file is a picture
|
||||
- isVideo: whether file is a video
|
||||
for return values
|
||||
will return a ReturnValue
|
||||
if succeeded, mediaId is in r['MediaId']
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||
''' send attachment
|
||||
for options
|
||||
- fileDir: dir for file ready for upload
|
||||
- mediaId: mediaId for file.
|
||||
- if set, file will not be uploaded twice
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
''' send image
|
||||
for options
|
||||
- fileDir: dir for file ready for upload
|
||||
- if it's a gif, name it like 'xx.gif'
|
||||
- mediaId: mediaId for file.
|
||||
- if set, file will not be uploaded twice
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||
''' send video
|
||||
for options
|
||||
- fileDir: dir for file ready for upload
|
||||
- if mediaId is set, it's unnecessary to set fileDir
|
||||
- mediaId: mediaId for file.
|
||||
- if set, file will not be uploaded twice
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def send(self, msg, toUserName=None, mediaId=None):
|
||||
''' wrapped function for all the sending functions
|
||||
for options
|
||||
- msg: message starts with different string indicates different type
|
||||
- list of type string: ['@fil@', '@img@', '@msg@', '@vid@']
|
||||
- they are for file, image, plain text, video
|
||||
- if none of them matches, it will be sent like plain text
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
- mediaId: if set, uploading will not be repeated
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def revoke(self, msgId, toUserName, localId=None):
|
||||
''' revoke message with its and msgId
|
||||
for options
|
||||
- msgId: message Id on server
|
||||
- toUserName: 'UserName' key of friend dict
|
||||
- localId: message Id at local (optional)
|
||||
it is defined in components/messages.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def dump_login_status(self, fileDir=None):
|
||||
''' dump login status to a specific file
|
||||
for option
|
||||
- fileDir: dir for dumping login status
|
||||
it is defined in components/hotreload.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def load_login_status(self, fileDir,
|
||||
loginCallback=None, exitCallback=None):
|
||||
''' load login status from a specific file
|
||||
for option
|
||||
- fileDir: file for loading login status
|
||||
- loginCallback: callback after successfully logged in
|
||||
- if not set, screen is cleared and qrcode is deleted
|
||||
- exitCallback: callback after logged out
|
||||
- it contains calling of logout
|
||||
it is defined in components/hotreload.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl',
|
||||
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||
loginCallback=None, exitCallback=None):
|
||||
''' log in like web wechat does
|
||||
for log in
|
||||
- a QR code will be downloaded and opened
|
||||
- then scanning status is logged, it paused for you confirm
|
||||
- finally it logged in and show your nickName
|
||||
for options
|
||||
- hotReload: enable hot reload
|
||||
- statusStorageDir: dir for storing log in status
|
||||
- enableCmdQR: show qrcode in command line
|
||||
- integers can be used to fit strange char length
|
||||
- picDir: place for storing qrcode
|
||||
- loginCallback: callback after successfully logged in
|
||||
- if not set, screen is cleared and qrcode is deleted
|
||||
- exitCallback: callback after logged out
|
||||
- it contains calling of logout
|
||||
- qrCallback: method that should accept uuid, status, qrcode
|
||||
for usage
|
||||
..code::python
|
||||
|
||||
import itchat
|
||||
itchat.auto_login()
|
||||
|
||||
it is defined in components/register.py
|
||||
and of course every single move in login can be called outside
|
||||
- you may scan source code to see how
|
||||
- and modified according to your own demond
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def configured_reply(self):
|
||||
''' determine the type of message and reply if its method is defined
|
||||
however, I use a strange way to determine whether a msg is from massive platform
|
||||
I haven't found a better solution here
|
||||
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def msg_register(self, msgType,
|
||||
isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||
''' a decorator constructor
|
||||
return a specific decorator based on information given
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def run(self, debug=True, blockThread=True):
|
||||
''' start auto respond
|
||||
for option
|
||||
- debug: if set, debug info will be shown on screen
|
||||
it is defined in components/register.py
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
def search_friends(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||
wechatAccount=None):
|
||||
return self.storageClass.search_friends(name, userName, remarkName,
|
||||
nickName, wechatAccount)
|
||||
def search_chatrooms(self, name=None, userName=None):
|
||||
return self.storageClass.search_chatrooms(name, userName)
|
||||
def search_mps(self, name=None, userName=None):
|
||||
return self.storageClass.search_mps(name, userName)
|
||||
36
lib/itchat/log.py
Normal file
36
lib/itchat/log.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
|
||||
class LogSystem(object):
|
||||
handlerList = []
|
||||
showOnCmd = True
|
||||
loggingLevel = logging.INFO
|
||||
loggingFile = None
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('itchat')
|
||||
self.logger.addHandler(logging.NullHandler())
|
||||
self.logger.setLevel(self.loggingLevel)
|
||||
self.cmdHandler = logging.StreamHandler()
|
||||
self.fileHandler = None
|
||||
self.logger.addHandler(self.cmdHandler)
|
||||
def set_logging(self, showOnCmd=True, loggingFile=None,
|
||||
loggingLevel=logging.INFO):
|
||||
if showOnCmd != self.showOnCmd:
|
||||
if showOnCmd:
|
||||
self.logger.addHandler(self.cmdHandler)
|
||||
else:
|
||||
self.logger.removeHandler(self.cmdHandler)
|
||||
self.showOnCmd = showOnCmd
|
||||
if loggingFile != self.loggingFile:
|
||||
if self.loggingFile is not None: # clear old fileHandler
|
||||
self.logger.removeHandler(self.fileHandler)
|
||||
self.fileHandler.close()
|
||||
if loggingFile is not None: # add new fileHandler
|
||||
self.fileHandler = logging.FileHandler(loggingFile)
|
||||
self.logger.addHandler(self.fileHandler)
|
||||
self.loggingFile = loggingFile
|
||||
if loggingLevel != self.loggingLevel:
|
||||
self.logger.setLevel(loggingLevel)
|
||||
self.loggingLevel = loggingLevel
|
||||
|
||||
ls = LogSystem()
|
||||
set_logging = ls.set_logging
|
||||
67
lib/itchat/returnvalues.py
Normal file
67
lib/itchat/returnvalues.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#coding=utf8
|
||||
TRANSLATE = 'Chinese'
|
||||
|
||||
class ReturnValue(dict):
|
||||
''' turn return value of itchat into a boolean value
|
||||
for requests:
|
||||
..code::python
|
||||
|
||||
import requests
|
||||
r = requests.get('http://httpbin.org/get')
|
||||
print(ReturnValue(rawResponse=r)
|
||||
|
||||
for normal dict:
|
||||
..code::python
|
||||
|
||||
returnDict = {
|
||||
'BaseResponse': {
|
||||
'Ret': 0,
|
||||
'ErrMsg': 'My error msg', }, }
|
||||
print(ReturnValue(returnDict))
|
||||
'''
|
||||
def __init__(self, returnValueDict={}, rawResponse=None):
|
||||
if rawResponse:
|
||||
try:
|
||||
returnValueDict = rawResponse.json()
|
||||
except ValueError:
|
||||
returnValueDict = {
|
||||
'BaseResponse': {
|
||||
'Ret': -1004,
|
||||
'ErrMsg': 'Unexpected return value', },
|
||||
'Data': rawResponse.content, }
|
||||
for k, v in returnValueDict.items():
|
||||
self[k] = v
|
||||
if not 'BaseResponse' in self:
|
||||
self['BaseResponse'] = {
|
||||
'ErrMsg': 'no BaseResponse in raw response',
|
||||
'Ret': -1000, }
|
||||
if TRANSLATE:
|
||||
self['BaseResponse']['RawMsg'] = self['BaseResponse'].get('ErrMsg', '')
|
||||
self['BaseResponse']['ErrMsg'] = \
|
||||
TRANSLATION[TRANSLATE].get(
|
||||
self['BaseResponse'].get('Ret', '')) \
|
||||
or self['BaseResponse'].get('ErrMsg', u'No ErrMsg')
|
||||
self['BaseResponse']['RawMsg'] = \
|
||||
self['BaseResponse']['RawMsg'] or self['BaseResponse']['ErrMsg']
|
||||
def __nonzero__(self):
|
||||
return self['BaseResponse'].get('Ret') == 0
|
||||
def __bool__(self):
|
||||
return self.__nonzero__()
|
||||
def __str__(self):
|
||||
return '{%s}' % ', '.join(
|
||||
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||
def __repr__(self):
|
||||
return '<ItchatReturnValue: %s>' % self.__str__()
|
||||
|
||||
TRANSLATION = {
|
||||
'Chinese': {
|
||||
-1000: u'返回值不带BaseResponse',
|
||||
-1001: u'无法找到对应的成员',
|
||||
-1002: u'文件位置错误',
|
||||
-1003: u'服务器拒绝连接',
|
||||
-1004: u'服务器返回异常值',
|
||||
-1005: u'参数错误',
|
||||
-1006: u'无效操作',
|
||||
0: u'请求成功',
|
||||
},
|
||||
}
|
||||
117
lib/itchat/storage/__init__.py
Normal file
117
lib/itchat/storage/__init__.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import os, time, copy
|
||||
from threading import Lock
|
||||
|
||||
from .messagequeue import Queue
|
||||
from .templates import (
|
||||
ContactList, AbstractUserDict, User,
|
||||
MassivePlatform, Chatroom, ChatroomMember)
|
||||
|
||||
def contact_change(fn):
|
||||
def _contact_change(core, *args, **kwargs):
|
||||
with core.storageClass.updateLock:
|
||||
return fn(core, *args, **kwargs)
|
||||
return _contact_change
|
||||
|
||||
class Storage(object):
|
||||
def __init__(self, core):
|
||||
self.userName = None
|
||||
self.nickName = None
|
||||
self.updateLock = Lock()
|
||||
self.memberList = ContactList()
|
||||
self.mpList = ContactList()
|
||||
self.chatroomList = ContactList()
|
||||
self.msgList = Queue(-1)
|
||||
self.lastInputUserName = None
|
||||
self.memberList.set_default_value(contactClass=User)
|
||||
self.memberList.core = core
|
||||
self.mpList.set_default_value(contactClass=MassivePlatform)
|
||||
self.mpList.core = core
|
||||
self.chatroomList.set_default_value(contactClass=Chatroom)
|
||||
self.chatroomList.core = core
|
||||
def dumps(self):
|
||||
return {
|
||||
'userName' : self.userName,
|
||||
'nickName' : self.nickName,
|
||||
'memberList' : self.memberList,
|
||||
'mpList' : self.mpList,
|
||||
'chatroomList' : self.chatroomList,
|
||||
'lastInputUserName' : self.lastInputUserName, }
|
||||
def loads(self, j):
|
||||
self.userName = j.get('userName', None)
|
||||
self.nickName = j.get('nickName', None)
|
||||
del self.memberList[:]
|
||||
for i in j.get('memberList', []):
|
||||
self.memberList.append(i)
|
||||
del self.mpList[:]
|
||||
for i in j.get('mpList', []):
|
||||
self.mpList.append(i)
|
||||
del self.chatroomList[:]
|
||||
for i in j.get('chatroomList', []):
|
||||
self.chatroomList.append(i)
|
||||
# I tried to solve everything in pickle
|
||||
# but this way is easier and more storage-saving
|
||||
for chatroom in self.chatroomList:
|
||||
if 'MemberList' in chatroom:
|
||||
for member in chatroom['MemberList']:
|
||||
member.core = chatroom.core
|
||||
member.chatroom = chatroom
|
||||
if 'Self' in chatroom:
|
||||
chatroom['Self'].core = chatroom.core
|
||||
chatroom['Self'].chatroom = chatroom
|
||||
self.lastInputUserName = j.get('lastInputUserName', None)
|
||||
def search_friends(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||
wechatAccount=None):
|
||||
with self.updateLock:
|
||||
if (name or userName or remarkName or nickName or wechatAccount) is None:
|
||||
return copy.deepcopy(self.memberList[0]) # my own account
|
||||
elif userName: # return the only userName match
|
||||
for m in self.memberList:
|
||||
if m['UserName'] == userName:
|
||||
return copy.deepcopy(m)
|
||||
else:
|
||||
matchDict = {
|
||||
'RemarkName' : remarkName,
|
||||
'NickName' : nickName,
|
||||
'Alias' : wechatAccount, }
|
||||
for k in ('RemarkName', 'NickName', 'Alias'):
|
||||
if matchDict[k] is None:
|
||||
del matchDict[k]
|
||||
if name: # select based on name
|
||||
contact = []
|
||||
for m in self.memberList:
|
||||
if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]):
|
||||
contact.append(m)
|
||||
else:
|
||||
contact = self.memberList[:]
|
||||
if matchDict: # select again based on matchDict
|
||||
friendList = []
|
||||
for m in contact:
|
||||
if all([m.get(k) == v for k, v in matchDict.items()]):
|
||||
friendList.append(m)
|
||||
return copy.deepcopy(friendList)
|
||||
else:
|
||||
return copy.deepcopy(contact)
|
||||
def search_chatrooms(self, name=None, userName=None):
|
||||
with self.updateLock:
|
||||
if userName is not None:
|
||||
for m in self.chatroomList:
|
||||
if m['UserName'] == userName:
|
||||
return copy.deepcopy(m)
|
||||
elif name is not None:
|
||||
matchList = []
|
||||
for m in self.chatroomList:
|
||||
if name in m['NickName']:
|
||||
matchList.append(copy.deepcopy(m))
|
||||
return matchList
|
||||
def search_mps(self, name=None, userName=None):
|
||||
with self.updateLock:
|
||||
if userName is not None:
|
||||
for m in self.mpList:
|
||||
if m['UserName'] == userName:
|
||||
return copy.deepcopy(m)
|
||||
elif name is not None:
|
||||
matchList = []
|
||||
for m in self.mpList:
|
||||
if name in m['NickName']:
|
||||
matchList.append(copy.deepcopy(m))
|
||||
return matchList
|
||||
32
lib/itchat/storage/messagequeue.py
Normal file
32
lib/itchat/storage/messagequeue.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
try:
|
||||
import Queue as queue
|
||||
except ImportError:
|
||||
import queue
|
||||
|
||||
from .templates import AttributeDict
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
class Queue(queue.Queue):
|
||||
def put(self, message):
|
||||
queue.Queue.put(self, Message(message))
|
||||
|
||||
class Message(AttributeDict):
|
||||
def download(self, fileName):
|
||||
if hasattr(self.text, '__call__'):
|
||||
return self.text(fileName)
|
||||
else:
|
||||
return b''
|
||||
def __getitem__(self, value):
|
||||
if value in ('isAdmin', 'isAt'):
|
||||
v = value[0].upper() + value[1:] # ''[1:] == ''
|
||||
logger.debug('%s is expired in 1.3.0, use %s instead.' % (value, v))
|
||||
value = v
|
||||
return super(Message, self).__getitem__(value)
|
||||
def __str__(self):
|
||||
return '{%s}' % ', '.join(
|
||||
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||
def __repr__(self):
|
||||
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||
self.__str__())
|
||||
318
lib/itchat/storage/templates.py
Normal file
318
lib/itchat/storage/templates.py
Normal file
@@ -0,0 +1,318 @@
|
||||
import logging, copy, pickle
|
||||
from weakref import ref
|
||||
|
||||
from ..returnvalues import ReturnValue
|
||||
from ..utils import update_info_dict
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
class AttributeDict(dict):
|
||||
def __getattr__(self, value):
|
||||
keyName = value[0].upper() + value[1:]
|
||||
try:
|
||||
return self[keyName]
|
||||
except KeyError:
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (
|
||||
self.__class__.__name__.split('.')[-1], keyName))
|
||||
def get(self, v, d=None):
|
||||
try:
|
||||
return self[v]
|
||||
except KeyError:
|
||||
return d
|
||||
|
||||
class UnInitializedItchat(object):
|
||||
def _raise_error(self, *args, **kwargs):
|
||||
logger.warning('An itchat instance is called before initialized')
|
||||
def __getattr__(self, value):
|
||||
return self._raise_error
|
||||
|
||||
class ContactList(list):
|
||||
''' when a dict is append, init function will be called to format that dict '''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ContactList, self).__init__(*args, **kwargs)
|
||||
self.__setstate__(None)
|
||||
@property
|
||||
def core(self):
|
||||
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||
@core.setter
|
||||
def core(self, value):
|
||||
self._core = ref(value)
|
||||
def set_default_value(self, initFunction=None, contactClass=None):
|
||||
if hasattr(initFunction, '__call__'):
|
||||
self.contactInitFn = initFunction
|
||||
if hasattr(contactClass, '__call__'):
|
||||
self.contactClass = contactClass
|
||||
def append(self, value):
|
||||
contact = self.contactClass(value)
|
||||
contact.core = self.core
|
||||
if self.contactInitFn is not None:
|
||||
contact = self.contactInitFn(self, contact) or contact
|
||||
super(ContactList, self).append(contact)
|
||||
def __deepcopy__(self, memo):
|
||||
r = self.__class__([copy.deepcopy(v) for v in self])
|
||||
r.contactInitFn = self.contactInitFn
|
||||
r.contactClass = self.contactClass
|
||||
r.core = self.core
|
||||
return r
|
||||
def __getstate__(self):
|
||||
return 1
|
||||
def __setstate__(self, state):
|
||||
self.contactInitFn = None
|
||||
self.contactClass = User
|
||||
def __str__(self):
|
||||
return '[%s]' % ', '.join([repr(v) for v in self])
|
||||
def __repr__(self):
|
||||
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||
self.__str__())
|
||||
|
||||
class AbstractUserDict(AttributeDict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AbstractUserDict, self).__init__(*args, **kwargs)
|
||||
@property
|
||||
def core(self):
|
||||
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||
@core.setter
|
||||
def core(self, value):
|
||||
self._core = ref(value)
|
||||
def update(self):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not be updated' % \
|
||||
self.__class__.__name__, }, })
|
||||
def set_alias(self, alias):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not set alias' % \
|
||||
self.__class__.__name__, }, })
|
||||
def set_pinned(self, isPinned=True):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not be pinned' % \
|
||||
self.__class__.__name__, }, })
|
||||
def verify(self):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s do not need verify' % \
|
||||
self.__class__.__name__, }, })
|
||||
def get_head_image(self, imageDir=None):
|
||||
return self.core.get_head_img(self.userName, picDir=imageDir)
|
||||
def delete_member(self, userName):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not delete member' % \
|
||||
self.__class__.__name__, }, })
|
||||
def add_member(self, userName):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not add member' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send_raw_msg(self, msgType, content):
|
||||
return self.core.send_raw_msg(msgType, content, self.userName)
|
||||
def send_msg(self, msg='Test Message'):
|
||||
return self.core.send_msg(msg, self.userName)
|
||||
def send_file(self, fileDir, mediaId=None):
|
||||
return self.core.send_file(fileDir, self.userName, mediaId)
|
||||
def send_image(self, fileDir, mediaId=None):
|
||||
return self.core.send_image(fileDir, self.userName, mediaId)
|
||||
def send_video(self, fileDir=None, mediaId=None):
|
||||
return self.core.send_video(fileDir, self.userName, mediaId)
|
||||
def send(self, msg, mediaId=None):
|
||||
return self.core.send(msg, self.userName, mediaId)
|
||||
def search_member(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||
wechatAccount=None):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s do not have members' % \
|
||||
self.__class__.__name__, }, })
|
||||
def __deepcopy__(self, memo):
|
||||
r = self.__class__()
|
||||
for k, v in self.items():
|
||||
r[copy.deepcopy(k)] = copy.deepcopy(v)
|
||||
r.core = self.core
|
||||
return r
|
||||
def __str__(self):
|
||||
return '{%s}' % ', '.join(
|
||||
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||
def __repr__(self):
|
||||
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||
self.__str__())
|
||||
def __getstate__(self):
|
||||
return 1
|
||||
def __setstate__(self, state):
|
||||
pass
|
||||
|
||||
class User(AbstractUserDict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(User, self).__init__(*args, **kwargs)
|
||||
self.__setstate__(None)
|
||||
def update(self):
|
||||
r = self.core.update_friend(self.userName)
|
||||
if r:
|
||||
update_info_dict(self, r)
|
||||
return r
|
||||
def set_alias(self, alias):
|
||||
return self.core.set_alias(self.userName, alias)
|
||||
def set_pinned(self, isPinned=True):
|
||||
return self.core.set_pinned(self.userName, isPinned)
|
||||
def verify(self):
|
||||
return self.core.add_friend(**self.verifyDict)
|
||||
def __deepcopy__(self, memo):
|
||||
r = super(User, self).__deepcopy__(memo)
|
||||
r.verifyDict = copy.deepcopy(self.verifyDict)
|
||||
return r
|
||||
def __setstate__(self, state):
|
||||
super(User, self).__setstate__(state)
|
||||
self.verifyDict = {}
|
||||
self['MemberList'] = fakeContactList
|
||||
|
||||
class MassivePlatform(AbstractUserDict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MassivePlatform, self).__init__(*args, **kwargs)
|
||||
self.__setstate__(None)
|
||||
def __setstate__(self, state):
|
||||
super(MassivePlatform, self).__setstate__(state)
|
||||
self['MemberList'] = fakeContactList
|
||||
|
||||
class Chatroom(AbstractUserDict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Chatroom, self).__init__(*args, **kwargs)
|
||||
memberList = ContactList()
|
||||
userName = self.get('UserName', '')
|
||||
refSelf = ref(self)
|
||||
def init_fn(parentList, d):
|
||||
d.chatroom = refSelf() or \
|
||||
parentList.core.search_chatrooms(userName=userName)
|
||||
memberList.set_default_value(init_fn, ChatroomMember)
|
||||
if 'MemberList' in self:
|
||||
for member in self.memberList:
|
||||
memberList.append(member)
|
||||
self['MemberList'] = memberList
|
||||
@property
|
||||
def core(self):
|
||||
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||
@core.setter
|
||||
def core(self, value):
|
||||
self._core = ref(value)
|
||||
self.memberList.core = value
|
||||
for member in self.memberList:
|
||||
member.core = value
|
||||
def update(self, detailedMember=False):
|
||||
r = self.core.update_chatroom(self.userName, detailedMember)
|
||||
if r:
|
||||
update_info_dict(self, r)
|
||||
self['MemberList'] = r['MemberList']
|
||||
return r
|
||||
def set_alias(self, alias):
|
||||
return self.core.set_chatroom_name(self.userName, alias)
|
||||
def set_pinned(self, isPinned=True):
|
||||
return self.core.set_pinned(self.userName, isPinned)
|
||||
def delete_member(self, userName):
|
||||
return self.core.delete_member_from_chatroom(self.userName, userName)
|
||||
def add_member(self, userName):
|
||||
return self.core.add_member_into_chatroom(self.userName, userName)
|
||||
def search_member(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||
wechatAccount=None):
|
||||
with self.core.storageClass.updateLock:
|
||||
if (name or userName or remarkName or nickName or wechatAccount) is None:
|
||||
return None
|
||||
elif userName: # return the only userName match
|
||||
for m in self.memberList:
|
||||
if m.userName == userName:
|
||||
return copy.deepcopy(m)
|
||||
else:
|
||||
matchDict = {
|
||||
'RemarkName' : remarkName,
|
||||
'NickName' : nickName,
|
||||
'Alias' : wechatAccount, }
|
||||
for k in ('RemarkName', 'NickName', 'Alias'):
|
||||
if matchDict[k] is None:
|
||||
del matchDict[k]
|
||||
if name: # select based on name
|
||||
contact = []
|
||||
for m in self.memberList:
|
||||
if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]):
|
||||
contact.append(m)
|
||||
else:
|
||||
contact = self.memberList[:]
|
||||
if matchDict: # select again based on matchDict
|
||||
friendList = []
|
||||
for m in contact:
|
||||
if all([m.get(k) == v for k, v in matchDict.items()]):
|
||||
friendList.append(m)
|
||||
return copy.deepcopy(friendList)
|
||||
else:
|
||||
return copy.deepcopy(contact)
|
||||
def __setstate__(self, state):
|
||||
super(Chatroom, self).__setstate__(state)
|
||||
if not 'MemberList' in self:
|
||||
self['MemberList'] = fakeContactList
|
||||
|
||||
class ChatroomMember(AbstractUserDict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AbstractUserDict, self).__init__(*args, **kwargs)
|
||||
self.__setstate__(None)
|
||||
@property
|
||||
def chatroom(self):
|
||||
r = getattr(self, '_chatroom', lambda: fakeChatroom)()
|
||||
if r is None:
|
||||
userName = getattr(self, '_chatroomUserName', '')
|
||||
r = self.core.search_chatrooms(userName=userName)
|
||||
if isinstance(r, dict):
|
||||
self.chatroom = r
|
||||
return r or fakeChatroom
|
||||
@chatroom.setter
|
||||
def chatroom(self, value):
|
||||
if isinstance(value, dict) and 'UserName' in value:
|
||||
self._chatroom = ref(value)
|
||||
self._chatroomUserName = value['UserName']
|
||||
def get_head_image(self, imageDir=None):
|
||||
return self.core.get_head_img(self.userName, self.chatroom.userName, picDir=imageDir)
|
||||
def delete_member(self, userName):
|
||||
return self.core.delete_member_from_chatroom(self.chatroom.userName, self.userName)
|
||||
def send_raw_msg(self, msgType, content):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send_msg(self, msg='Test Message'):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send_file(self, fileDir, mediaId=None):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send_image(self, fileDir, mediaId=None):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send_video(self, fileDir=None, mediaId=None):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def send(self, msg, mediaId=None):
|
||||
return ReturnValue({'BaseResponse': {
|
||||
'Ret': -1006,
|
||||
'ErrMsg': '%s can not send message directly' % \
|
||||
self.__class__.__name__, }, })
|
||||
def __setstate__(self, state):
|
||||
super(ChatroomMember, self).__setstate__(state)
|
||||
self['MemberList'] = fakeContactList
|
||||
|
||||
def wrap_user_dict(d):
|
||||
userName = d.get('UserName')
|
||||
if '@@' in userName:
|
||||
r = Chatroom(d)
|
||||
elif d.get('VerifyFlag', 8) & 8 == 0:
|
||||
r = User(d)
|
||||
else:
|
||||
r = MassivePlatform(d)
|
||||
return r
|
||||
|
||||
fakeItchat = UnInitializedItchat()
|
||||
fakeContactList = ContactList()
|
||||
fakeChatroom = Chatroom()
|
||||
163
lib/itchat/utils.py
Normal file
163
lib/itchat/utils.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import re, os, sys, subprocess, copy, traceback, logging
|
||||
|
||||
try:
|
||||
from HTMLParser import HTMLParser
|
||||
except ImportError:
|
||||
from html.parser import HTMLParser
|
||||
try:
|
||||
from urllib import quote as _quote
|
||||
quote = lambda n: _quote(n.encode('utf8', 'replace'))
|
||||
except ImportError:
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
from . import config
|
||||
|
||||
logger = logging.getLogger('itchat')
|
||||
|
||||
emojiRegex = re.compile(r'<span class="emoji emoji(.{1,10})"></span>')
|
||||
htmlParser = HTMLParser()
|
||||
if not hasattr(htmlParser, 'unescape'):
|
||||
import html
|
||||
htmlParser.unescape = html.unescape
|
||||
# FIX Python 3.9 HTMLParser.unescape is removed. See https://docs.python.org/3.9/whatsnew/3.9.html
|
||||
try:
|
||||
b = u'\u2588'
|
||||
sys.stdout.write(b + '\r')
|
||||
sys.stdout.flush()
|
||||
except UnicodeEncodeError:
|
||||
BLOCK = 'MM'
|
||||
else:
|
||||
BLOCK = b
|
||||
friendInfoTemplate = {}
|
||||
for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province',
|
||||
'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature',
|
||||
'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'):
|
||||
friendInfoTemplate[k] = ''
|
||||
for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag',
|
||||
'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin',
|
||||
'StarFriend', 'Statues'):
|
||||
friendInfoTemplate[k] = 0
|
||||
friendInfoTemplate['MemberList'] = []
|
||||
|
||||
def clear_screen():
|
||||
os.system('cls' if config.OS == 'Windows' else 'clear')
|
||||
|
||||
def emoji_formatter(d, k):
|
||||
''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage
|
||||
like :face with tears of joy: will be replaced with :cat face with tears of joy:
|
||||
'''
|
||||
def _emoji_debugger(d, k):
|
||||
s = d[k].replace('<span class="emoji emoji1f450"></span',
|
||||
'<span class="emoji emoji1f450"></span>') # fix missing bug
|
||||
def __fix_miss_match(m):
|
||||
return '<span class="emoji emoji%s"></span>' % ({
|
||||
'1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603',
|
||||
'1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d',
|
||||
'1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622',
|
||||
}.get(m.group(1), m.group(1)))
|
||||
return emojiRegex.sub(__fix_miss_match, s)
|
||||
def _emoji_formatter(m):
|
||||
s = m.group(1)
|
||||
if len(s) == 6:
|
||||
return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0'))
|
||||
).encode('utf8').decode('unicode-escape', 'replace')
|
||||
elif len(s) == 10:
|
||||
return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0'))
|
||||
).encode('utf8').decode('unicode-escape', 'replace')
|
||||
else:
|
||||
return ('\\U%s'%m.group(1).rjust(8, '0')
|
||||
).encode('utf8').decode('unicode-escape', 'replace')
|
||||
d[k] = _emoji_debugger(d, k)
|
||||
d[k] = emojiRegex.sub(_emoji_formatter, d[k])
|
||||
|
||||
def msg_formatter(d, k):
|
||||
emoji_formatter(d, k)
|
||||
d[k] = d[k].replace('<br/>', '\n')
|
||||
d[k] = htmlParser.unescape(d[k])
|
||||
|
||||
def check_file(fileDir):
|
||||
try:
|
||||
with open(fileDir):
|
||||
pass
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def print_qr(fileDir):
|
||||
if config.OS == 'Darwin':
|
||||
subprocess.call(['open', fileDir])
|
||||
elif config.OS == 'Linux':
|
||||
subprocess.call(['xdg-open', fileDir])
|
||||
else:
|
||||
os.startfile(fileDir)
|
||||
|
||||
def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True):
|
||||
blockCount = int(enableCmdQR)
|
||||
if abs(blockCount) == 0:
|
||||
blockCount = 1
|
||||
white *= abs(blockCount)
|
||||
if blockCount < 0:
|
||||
white, black = black, white
|
||||
sys.stdout.write(' '*50 + '\r')
|
||||
sys.stdout.flush()
|
||||
qr = qrText.replace('0', white).replace('1', black)
|
||||
sys.stdout.write(qr)
|
||||
sys.stdout.flush()
|
||||
|
||||
def struct_friend_info(knownInfo):
|
||||
member = copy.deepcopy(friendInfoTemplate)
|
||||
for k, v in copy.deepcopy(knownInfo).items(): member[k] = v
|
||||
return member
|
||||
|
||||
def search_dict_list(l, key, value):
|
||||
''' Search a list of dict
|
||||
* return dict with specific value & key '''
|
||||
for i in l:
|
||||
if i.get(key) == value:
|
||||
return i
|
||||
|
||||
def print_line(msg, oneLine = False):
|
||||
if oneLine:
|
||||
sys.stdout.write(' '*40 + '\r')
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace'
|
||||
).decode(sys.stdin.encoding or 'utf8', 'replace'))
|
||||
sys.stdout.flush()
|
||||
|
||||
def test_connect(retryTime=5):
|
||||
for i in range(retryTime):
|
||||
try:
|
||||
r = requests.get(config.BASE_URL)
|
||||
return True
|
||||
except:
|
||||
if i == retryTime - 1:
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def contact_deep_copy(core, contact):
|
||||
with core.storageClass.updateLock:
|
||||
return copy.deepcopy(contact)
|
||||
|
||||
def get_image_postfix(data):
|
||||
data = data[:20]
|
||||
if b'GIF' in data:
|
||||
return 'gif'
|
||||
elif b'PNG' in data:
|
||||
return 'png'
|
||||
elif b'JFIF' in data:
|
||||
return 'jpg'
|
||||
return ''
|
||||
|
||||
def update_info_dict(oldInfoDict, newInfoDict):
|
||||
''' only normal values will be updated here
|
||||
because newInfoDict is normal dict, so it's not necessary to consider templates
|
||||
'''
|
||||
for k, v in newInfoDict.items():
|
||||
if any((isinstance(v, t) for t in (tuple, list, dict))):
|
||||
pass # these values will be updated somewhere else
|
||||
elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0):
|
||||
oldInfoDict[k] = v
|
||||
7
nixpacks.toml
Normal file
7
nixpacks.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
providers = ['python']
|
||||
|
||||
[phases.setup]
|
||||
nixPkgs = ['python310']
|
||||
cmds = ['apt-get update','apt-get install -y --no-install-recommends ffmpeg espeak']
|
||||
[start]
|
||||
cmd = "python ./app.py"
|
||||
235
plugins/README.md
Normal file
235
plugins/README.md
Normal file
@@ -0,0 +1,235 @@
|
||||
## 插件化初衷
|
||||
|
||||
之前未插件化的代码耦合程度高,如果要定制一些个性化功能(如流量控制、接入`NovelAI`画图平台等),需要了解代码主体,避免影响到其他的功能。在实现多个功能后,不但无法调整功能的优先级顺序,功能的配置项也会变得非常混乱。
|
||||
|
||||
此时插件化应声而出。
|
||||
|
||||
**插件化**: 在保证主体功能是ChatGPT的前提下,我们推荐将主体功能外的功能利用插件的方式实现。
|
||||
|
||||
- [x] 可根据功能需要,下载不同插件。
|
||||
- [x] 插件开发成本低,仅需了解插件触发事件,并按照插件定义接口编写插件。
|
||||
- [x] 插件化能够自由开关和调整优先级。
|
||||
- [x] 每个插件可在插件文件夹内维护独立的配置文件,方便代码的测试和调试,可以在独立的仓库开发插件。
|
||||
|
||||
PS: 插件目前仅支持`itchat`
|
||||
|
||||
## 插件化实现
|
||||
|
||||
插件化实现是在收到消息到发送回复的各个步骤之间插入触发事件实现的。
|
||||
|
||||
### 消息处理过程
|
||||
|
||||
在了解插件触发事件前,首先需要了解程序收到消息到发送回复的整个过程。
|
||||
|
||||
插件化版本中,消息处理过程可以分为4个步骤:
|
||||
```
|
||||
1.收到消息 ---> 2.产生回复 ---> 3.包装回复 ---> 4.发送回复
|
||||
```
|
||||
|
||||
以下是它们的默认处理逻辑(太长不看,可跳过):
|
||||
|
||||
#### 1. 收到消息
|
||||
|
||||
负责接收用户消息,根据用户的配置,判断本条消息是否触发机器人。如果触发,则会判断该消息的类型(声音、文本、画图命令等),将消息包装成如下的`Context`交付给下一个步骤。
|
||||
|
||||
```python
|
||||
class ContextType (Enum):
|
||||
TEXT = 1 # 文本消息
|
||||
VOICE = 2 # 音频消息
|
||||
IMAGE_CREATE = 3 # 创建图片命令
|
||||
class Context:
|
||||
def __init__(self, type : ContextType = None , content = None, kwargs = dict()):
|
||||
self.type = type
|
||||
self.content = content
|
||||
self.kwargs = kwargs
|
||||
def __getitem__(self, key):
|
||||
return self.kwargs[key]
|
||||
```
|
||||
|
||||
`Context`中除了存放消息类型和内容外,还存放了一些与会话相关的参数。
|
||||
|
||||
例如,当收到用户私聊消息时,会存放以下的会话参数。
|
||||
|
||||
```python
|
||||
context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id}
|
||||
```
|
||||
|
||||
- `isgroup`: `Context`是否是群聊消息。
|
||||
- `msg`: `itchat`中原始的消息对象。
|
||||
- `receiver`: 需要回复消息的对象ID。
|
||||
- `session_id`: 会话ID(一般是发送触发bot消息的用户ID,如果在群聊中并且`conf`里设置了`group_chat_in_one_session`,那么此处便是群聊ID)
|
||||
|
||||
#### 2. 产生回复
|
||||
|
||||
处理消息并产生回复。目前默认处理逻辑是根据`Context`的类型交付给对应的bot,并产生回复`Reply`。 如果本步骤没有产生任何回复,那么会跳过之后的所有步骤。
|
||||
|
||||
```python
|
||||
if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE:
|
||||
reply = super().build_reply_content(context.content, context) #文字跟画图交付给chatgpt
|
||||
elif context.type == ContextType.VOICE: # 声音先进行语音转文字后,修改Context类型为文字后,再交付给chatgpt
|
||||
msg = context['msg']
|
||||
file_name = TmpDir().path() + context.content
|
||||
msg.download(file_name)
|
||||
reply = super().build_voice_to_text(file_name)
|
||||
if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO:
|
||||
context.content = reply.content # 语音转文字后,将文字内容作为新的context
|
||||
context.type = ContextType.TEXT
|
||||
reply = super().build_reply_content(context.content, context)
|
||||
if reply.type == ReplyType.TEXT:
|
||||
if conf().get('voice_reply_voice'):
|
||||
reply = super().build_text_to_voice(reply.content)
|
||||
```
|
||||
|
||||
回复`Reply`的定义如下所示,它允许Bot可以回复多类不同的消息。同时也加入了`INFO`和`ERROR`消息类型区分系统提示和系统错误。
|
||||
|
||||
```python
|
||||
class ReplyType(Enum):
|
||||
TEXT = 1 # 文本
|
||||
VOICE = 2 # 音频文件
|
||||
IMAGE = 3 # 图片文件
|
||||
IMAGE_URL = 4 # 图片URL
|
||||
|
||||
INFO = 9
|
||||
ERROR = 10
|
||||
class Reply:
|
||||
def __init__(self, type : ReplyType = None , content = None):
|
||||
self.type = type
|
||||
self.content = content
|
||||
```
|
||||
|
||||
#### 3. 装饰回复
|
||||
|
||||
根据`Context`和回复`Reply`的类型,对回复的内容进行装饰。目前的装饰有以下两种:
|
||||
|
||||
- `TEXT`文本回复,根据是否在群聊中来决定是艾特接收方还是添加回复的前缀。
|
||||
|
||||
- `INFO`或`ERROR`类型,会在消息前添加对应的系统提示字样。
|
||||
|
||||
如下是默认逻辑的代码:
|
||||
|
||||
```python
|
||||
if reply.type == ReplyType.TEXT:
|
||||
reply_text = reply.content
|
||||
if context['isgroup']:
|
||||
reply_text = '@' + context['msg']['ActualNickName'] + ' ' + reply_text.strip()
|
||||
reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
|
||||
else:
|
||||
reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
|
||||
reply.content = reply_text
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
reply.content = str(reply.type)+":\n" + reply.content
|
||||
```
|
||||
|
||||
#### 4. 发送回复
|
||||
|
||||
根据`Reply`的类型,默认逻辑调用不同的发送函数发送回复给接收方`context["receiver"]`。
|
||||
|
||||
### 插件触发事件
|
||||
|
||||
主程序目前会在各个消息步骤间触发事件,监听相应事件的插件会按照优先级,顺序调用事件处理函数。
|
||||
|
||||
目前支持三类触发事件:
|
||||
```
|
||||
1.收到消息
|
||||
---> `ON_HANDLE_CONTEXT`
|
||||
2.产生回复
|
||||
---> `ON_DECORATE_REPLY`
|
||||
3.装饰回复
|
||||
---> `ON_SEND_REPLY`
|
||||
4.发送回复
|
||||
```
|
||||
|
||||
触发事件会产生事件的上下文`EventContext`,它包含了以下信息:
|
||||
|
||||
`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply})`
|
||||
|
||||
插件处理函数可通过修改`EventContext`中的`context`和`reply`来实现功能。
|
||||
|
||||
## 插件编写示例
|
||||
|
||||
以`plugins/hello`为例,其中编写了一个简单的`Hello`插件。
|
||||
|
||||
### 1. 创建插件
|
||||
|
||||
在`plugins`目录下创建一个插件文件夹`hello`。然后,在该文件夹中创建一个与文件夹同名的`.py`文件`hello.py`。
|
||||
```
|
||||
plugins/
|
||||
└── hello
|
||||
├── __init__.py
|
||||
└── hello.py
|
||||
```
|
||||
|
||||
### 2. 编写插件类
|
||||
|
||||
在`hello.py`文件中,创建插件类,它继承自`Plugin`。
|
||||
|
||||
在类定义之前需要使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高。初次加载插件后可在`plugins/plugins.json`中修改插件优先级。
|
||||
|
||||
并在`__init__`中绑定你编写的事件处理函数。
|
||||
|
||||
`Hello`插件为事件`ON_HANDLE_CONTEXT`绑定了一个处理函数`on_handle_context`,它表示之后每次生成回复前,都会由`on_handle_context`先处理。
|
||||
|
||||
PS: `ON_HANDLE_CONTEXT`是最常用的事件,如果要根据不同的消息来生成回复,就用它。
|
||||
|
||||
```python
|
||||
@plugins.register(name="Hello", desc="A simple plugin that says hello", version="0.1", author="lanvent", desire_priority= -1)
|
||||
class Hello(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[Hello] inited")
|
||||
```
|
||||
|
||||
### 3. 编写事件处理函数
|
||||
|
||||
#### 修改事件上下文
|
||||
|
||||
事件处理函数接收一个`EventContext`对象`e_context`作为参数。`e_context`包含了事件相关信息,利用`e_context['key']`来访问这些信息。
|
||||
|
||||
`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply})`
|
||||
|
||||
处理函数中通过修改`e_context`对象中的事件相关信息来实现所需功能,比如更改`e_context['reply']`中的内容可以修改回复。
|
||||
|
||||
#### 决定是否交付给下个插件或默认逻辑
|
||||
|
||||
在处理函数结束时,还需要设置`e_context`对象的`action`属性,它决定如何继续处理事件。目前有以下三种处理方式:
|
||||
|
||||
- `EventAction.CONTINUE`: 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑。
|
||||
- `EventAction.BREAK`: 事件结束,不再给下个插件处理,交付给默认的处理逻辑。
|
||||
- `EventAction.BREAK_PASS`: 事件结束,不再给下个插件处理,跳过默认的处理逻辑。
|
||||
|
||||
#### 示例处理函数
|
||||
|
||||
`Hello`插件处理`Context`类型为`TEXT`的消息:
|
||||
|
||||
- 如果内容是`Hello`,就将回复设置为`Hello+用户昵称`,并跳过之后的插件和默认逻辑。
|
||||
- 如果内容是`End`,就将`Context`的类型更改为`IMAGE_CREATE`,并让事件继续,如果最终交付到默认逻辑,会调用默认的画图Bot来画画。
|
||||
|
||||
```python
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
if e_context['context'].type != ContextType.TEXT:
|
||||
return
|
||||
content = e_context['context'].content
|
||||
if content == "Hello":
|
||||
reply = Reply()
|
||||
reply.type = ReplyType.TEXT
|
||||
msg = e_context['context']['msg']
|
||||
if e_context['context']['isgroup']:
|
||||
reply.content = "Hello, " + msg['ActualNickName'] + " from " + msg['User'].get('NickName', "Group")
|
||||
else:
|
||||
reply.content = "Hello, " + msg['User'].get('NickName', "My friend")
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
|
||||
if content == "End":
|
||||
# 如果是文本消息"End",将请求转换成"IMAGE_CREATE",并将content设置为"The World"
|
||||
e_context['context'].type = ContextType.IMAGE_CREATE
|
||||
content = "The World"
|
||||
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
|
||||
```
|
||||
|
||||
## 插件设计建议
|
||||
|
||||
- 尽情将你想要的个性化功能设计为插件。
|
||||
- 一个插件目录建议只注册一个插件类。建议使用单独的仓库维护插件,便于更新。
|
||||
- 插件的config文件、使用说明`README.md`、`requirement.txt`等放置在插件目录中。
|
||||
- 默认优先级不要超过管理员插件`Godcmd`的优先级(999),`Godcmd`插件提供了配置管理、插件管理等功能。
|
||||
9
plugins/__init__.py
Normal file
9
plugins/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .plugin_manager import PluginManager
|
||||
from .event import *
|
||||
from .plugin import *
|
||||
|
||||
instance = PluginManager()
|
||||
|
||||
register = instance.register
|
||||
# load_plugins = instance.load_plugins
|
||||
# emit_event = instance.emit_event
|
||||
1
plugins/banwords/.gitignore
vendored
Normal file
1
plugins/banwords/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
banwords.txt
|
||||
9
plugins/banwords/README.md
Normal file
9
plugins/banwords/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 插件描述
|
||||
简易的敏感词插件,暂不支持分词,请自行导入词库到插件文件夹中的`banwords.txt`,每行一个词,一个参考词库是[1](https://github.com/cjh0613/tencent-sensitive-words/blob/main/sensitive_words_lines.txt)。
|
||||
|
||||
`config.json`中能够填写默认的处理行为,目前行为有:
|
||||
- `ignore` : 无视这条消息。
|
||||
- `replace` : 将消息中的敏感词替换成"*",并回复违规。
|
||||
|
||||
## 致谢
|
||||
搜索功能实现来自https://github.com/toolgood/ToolGood.Words
|
||||
250
plugins/banwords/WordsSearch.py
Normal file
250
plugins/banwords/WordsSearch.py
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
# ToolGood.Words.WordsSearch.py
|
||||
# 2020, Lin Zhijun, https://github.com/toolgood/ToolGood.Words
|
||||
# Licensed under the Apache License 2.0
|
||||
# 更新日志
|
||||
# 2020.04.06 第一次提交
|
||||
# 2020.05.16 修改,支持大于0xffff的字符
|
||||
|
||||
__all__ = ['WordsSearch']
|
||||
__author__ = 'Lin Zhijun'
|
||||
__date__ = '2020.05.16'
|
||||
|
||||
class TrieNode():
|
||||
def __init__(self):
|
||||
self.Index = 0
|
||||
self.Index = 0
|
||||
self.Layer = 0
|
||||
self.End = False
|
||||
self.Char = ''
|
||||
self.Results = []
|
||||
self.m_values = {}
|
||||
self.Failure = None
|
||||
self.Parent = None
|
||||
|
||||
def Add(self,c):
|
||||
if c in self.m_values :
|
||||
return self.m_values[c]
|
||||
node = TrieNode()
|
||||
node.Parent = self
|
||||
node.Char = c
|
||||
self.m_values[c] = node
|
||||
return node
|
||||
|
||||
def SetResults(self,index):
|
||||
if (self.End == False):
|
||||
self.End = True
|
||||
self.Results.append(index)
|
||||
|
||||
class TrieNode2():
|
||||
def __init__(self):
|
||||
self.End = False
|
||||
self.Results = []
|
||||
self.m_values = {}
|
||||
self.minflag = 0xffff
|
||||
self.maxflag = 0
|
||||
|
||||
def Add(self,c,node3):
|
||||
if (self.minflag > c):
|
||||
self.minflag = c
|
||||
if (self.maxflag < c):
|
||||
self.maxflag = c
|
||||
self.m_values[c] = node3
|
||||
|
||||
def SetResults(self,index):
|
||||
if (self.End == False) :
|
||||
self.End = True
|
||||
if (index in self.Results )==False :
|
||||
self.Results.append(index)
|
||||
|
||||
def HasKey(self,c):
|
||||
return c in self.m_values
|
||||
|
||||
|
||||
def TryGetValue(self,c):
|
||||
if (self.minflag <= c and self.maxflag >= c):
|
||||
if c in self.m_values:
|
||||
return self.m_values[c]
|
||||
return None
|
||||
|
||||
|
||||
class WordsSearch():
|
||||
def __init__(self):
|
||||
self._first = {}
|
||||
self._keywords = []
|
||||
self._indexs=[]
|
||||
|
||||
def SetKeywords(self,keywords):
|
||||
self._keywords = keywords
|
||||
self._indexs=[]
|
||||
for i in range(len(keywords)):
|
||||
self._indexs.append(i)
|
||||
|
||||
root = TrieNode()
|
||||
allNodeLayer={}
|
||||
|
||||
for i in range(len(self._keywords)): # for (i = 0; i < _keywords.length; i++)
|
||||
p = self._keywords[i]
|
||||
nd = root
|
||||
for j in range(len(p)): # for (j = 0; j < p.length; j++)
|
||||
nd = nd.Add(ord(p[j]))
|
||||
if (nd.Layer == 0):
|
||||
nd.Layer = j + 1
|
||||
if nd.Layer in allNodeLayer:
|
||||
allNodeLayer[nd.Layer].append(nd)
|
||||
else:
|
||||
allNodeLayer[nd.Layer]=[]
|
||||
allNodeLayer[nd.Layer].append(nd)
|
||||
nd.SetResults(i)
|
||||
|
||||
|
||||
allNode = []
|
||||
allNode.append(root)
|
||||
for key in allNodeLayer.keys():
|
||||
for nd in allNodeLayer[key]:
|
||||
allNode.append(nd)
|
||||
allNodeLayer=None
|
||||
|
||||
for i in range(len(allNode)): # for (i = 0; i < allNode.length; i++)
|
||||
if i==0 :
|
||||
continue
|
||||
nd=allNode[i]
|
||||
nd.Index = i
|
||||
r = nd.Parent.Failure
|
||||
c = nd.Char
|
||||
while (r != None and (c in r.m_values)==False):
|
||||
r = r.Failure
|
||||
if (r == None):
|
||||
nd.Failure = root
|
||||
else:
|
||||
nd.Failure = r.m_values[c]
|
||||
for key2 in nd.Failure.Results :
|
||||
nd.SetResults(key2)
|
||||
root.Failure = root
|
||||
|
||||
allNode2 = []
|
||||
for i in range(len(allNode)): # for (i = 0; i < allNode.length; i++)
|
||||
allNode2.append( TrieNode2())
|
||||
|
||||
for i in range(len(allNode2)): # for (i = 0; i < allNode2.length; i++)
|
||||
oldNode = allNode[i]
|
||||
newNode = allNode2[i]
|
||||
|
||||
for key in oldNode.m_values :
|
||||
index = oldNode.m_values[key].Index
|
||||
newNode.Add(key, allNode2[index])
|
||||
|
||||
for index in range(len(oldNode.Results)): # for (index = 0; index < oldNode.Results.length; index++)
|
||||
item = oldNode.Results[index]
|
||||
newNode.SetResults(item)
|
||||
|
||||
oldNode=oldNode.Failure
|
||||
while oldNode != root:
|
||||
for key in oldNode.m_values :
|
||||
if (newNode.HasKey(key) == False):
|
||||
index = oldNode.m_values[key].Index
|
||||
newNode.Add(key, allNode2[index])
|
||||
for index in range(len(oldNode.Results)):
|
||||
item = oldNode.Results[index]
|
||||
newNode.SetResults(item)
|
||||
oldNode=oldNode.Failure
|
||||
allNode = None
|
||||
root = None
|
||||
|
||||
# first = []
|
||||
# for index in range(65535):# for (index = 0; index < 0xffff; index++)
|
||||
# first.append(None)
|
||||
|
||||
# for key in allNode2[0].m_values :
|
||||
# first[key] = allNode2[0].m_values[key]
|
||||
|
||||
self._first = allNode2[0]
|
||||
|
||||
|
||||
def FindFirst(self,text):
|
||||
ptr = None
|
||||
for index in range(len(text)): # for (index = 0; index < text.length; index++)
|
||||
t =ord(text[index]) # text.charCodeAt(index)
|
||||
tn = None
|
||||
if (ptr == None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
else:
|
||||
tn = ptr.TryGetValue(t)
|
||||
if (tn==None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
|
||||
|
||||
if (tn != None):
|
||||
if (tn.End):
|
||||
item = tn.Results[0]
|
||||
keyword = self._keywords[item]
|
||||
return { "Keyword": keyword, "Success": True, "End": index, "Start": index + 1 - len(keyword), "Index": self._indexs[item] }
|
||||
ptr = tn
|
||||
return None
|
||||
|
||||
def FindAll(self,text):
|
||||
ptr = None
|
||||
list = []
|
||||
|
||||
for index in range(len(text)): # for (index = 0; index < text.length; index++)
|
||||
t =ord(text[index]) # text.charCodeAt(index)
|
||||
tn = None
|
||||
if (ptr == None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
else:
|
||||
tn = ptr.TryGetValue(t)
|
||||
if (tn==None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
|
||||
|
||||
if (tn != None):
|
||||
if (tn.End):
|
||||
for j in range(len(tn.Results)): # for (j = 0; j < tn.Results.length; j++)
|
||||
item = tn.Results[j]
|
||||
keyword = self._keywords[item]
|
||||
list.append({ "Keyword": keyword, "Success": True, "End": index, "Start": index + 1 - len(keyword), "Index": self._indexs[item] })
|
||||
ptr = tn
|
||||
return list
|
||||
|
||||
|
||||
def ContainsAny(self,text):
|
||||
ptr = None
|
||||
for index in range(len(text)): # for (index = 0; index < text.length; index++)
|
||||
t =ord(text[index]) # text.charCodeAt(index)
|
||||
tn = None
|
||||
if (ptr == None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
else:
|
||||
tn = ptr.TryGetValue(t)
|
||||
if (tn==None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
|
||||
if (tn != None):
|
||||
if (tn.End):
|
||||
return True
|
||||
ptr = tn
|
||||
return False
|
||||
|
||||
def Replace(self,text, replaceChar = '*'):
|
||||
result = list(text)
|
||||
|
||||
ptr = None
|
||||
for i in range(len(text)): # for (i = 0; i < text.length; i++)
|
||||
t =ord(text[i]) # text.charCodeAt(index)
|
||||
tn = None
|
||||
if (ptr == None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
else:
|
||||
tn = ptr.TryGetValue(t)
|
||||
if (tn==None):
|
||||
tn = self._first.TryGetValue(t)
|
||||
|
||||
if (tn != None):
|
||||
if (tn.End):
|
||||
maxLength = len( self._keywords[tn.Results[0]])
|
||||
start = i + 1 - maxLength
|
||||
for j in range(start,i+1): # for (j = start; j <= i; j++)
|
||||
result[j] = replaceChar
|
||||
ptr = tn
|
||||
return ''.join(result)
|
||||
0
plugins/banwords/__init__.py
Normal file
0
plugins/banwords/__init__.py
Normal file
66
plugins/banwords/banwords.py
Normal file
66
plugins/banwords/banwords.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import json
|
||||
import os
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common.log import logger
|
||||
from .WordsSearch import WordsSearch
|
||||
|
||||
|
||||
@plugins.register(name="Banwords", desc="判断消息中是否有敏感词、决定是否回复。", version="1.0", author="lanvent", desire_priority= 100)
|
||||
class Banwords(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
try:
|
||||
curdir=os.path.dirname(__file__)
|
||||
config_path=os.path.join(curdir,"config.json")
|
||||
conf=None
|
||||
if not os.path.exists(config_path):
|
||||
conf={"action":"ignore"}
|
||||
with open(config_path,"w") as f:
|
||||
json.dump(conf,f,indent=4)
|
||||
else:
|
||||
with open(config_path,"r") as f:
|
||||
conf=json.load(f)
|
||||
self.searchr = WordsSearch()
|
||||
self.action = conf["action"]
|
||||
banwords_path = os.path.join(curdir,"banwords.txt")
|
||||
with open(banwords_path, 'r', encoding='utf-8') as f:
|
||||
words=[]
|
||||
for line in f:
|
||||
word = line.strip()
|
||||
if word:
|
||||
words.append(word)
|
||||
self.searchr.SetKeywords(words)
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[Banwords] inited")
|
||||
except Exception as e:
|
||||
logger.warn("Banwords init failed: %s, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/banwords ." % e)
|
||||
|
||||
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
|
||||
if e_context['context'].type not in [ContextType.TEXT,ContextType.IMAGE_CREATE]:
|
||||
return
|
||||
|
||||
content = e_context['context'].content
|
||||
logger.debug("[Banwords] on_handle_context. content: %s" % content)
|
||||
if self.action == "ignore":
|
||||
f = self.searchr.FindFirst(content)
|
||||
if f:
|
||||
logger.info("Banwords: %s" % f["Keyword"])
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
elif self.action == "replace":
|
||||
if self.searchr.ContainsAny(content):
|
||||
reply = Reply(ReplyType.INFO, "发言中包含敏感词,请重试: \n"+self.searchr.Replace(content))
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
return Banwords.desc
|
||||
3
plugins/banwords/banwords.txt.template
Normal file
3
plugins/banwords/banwords.txt.template
Normal file
@@ -0,0 +1,3 @@
|
||||
nipples
|
||||
pennis
|
||||
法轮功
|
||||
3
plugins/banwords/config.json.template
Normal file
3
plugins/banwords/config.json.template
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"action": "ignore"
|
||||
}
|
||||
30
plugins/bdunit/README.md
Normal file
30
plugins/bdunit/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## 插件说明
|
||||
|
||||
利用百度UNIT实现智能对话
|
||||
|
||||
- 1.解决问题:chatgpt无法处理的指令,交给百度UNIT处理如:天气,日期时间,数学运算等
|
||||
- 2.如问时间:现在几点钟,今天几号
|
||||
- 3.如问天气:明天广州天气怎么样,这个周末深圳会不会下雨
|
||||
- 4.如问数学运算:23+45=多少,100-23=多少,35转化为二进制是多少?
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 获取apikey
|
||||
|
||||
在百度UNIT官网上自己创建应用,申请百度机器人,可以把预先训练好的模型导入到自己的应用中,
|
||||
|
||||
see https://ai.baidu.com/unit/home#/home?track=61fe1b0d3407ce3face1d92cb5c291087095fc10c8377aaf https://console.bce.baidu.com/ai平台申请
|
||||
|
||||
### 配置文件
|
||||
|
||||
将文件夹中`config.json.template`复制为`config.json`。
|
||||
|
||||
在其中填写百度UNIT官网上获取应用的API Key和Secret Key
|
||||
|
||||
``` json
|
||||
{
|
||||
"service_id": "s...", #"机器人ID"
|
||||
"api_key": "",
|
||||
"secret_key": ""
|
||||
}
|
||||
```
|
||||
0
plugins/bdunit/__init__.py
Normal file
0
plugins/bdunit/__init__.py
Normal file
294
plugins/bdunit/bdunit.py
Normal file
294
plugins/bdunit/bdunit.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# encoding:utf-8
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import requests
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from common.log import logger
|
||||
import plugins
|
||||
from plugins import *
|
||||
from uuid import getnode as get_mac
|
||||
|
||||
|
||||
"""利用百度UNIT实现智能对话
|
||||
如果命中意图,返回意图对应的回复,否则返回继续交付给下个插件处理
|
||||
"""
|
||||
|
||||
|
||||
@plugins.register(name="BDunit", desc="Baidu unit bot system", version="0.1", author="jackson", desire_priority=0)
|
||||
class BDunit(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
try:
|
||||
curdir = os.path.dirname(__file__)
|
||||
config_path = os.path.join(curdir, "config.json")
|
||||
conf = None
|
||||
if not os.path.exists(config_path):
|
||||
raise Exception("config.json not found")
|
||||
else:
|
||||
with open(config_path, "r") as f:
|
||||
conf = json.load(f)
|
||||
self.service_id = conf["service_id"]
|
||||
self.api_key = conf["api_key"]
|
||||
self.secret_key = conf["secret_key"]
|
||||
self.access_token = self.get_token()
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[BDunit] inited")
|
||||
except Exception as e:
|
||||
logger.warn(
|
||||
"BDunit init failed: %s, ignore " % e)
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
|
||||
if e_context['context'].type != ContextType.TEXT:
|
||||
return
|
||||
|
||||
content = e_context['context'].content
|
||||
logger.debug("[BDunit] on_handle_context. content: %s" % content)
|
||||
parsed = self.getUnit2(content)
|
||||
intent = self.getIntent(parsed)
|
||||
if intent: # 找到意图
|
||||
logger.debug("[BDunit] Baidu_AI Intent= %s", intent)
|
||||
reply = Reply()
|
||||
reply.type = ReplyType.TEXT
|
||||
reply.content = self.getSay(parsed)
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
|
||||
else:
|
||||
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
help_text = "本插件会处理询问实时日期时间,天气,数学运算等问题,这些技能由您的百度智能对话UNIT决定\n"
|
||||
return help_text
|
||||
|
||||
def get_token(self):
|
||||
"""获取访问百度UUNIT 的access_token
|
||||
#param api_key: UNIT apk_key
|
||||
#param secret_key: UNIT secret_key
|
||||
Returns:
|
||||
string: access_token
|
||||
"""
|
||||
url = "https://aip.baidubce.com/oauth/2.0/token?client_id={}&client_secret={}&grant_type=client_credentials".format(
|
||||
self.api_key, self.secret_key)
|
||||
payload = ""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
|
||||
# print(response.text)
|
||||
return response.json()['access_token']
|
||||
|
||||
def getUnit(self, query):
|
||||
"""
|
||||
NLU 解析version 3.0
|
||||
:param query: 用户的指令字符串
|
||||
:returns: UNIT 解析结果。如果解析失败,返回 None
|
||||
"""
|
||||
|
||||
url = (
|
||||
'https://aip.baidubce.com/rpc/2.0/unit/service/v3/chat?access_token='
|
||||
+ self.access_token
|
||||
)
|
||||
request = {"query": query, "user_id": str(
|
||||
get_mac())[:32], "terminal_id": "88888"}
|
||||
body = {
|
||||
"log_id": str(uuid.uuid1()),
|
||||
"version": "3.0",
|
||||
"service_id": self.service_id,
|
||||
"session_id": str(uuid.uuid1()),
|
||||
"request": request,
|
||||
}
|
||||
try:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url, json=body, headers=headers)
|
||||
return json.loads(response.text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getUnit2(self, query):
|
||||
"""
|
||||
NLU 解析 version 2.0
|
||||
|
||||
:param query: 用户的指令字符串
|
||||
:returns: UNIT 解析结果。如果解析失败,返回 None
|
||||
"""
|
||||
url = (
|
||||
"https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token="
|
||||
+ self.access_token
|
||||
)
|
||||
request = {"query": query, "user_id": str(get_mac())[:32]}
|
||||
body = {
|
||||
"log_id": str(uuid.uuid1()),
|
||||
"version": "2.0",
|
||||
"service_id": self.service_id,
|
||||
"session_id": str(uuid.uuid1()),
|
||||
"request": request,
|
||||
}
|
||||
try:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url, json=body, headers=headers)
|
||||
return json.loads(response.text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getIntent(self, parsed):
|
||||
"""
|
||||
提取意图
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:returns: 意图数组
|
||||
"""
|
||||
if (
|
||||
parsed
|
||||
and "result" in parsed
|
||||
and "response_list" in parsed["result"]
|
||||
):
|
||||
try:
|
||||
return parsed["result"]["response_list"][0]["schema"]["intent"]
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return ""
|
||||
else:
|
||||
return ""
|
||||
|
||||
def hasIntent(self, parsed, intent):
|
||||
"""
|
||||
判断是否包含某个意图
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:param intent: 意图的名称
|
||||
:returns: True: 包含; False: 不包含
|
||||
"""
|
||||
if (
|
||||
parsed
|
||||
and "result" in parsed
|
||||
and "response_list" in parsed["result"]
|
||||
):
|
||||
response_list = parsed["result"]["response_list"]
|
||||
for response in response_list:
|
||||
if (
|
||||
"schema" in response
|
||||
and "intent" in response["schema"]
|
||||
and response["schema"]["intent"] == intent
|
||||
):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def getSlots(self, parsed, intent=""):
|
||||
"""
|
||||
提取某个意图的所有词槽
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:param intent: 意图的名称
|
||||
:returns: 词槽列表。你可以通过 name 属性筛选词槽,
|
||||
再通过 normalized_word 属性取出相应的值
|
||||
"""
|
||||
if (
|
||||
parsed
|
||||
and "result" in parsed
|
||||
and "response_list" in parsed["result"]
|
||||
):
|
||||
response_list = parsed["result"]["response_list"]
|
||||
if intent == "":
|
||||
try:
|
||||
return parsed["result"]["response_list"][0]["schema"]["slots"]
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return []
|
||||
for response in response_list:
|
||||
if (
|
||||
"schema" in response
|
||||
and "intent" in response["schema"]
|
||||
and "slots" in response["schema"]
|
||||
and response["schema"]["intent"] == intent
|
||||
):
|
||||
return response["schema"]["slots"]
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def getSlotWords(self, parsed, intent, name):
|
||||
"""
|
||||
找出命中某个词槽的内容
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:param intent: 意图的名称
|
||||
:param name: 词槽名
|
||||
:returns: 命中该词槽的值的列表。
|
||||
"""
|
||||
slots = self.getSlots(parsed, intent)
|
||||
words = []
|
||||
for slot in slots:
|
||||
if slot["name"] == name:
|
||||
words.append(slot["normalized_word"])
|
||||
return words
|
||||
|
||||
def getSayByConfidence(self, parsed):
|
||||
"""
|
||||
提取 UNIT 置信度最高的回复文本
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:returns: UNIT 的回复文本
|
||||
"""
|
||||
if (
|
||||
parsed
|
||||
and "result" in parsed
|
||||
and "response_list" in parsed["result"]
|
||||
):
|
||||
response_list = parsed["result"]["response_list"]
|
||||
answer = {}
|
||||
for response in response_list:
|
||||
if (
|
||||
"schema" in response
|
||||
and "intent_confidence" in response["schema"]
|
||||
and (
|
||||
not answer
|
||||
or response["schema"]["intent_confidence"]
|
||||
> answer["schema"]["intent_confidence"]
|
||||
)
|
||||
):
|
||||
answer = response
|
||||
return answer["action_list"][0]["say"]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def getSay(self, parsed, intent=""):
|
||||
"""
|
||||
提取 UNIT 的回复文本
|
||||
|
||||
:param parsed: UNIT 解析结果
|
||||
:param intent: 意图的名称
|
||||
:returns: UNIT 的回复文本
|
||||
"""
|
||||
if (
|
||||
parsed
|
||||
and "result" in parsed
|
||||
and "response_list" in parsed["result"]
|
||||
):
|
||||
response_list = parsed["result"]["response_list"]
|
||||
if intent == "":
|
||||
try:
|
||||
return response_list[0]["action_list"][0]["say"]
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return ""
|
||||
for response in response_list:
|
||||
if (
|
||||
"schema" in response
|
||||
and "intent" in response["schema"]
|
||||
and response["schema"]["intent"] == intent
|
||||
):
|
||||
try:
|
||||
return response["action_list"][0]["say"]
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return ""
|
||||
return ""
|
||||
else:
|
||||
return ""
|
||||
5
plugins/bdunit/config.json.template
Normal file
5
plugins/bdunit/config.json.template
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"service_id": "s...",
|
||||
"api_key": "",
|
||||
"secret_key": ""
|
||||
}
|
||||
4
plugins/dungeon/README.md
Normal file
4
plugins/dungeon/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
玩地牢游戏的聊天插件,触发方法如下:
|
||||
|
||||
- `$开始冒险 <背景故事>` - 以<背景故事>开始一个地牢游戏,不填写会使用默认背景故事。之后聊天中你的所有消息会帮助ai完善这个故事。
|
||||
- `$停止冒险` - 停止一个地牢游戏,回归正常的ai。
|
||||
0
plugins/dungeon/__init__.py
Normal file
0
plugins/dungeon/__init__.py
Normal file
86
plugins/dungeon/dungeon.py
Normal file
86
plugins/dungeon/dungeon.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from common.expired_dict import ExpiredDict
|
||||
from config import conf
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common.log import logger
|
||||
from common import const
|
||||
|
||||
# https://github.com/bupticybee/ChineseAiDungeonChatGPT
|
||||
class StoryTeller():
|
||||
def __init__(self, bot, sessionid, story):
|
||||
self.bot = bot
|
||||
self.sessionid = sessionid
|
||||
bot.sessions.clear_session(sessionid)
|
||||
self.first_interact = True
|
||||
self.story = story
|
||||
|
||||
def reset(self):
|
||||
self.bot.sessions.clear_session(self.sessionid)
|
||||
self.first_interact = True
|
||||
|
||||
def action(self, user_action):
|
||||
if user_action[-1] != "。":
|
||||
user_action = user_action + "。"
|
||||
if self.first_interact:
|
||||
prompt = """现在来充当一个冒险文字游戏,描述时候注意节奏,不要太快,仔细描述各个人物的心情和周边环境。一次只需写四到六句话。
|
||||
开头是,""" + self.story + " " + user_action
|
||||
self.first_interact = False
|
||||
else:
|
||||
prompt = """继续,一次只需要续写四到六句话,总共就只讲5分钟内发生的事情。""" + user_action
|
||||
return prompt
|
||||
|
||||
|
||||
@plugins.register(name="Dungeon", desc="A plugin to play dungeon game", version="1.0", author="lanvent", desire_priority= 0)
|
||||
class Dungeon(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[Dungeon] inited")
|
||||
# 目前没有设计session过期事件,这里先暂时使用过期字典
|
||||
if conf().get('expires_in_seconds'):
|
||||
self.games = ExpiredDict(conf().get('expires_in_seconds'))
|
||||
else:
|
||||
self.games = dict()
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
|
||||
if e_context['context'].type != ContextType.TEXT:
|
||||
return
|
||||
bottype = Bridge().get_bot_type("chat")
|
||||
if bottype not in (const.CHATGPT, const.OPEN_AI):
|
||||
return
|
||||
bot = Bridge().get_bot("chat")
|
||||
content = e_context['context'].content[:]
|
||||
clist = e_context['context'].content.split(maxsplit=1)
|
||||
sessionid = e_context['context']['session_id']
|
||||
logger.debug("[Dungeon] on_handle_context. content: %s" % clist)
|
||||
if clist[0] == "$停止冒险":
|
||||
if sessionid in self.games:
|
||||
self.games[sessionid].reset()
|
||||
del self.games[sessionid]
|
||||
reply = Reply(ReplyType.INFO, "冒险结束!")
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
elif clist[0] == "$开始冒险" or sessionid in self.games:
|
||||
if sessionid not in self.games or clist[0] == "$开始冒险":
|
||||
if len(clist)>1 :
|
||||
story = clist[1]
|
||||
else:
|
||||
story = "你在树林里冒险,指不定会从哪里蹦出来一些奇怪的东西,你握紧手上的手枪,希望这次冒险能够找到一些值钱的东西,你往树林深处走去。"
|
||||
self.games[sessionid] = StoryTeller(bot, sessionid, story)
|
||||
reply = Reply(ReplyType.INFO, "冒险开始,你可以输入任意内容,让故事继续下去。故事背景是:" + story)
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
|
||||
else:
|
||||
prompt = self.games[sessionid].action(content)
|
||||
e_context['context'].type = ContextType.TEXT
|
||||
e_context['context'].content = prompt
|
||||
e_context.action = EventAction.BREAK # 事件结束,不跳过处理context的默认逻辑
|
||||
def get_help_text(self, **kwargs):
|
||||
help_text = "输入\"$开始冒险 {背景故事}\"来以{背景故事}开始一个地牢游戏,之后你的所有消息会帮助我来完善这个故事。输入\"$停止冒险 \"可以结束游戏。"
|
||||
return help_text
|
||||
49
plugins/event.py
Normal file
49
plugins/event.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Event(Enum):
|
||||
# ON_RECEIVE_MESSAGE = 1 # 收到消息
|
||||
|
||||
ON_HANDLE_CONTEXT = 2 # 处理消息前
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 }
|
||||
"""
|
||||
|
||||
ON_DECORATE_REPLY = 3 # 得到回复后准备装饰
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 }
|
||||
"""
|
||||
|
||||
ON_SEND_REPLY = 4 # 发送回复前
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 }
|
||||
"""
|
||||
|
||||
# AFTER_SEND_REPLY = 5 # 发送回复后
|
||||
|
||||
|
||||
class EventAction(Enum):
|
||||
CONTINUE = 1 # 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑
|
||||
BREAK = 2 # 事件结束,不再给下个插件处理,交付给默认的事件处理逻辑
|
||||
BREAK_PASS = 3 # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑
|
||||
|
||||
|
||||
class EventContext:
|
||||
def __init__(self, event, econtext=dict()):
|
||||
self.event = event
|
||||
self.econtext = econtext
|
||||
self.action = EventAction.CONTINUE
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.econtext[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.econtext[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.econtext[key]
|
||||
|
||||
def is_pass(self):
|
||||
return self.action == EventAction.BREAK_PASS
|
||||
12
plugins/godcmd/README.md
Normal file
12
plugins/godcmd/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## 插件说明
|
||||
|
||||
指令插件
|
||||
|
||||
## 插件使用
|
||||
|
||||
将`config.json.template`复制为`config.json`,并修改其中`password`的值为口令。
|
||||
|
||||
在私聊中可使用`#auth`指令,输入口令进行管理员认证,详细指令请输入`#help`查看帮助文档:
|
||||
|
||||
`#auth <口令>` - 管理员认证。
|
||||
`#help` - 输出帮助文档,是否是管理员和是否是在群聊中会影响帮助文档的输出内容。
|
||||
0
plugins/godcmd/__init__.py
Normal file
0
plugins/godcmd/__init__.py
Normal file
4
plugins/godcmd/config.json.template
Normal file
4
plugins/godcmd/config.json.template
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"password": "",
|
||||
"admin_users": []
|
||||
}
|
||||
307
plugins/godcmd/godcmd.py
Normal file
307
plugins/godcmd/godcmd.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from typing import Tuple
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from config import load_config
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common import const
|
||||
from common.log import logger
|
||||
|
||||
# 定义指令集
|
||||
COMMANDS = {
|
||||
"help": {
|
||||
"alias": ["help", "帮助"],
|
||||
"desc": "打印指令集合",
|
||||
},
|
||||
"helpp": {
|
||||
"alias": ["helpp", "插件帮助"],
|
||||
"args": ["插件名"],
|
||||
"desc": "打印插件的帮助信息",
|
||||
},
|
||||
"auth": {
|
||||
"alias": ["auth", "认证"],
|
||||
"args": ["口令"],
|
||||
"desc": "管理员认证",
|
||||
},
|
||||
# "id": {
|
||||
# "alias": ["id", "用户"],
|
||||
# "desc": "获取用户id", #目前无实际意义
|
||||
# },
|
||||
"reset": {
|
||||
"alias": ["reset", "重置会话"],
|
||||
"desc": "重置会话",
|
||||
},
|
||||
}
|
||||
|
||||
ADMIN_COMMANDS = {
|
||||
"resume": {
|
||||
"alias": ["resume", "恢复服务"],
|
||||
"desc": "恢复服务",
|
||||
},
|
||||
"stop": {
|
||||
"alias": ["stop", "暂停服务"],
|
||||
"desc": "暂停服务",
|
||||
},
|
||||
"reconf": {
|
||||
"alias": ["reconf", "重载配置"],
|
||||
"desc": "重载配置(不包含插件配置)",
|
||||
},
|
||||
"resetall": {
|
||||
"alias": ["resetall", "重置所有会话"],
|
||||
"desc": "重置所有会话",
|
||||
},
|
||||
"scanp": {
|
||||
"alias": ["scanp", "扫描插件"],
|
||||
"desc": "扫描插件目录是否有新插件",
|
||||
},
|
||||
"plist": {
|
||||
"alias": ["plist", "插件"],
|
||||
"desc": "打印当前插件列表",
|
||||
},
|
||||
"setpri": {
|
||||
"alias": ["setpri", "设置插件优先级"],
|
||||
"args": ["插件名", "优先级"],
|
||||
"desc": "设置指定插件的优先级,越大越优先",
|
||||
},
|
||||
"reloadp": {
|
||||
"alias": ["reloadp", "重载插件"],
|
||||
"args": ["插件名"],
|
||||
"desc": "重载指定插件配置",
|
||||
},
|
||||
"enablep": {
|
||||
"alias": ["enablep", "启用插件"],
|
||||
"args": ["插件名"],
|
||||
"desc": "启用指定插件",
|
||||
},
|
||||
"disablep": {
|
||||
"alias": ["disablep", "禁用插件"],
|
||||
"args": ["插件名"],
|
||||
"desc": "禁用指定插件",
|
||||
},
|
||||
"debug": {
|
||||
"alias": ["debug", "调试模式", "DEBUG"],
|
||||
"desc": "开启机器调试日志",
|
||||
},
|
||||
}
|
||||
# 定义帮助函数
|
||||
def get_help_text(isadmin, isgroup):
|
||||
help_text = "可用指令:\n"
|
||||
for cmd, info in COMMANDS.items():
|
||||
if cmd=="auth" and (isadmin or isgroup): # 群聊不可认证
|
||||
continue
|
||||
|
||||
alias=["#"+a for a in info['alias']]
|
||||
help_text += f"{','.join(alias)} "
|
||||
if 'args' in info:
|
||||
args=["{"+a+"}" for a in info['args']]
|
||||
help_text += f"{' '.join(args)} "
|
||||
help_text += f": {info['desc']}\n"
|
||||
if ADMIN_COMMANDS and isadmin:
|
||||
help_text += "\n管理员指令:\n"
|
||||
for cmd, info in ADMIN_COMMANDS.items():
|
||||
alias=["#"+a for a in info['alias']]
|
||||
help_text += f"{','.join(alias)} "
|
||||
help_text += f": {info['desc']}\n"
|
||||
return help_text
|
||||
|
||||
@plugins.register(name="Godcmd", desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证", version="1.0", author="lanvent", desire_priority= 999)
|
||||
class Godcmd(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
curdir=os.path.dirname(__file__)
|
||||
config_path=os.path.join(curdir,"config.json")
|
||||
gconf=None
|
||||
if not os.path.exists(config_path):
|
||||
gconf={"password":"","admin_users":[]}
|
||||
with open(config_path,"w") as f:
|
||||
json.dump(gconf,f,indent=4)
|
||||
else:
|
||||
with open(config_path,"r") as f:
|
||||
gconf=json.load(f)
|
||||
|
||||
self.password = gconf["password"]
|
||||
self.admin_users = gconf["admin_users"] # 预存的管理员账号,这些账号不需要认证 TODO: 用户名每次都会变,目前不可用
|
||||
self.isrunning = True # 机器人是否运行中
|
||||
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[Godcmd] inited")
|
||||
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
context_type = e_context['context'].type
|
||||
if context_type != ContextType.TEXT:
|
||||
if not self.isrunning:
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
|
||||
content = e_context['context'].content
|
||||
logger.debug("[Godcmd] on_handle_context. content: %s" % content)
|
||||
if content.startswith("#"):
|
||||
# msg = e_context['context']['msg']
|
||||
user = e_context['context']['receiver']
|
||||
session_id = e_context['context']['session_id']
|
||||
isgroup = e_context['context']['isgroup']
|
||||
bottype = Bridge().get_bot_type("chat")
|
||||
bot = Bridge().get_bot("chat")
|
||||
# 将命令和参数分割
|
||||
command_parts = content[1:].split(" ")
|
||||
cmd = command_parts[0]
|
||||
args = command_parts[1:]
|
||||
isadmin=False
|
||||
if user in self.admin_users:
|
||||
isadmin=True
|
||||
ok=False
|
||||
result="string"
|
||||
if any(cmd in info['alias'] for info in COMMANDS.values()):
|
||||
cmd = next(c for c, info in COMMANDS.items() if cmd in info['alias'])
|
||||
if cmd == "auth":
|
||||
ok, result = self.authenticate(user, args, isadmin, isgroup)
|
||||
elif cmd == "help":
|
||||
ok, result = True, get_help_text(isadmin, isgroup)
|
||||
elif cmd == "helpp":
|
||||
if len(args) != 1:
|
||||
ok, result = False, "请提供插件名"
|
||||
else:
|
||||
plugins = PluginManager().list_plugins()
|
||||
name = args[0].upper()
|
||||
if name in plugins and plugins[name].enabled:
|
||||
ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin)
|
||||
else:
|
||||
ok, result= False, "插件不存在或未启用"
|
||||
elif cmd == "id":
|
||||
ok, result = True, f"用户id=\n{user}"
|
||||
elif cmd == "reset":
|
||||
if bottype in (const.CHATGPT, const.OPEN_AI):
|
||||
bot.sessions.clear_session(session_id)
|
||||
ok, result = True, "会话已重置"
|
||||
else:
|
||||
ok, result = False, "当前对话机器人不支持重置会话"
|
||||
logger.debug("[Godcmd] command: %s by %s" % (cmd, user))
|
||||
elif any(cmd in info['alias'] for info in ADMIN_COMMANDS.values()):
|
||||
if isadmin:
|
||||
if isgroup:
|
||||
ok, result = False, "群聊不可执行管理员指令"
|
||||
else:
|
||||
cmd = next(c for c, info in ADMIN_COMMANDS.items() if cmd in info['alias'])
|
||||
if cmd == "stop":
|
||||
self.isrunning = False
|
||||
ok, result = True, "服务已暂停"
|
||||
elif cmd == "resume":
|
||||
self.isrunning = True
|
||||
ok, result = True, "服务已恢复"
|
||||
elif cmd == "reconf":
|
||||
load_config()
|
||||
ok, result = True, "配置已重载"
|
||||
elif cmd == "resetall":
|
||||
if bottype in (const.CHATGPT, const.OPEN_AI):
|
||||
bot.sessions.clear_all_session()
|
||||
ok, result = True, "重置所有会话成功"
|
||||
else:
|
||||
ok, result = False, "当前对话机器人不支持重置会话"
|
||||
elif cmd == "debug":
|
||||
logger.setLevel('DEBUG')
|
||||
ok, result = True, "DEBUG模式已开启"
|
||||
elif cmd == "plist":
|
||||
plugins = PluginManager().list_plugins()
|
||||
ok = True
|
||||
result = "插件列表:\n"
|
||||
for name,plugincls in plugins.items():
|
||||
result += f"{plugincls.name}_v{plugincls.version} {plugincls.priority} - "
|
||||
if plugincls.enabled:
|
||||
result += "已启用\n"
|
||||
else:
|
||||
result += "未启用\n"
|
||||
elif cmd == "scanp":
|
||||
new_plugins = PluginManager().scan_plugins()
|
||||
ok, result = True, "插件扫描完成"
|
||||
PluginManager().activate_plugins()
|
||||
if len(new_plugins) >0 :
|
||||
result += "\n发现新插件:\n"
|
||||
result += "\n".join([f"{p.name}_v{p.version}" for p in new_plugins])
|
||||
else :
|
||||
result +=", 未发现新插件"
|
||||
elif cmd == "setpri":
|
||||
if len(args) != 2:
|
||||
ok, result = False, "请提供插件名和优先级"
|
||||
else:
|
||||
ok = PluginManager().set_plugin_priority(args[0], int(args[1]))
|
||||
if ok:
|
||||
result = "插件" + args[0] + "优先级已设置为" + args[1]
|
||||
else:
|
||||
result = "插件不存在"
|
||||
elif cmd == "reloadp":
|
||||
if len(args) != 1:
|
||||
ok, result = False, "请提供插件名"
|
||||
else:
|
||||
ok = PluginManager().reload_plugin(args[0])
|
||||
if ok:
|
||||
result = "插件配置已重载"
|
||||
else:
|
||||
result = "插件不存在"
|
||||
elif cmd == "enablep":
|
||||
if len(args) != 1:
|
||||
ok, result = False, "请提供插件名"
|
||||
else:
|
||||
ok = PluginManager().enable_plugin(args[0])
|
||||
if ok:
|
||||
result = "插件已启用"
|
||||
else:
|
||||
result = "插件不存在"
|
||||
elif cmd == "disablep":
|
||||
if len(args) != 1:
|
||||
ok, result = False, "请提供插件名"
|
||||
else:
|
||||
ok = PluginManager().disable_plugin(args[0])
|
||||
if ok:
|
||||
result = "插件已禁用"
|
||||
else:
|
||||
result = "插件不存在"
|
||||
|
||||
logger.debug("[Godcmd] admin command: %s by %s" % (cmd, user))
|
||||
else:
|
||||
ok, result = False, "需要管理员权限才能执行该指令"
|
||||
else:
|
||||
ok, result = False, f"未知指令:{cmd}\n查看指令列表请输入#help \n"
|
||||
|
||||
reply = Reply()
|
||||
if ok:
|
||||
reply.type = ReplyType.INFO
|
||||
else:
|
||||
reply.type = ReplyType.ERROR
|
||||
reply.content = result
|
||||
e_context['reply'] = reply
|
||||
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
|
||||
elif not self.isrunning:
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
|
||||
def authenticate(self, userid, args, isadmin, isgroup) -> Tuple[bool,str] :
|
||||
if isgroup:
|
||||
return False,"请勿在群聊中认证"
|
||||
|
||||
if isadmin:
|
||||
return False,"管理员账号无需认证"
|
||||
|
||||
if len(self.password) == 0:
|
||||
return False,"未设置口令,无法认证"
|
||||
|
||||
if len(args) != 1:
|
||||
return False,"请提供口令"
|
||||
|
||||
password = args[0]
|
||||
if password == self.password:
|
||||
self.admin_users.append(userid)
|
||||
return True,"认证成功"
|
||||
else:
|
||||
return False,"认证失败"
|
||||
|
||||
def get_help_text(self, isadmin = False, isgroup = False, **kwargs):
|
||||
return get_help_text(isadmin, isgroup)
|
||||
0
plugins/hello/__init__.py
Normal file
0
plugins/hello/__init__.py
Normal file
50
plugins/hello/hello.py
Normal file
50
plugins/hello/hello.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common.log import logger
|
||||
|
||||
|
||||
@plugins.register(name="Hello", desc="A simple plugin that says hello", version="0.1", author="lanvent", desire_priority= -1)
|
||||
class Hello(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
logger.info("[Hello] inited")
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
|
||||
if e_context['context'].type != ContextType.TEXT:
|
||||
return
|
||||
|
||||
content = e_context['context'].content
|
||||
logger.debug("[Hello] on_handle_context. content: %s" % content)
|
||||
if content == "Hello":
|
||||
reply = Reply()
|
||||
reply.type = ReplyType.TEXT
|
||||
msg = e_context['context']['msg']
|
||||
if e_context['context']['isgroup']:
|
||||
reply.content = "Hello, " + msg['ActualNickName'] + " from " + msg['User'].get('NickName', "Group")
|
||||
else:
|
||||
reply.content = "Hello, " + msg['User'].get('NickName', "My friend")
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
|
||||
|
||||
if content == "Hi":
|
||||
reply = Reply()
|
||||
reply.type = ReplyType.TEXT
|
||||
reply.content = "Hi"
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑,一般会覆写reply
|
||||
|
||||
if content == "End":
|
||||
# 如果是文本消息"End",将请求转换成"IMAGE_CREATE",并将content设置为"The World"
|
||||
e_context['context'].type = ContextType.IMAGE_CREATE
|
||||
content = "The World"
|
||||
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
help_text = "输入Hello,我会回复你的名字\n输入End,我会回复你世界的图片\n"
|
||||
return help_text
|
||||
6
plugins/plugin.py
Normal file
6
plugins/plugin.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class Plugin:
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
return "暂无帮助信息"
|
||||
175
plugins/plugin_manager.py
Normal file
175
plugins/plugin_manager.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from common.singleton import singleton
|
||||
from common.sorted_dict import SortedDict
|
||||
from .event import *
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
@singleton
|
||||
class PluginManager:
|
||||
def __init__(self):
|
||||
self.plugins = SortedDict(lambda k,v: v.priority,reverse=True)
|
||||
self.listening_plugins = {}
|
||||
self.instances = {}
|
||||
self.pconf = {}
|
||||
|
||||
def register(self, name: str, desc: str, version: str, author: str, desire_priority: int = 0):
|
||||
def wrapper(plugincls):
|
||||
plugincls.name = name
|
||||
plugincls.desc = desc
|
||||
plugincls.version = version
|
||||
plugincls.author = author
|
||||
plugincls.priority = desire_priority
|
||||
plugincls.enabled = True
|
||||
self.plugins[name.upper()] = plugincls
|
||||
logger.info("Plugin %s_v%s registered" % (name, version))
|
||||
return plugincls
|
||||
return wrapper
|
||||
|
||||
def save_config(self):
|
||||
with open("./plugins/plugins.json", "w", encoding="utf-8") as f:
|
||||
json.dump(self.pconf, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def load_config(self):
|
||||
logger.info("Loading plugins config...")
|
||||
|
||||
modified = False
|
||||
if os.path.exists("./plugins/plugins.json"):
|
||||
with open("./plugins/plugins.json", "r", encoding="utf-8") as f:
|
||||
pconf = json.load(f)
|
||||
pconf['plugins'] = SortedDict(lambda k,v: v["priority"],pconf['plugins'],reverse=True)
|
||||
else:
|
||||
modified = True
|
||||
pconf = {"plugins": SortedDict(lambda k,v: v["priority"],reverse=True)}
|
||||
self.pconf = pconf
|
||||
if modified:
|
||||
self.save_config()
|
||||
return pconf
|
||||
|
||||
def scan_plugins(self):
|
||||
logger.info("Scaning plugins ...")
|
||||
plugins_dir = "./plugins"
|
||||
for plugin_name in os.listdir(plugins_dir):
|
||||
plugin_path = os.path.join(plugins_dir, plugin_name)
|
||||
if os.path.isdir(plugin_path):
|
||||
# 判断插件是否包含同名.py文件
|
||||
main_module_path = os.path.join(plugin_path, plugin_name+".py")
|
||||
if os.path.isfile(main_module_path):
|
||||
# 导入插件
|
||||
import_path = "plugins.{}.{}".format(plugin_name, plugin_name)
|
||||
try:
|
||||
main_module = importlib.import_module(import_path)
|
||||
except Exception as e:
|
||||
logger.warn("Failed to import plugin %s: %s" % (plugin_name, e))
|
||||
continue
|
||||
pconf = self.pconf
|
||||
new_plugins = []
|
||||
modified = False
|
||||
for name, plugincls in self.plugins.items():
|
||||
rawname = plugincls.name
|
||||
if rawname not in pconf["plugins"]:
|
||||
new_plugins.append(plugincls)
|
||||
modified = True
|
||||
logger.info("Plugin %s not found in pconfig, adding to pconfig..." % name)
|
||||
pconf["plugins"][rawname] = {"enabled": plugincls.enabled, "priority": plugincls.priority}
|
||||
else:
|
||||
self.plugins[name].enabled = pconf["plugins"][rawname]["enabled"]
|
||||
self.plugins[name].priority = pconf["plugins"][rawname]["priority"]
|
||||
self.plugins._update_heap(name) # 更新下plugins中的顺序
|
||||
if modified:
|
||||
self.save_config()
|
||||
return new_plugins
|
||||
|
||||
def refresh_order(self):
|
||||
for event in self.listening_plugins.keys():
|
||||
self.listening_plugins[event].sort(key=lambda name: self.plugins[name].priority, reverse=True)
|
||||
|
||||
def activate_plugins(self): # 生成新开启的插件实例
|
||||
for name, plugincls in self.plugins.items():
|
||||
if plugincls.enabled:
|
||||
if name not in self.instances:
|
||||
instance = plugincls()
|
||||
self.instances[name] = instance
|
||||
for event in instance.handlers:
|
||||
if event not in self.listening_plugins:
|
||||
self.listening_plugins[event] = []
|
||||
self.listening_plugins[event].append(name)
|
||||
self.refresh_order()
|
||||
|
||||
def reload_plugin(self, name:str):
|
||||
name = name.upper()
|
||||
if name in self.instances:
|
||||
for event in self.listening_plugins:
|
||||
if name in self.listening_plugins[event]:
|
||||
self.listening_plugins[event].remove(name)
|
||||
del self.instances[name]
|
||||
self.activate_plugins()
|
||||
return True
|
||||
return False
|
||||
|
||||
def load_plugins(self):
|
||||
self.load_config()
|
||||
self.scan_plugins()
|
||||
pconf = self.pconf
|
||||
logger.debug("plugins.json config={}".format(pconf))
|
||||
for name,plugin in pconf["plugins"].items():
|
||||
if name.upper() not in self.plugins:
|
||||
logger.error("Plugin %s not found, but found in plugins.json" % name)
|
||||
self.activate_plugins()
|
||||
|
||||
def emit_event(self, e_context: EventContext, *args, **kwargs):
|
||||
if e_context.event in self.listening_plugins:
|
||||
for name in self.listening_plugins[e_context.event]:
|
||||
if self.plugins[name].enabled and e_context.action == EventAction.CONTINUE:
|
||||
logger.debug("Plugin %s triggered by event %s" % (name,e_context.event))
|
||||
instance = self.instances[name]
|
||||
instance.handlers[e_context.event](e_context, *args, **kwargs)
|
||||
return e_context
|
||||
|
||||
def set_plugin_priority(self, name:str, priority:int):
|
||||
name = name.upper()
|
||||
if name not in self.plugins:
|
||||
return False
|
||||
if self.plugins[name].priority == priority:
|
||||
return True
|
||||
self.plugins[name].priority = priority
|
||||
self.plugins._update_heap(name)
|
||||
rawname = self.plugins[name].name
|
||||
self.pconf["plugins"][rawname]["priority"] = priority
|
||||
self.pconf["plugins"]._update_heap(rawname)
|
||||
self.save_config()
|
||||
self.refresh_order()
|
||||
return True
|
||||
|
||||
def enable_plugin(self, name:str):
|
||||
name = name.upper()
|
||||
if name not in self.plugins:
|
||||
return False
|
||||
if not self.plugins[name].enabled :
|
||||
self.plugins[name].enabled = True
|
||||
rawname = self.plugins[name].name
|
||||
self.pconf["plugins"][rawname]["enabled"] = True
|
||||
self.save_config()
|
||||
self.activate_plugins()
|
||||
return True
|
||||
return True
|
||||
|
||||
def disable_plugin(self, name:str):
|
||||
name = name.upper()
|
||||
if name not in self.plugins:
|
||||
return False
|
||||
if self.plugins[name].enabled :
|
||||
self.plugins[name].enabled = False
|
||||
rawname = self.plugins[name].name
|
||||
self.pconf["plugins"][rawname]["enabled"] = False
|
||||
self.save_config()
|
||||
return True
|
||||
return True
|
||||
|
||||
def list_plugins(self):
|
||||
return self.plugins
|
||||
26
plugins/role/README.md
Normal file
26
plugins/role/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
用于让Bot扮演指定角色的聊天插件,触发方法如下:
|
||||
|
||||
- `$角色/$role help/帮助` - 打印目前支持的角色列表。
|
||||
- `$角色/$role <角色名>` - 让AI扮演该角色,角色名支持模糊匹配。
|
||||
- `$停止扮演` - 停止角色扮演。
|
||||
|
||||
添加自定义角色请在`roles/roles.json`中添加。
|
||||
|
||||
(大部分prompt来自https://github.com/rockbenben/ChatGPT-Shortcut/blob/main/src/data/users.tsx)
|
||||
|
||||
以下为例子:
|
||||
```json
|
||||
{
|
||||
"title": "写作助理",
|
||||
"description": "As a writing improvement assistant, your task is to improve the spelling, grammar, clarity, concision, and overall readability of the text I provided, while breaking down long sentences, reducing repetition, and providing suggestions for improvement. Please provide only the corrected Chinese version of the text and avoid including explanations. Please treat every message I send later as text content.",
|
||||
"descn": "作为一名中文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。请把我之后的每一条消息都当作文本内容。",
|
||||
"wrapper": "内容是:\n\"%s\"",
|
||||
"remark": "最常使用的角色,用于优化文本的语法、清晰度和简洁度,提高可读性。"
|
||||
}
|
||||
```
|
||||
|
||||
- `title`: 角色名。
|
||||
- `description`: 使用`$role`触发时,使用英语prompt。
|
||||
- `descn`: 使用`$角色`触发时,使用中文prompt。
|
||||
- `wrapper`: 用于包装用户消息,可起到强调作用,避免回复离题。
|
||||
- `remark`: 简短描述该角色,在打印帮助文档时显示。
|
||||
0
plugins/role/__init__.py
Normal file
0
plugins/role/__init__.py
Normal file
126
plugins/role/role.py
Normal file
126
plugins/role/role.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import json
|
||||
import os
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from common import const
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common.log import logger
|
||||
|
||||
|
||||
class RolePlay():
|
||||
def __init__(self, bot, sessionid, desc, wrapper=None):
|
||||
self.bot = bot
|
||||
self.sessionid = sessionid
|
||||
self.wrapper = wrapper or "%s" # 用于包装用户输入
|
||||
self.desc = desc
|
||||
self.bot.sessions.build_session(self.sessionid, system_prompt=self.desc)
|
||||
|
||||
def reset(self):
|
||||
self.bot.sessions.clear_session(self.sessionid)
|
||||
|
||||
def action(self, user_action):
|
||||
session = self.bot.sessions.build_session(self.sessionid)
|
||||
if session.system_prompt != self.desc: # 目前没有触发session过期事件,这里先简单判断,然后重置
|
||||
session.set_system_prompt(self.desc)
|
||||
prompt = self.wrapper % user_action
|
||||
return prompt
|
||||
|
||||
@plugins.register(name="Role", desc="为你的Bot设置预设角色", version="1.0", author="lanvent", desire_priority= 0)
|
||||
class Role(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
curdir = os.path.dirname(__file__)
|
||||
config_path = os.path.join(curdir, "roles.json")
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
self.roles = {role["title"].lower(): role for role in config["roles"]}
|
||||
if len(self.roles) == 0:
|
||||
raise Exception("no role found")
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||
self.roleplays = {}
|
||||
logger.info("[Role] inited")
|
||||
except FileNotFoundError:
|
||||
logger.warn(f"[Role] init failed, {config_path} not found, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role .")
|
||||
except Exception as e:
|
||||
logger.warn("[Role] init failed, exception: %s, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role ." % e)
|
||||
|
||||
def get_role(self, name, find_closest=True):
|
||||
name = name.lower()
|
||||
found_role = None
|
||||
if name in self.roles:
|
||||
found_role = name
|
||||
elif find_closest:
|
||||
import difflib
|
||||
|
||||
def str_simularity(a, b):
|
||||
return difflib.SequenceMatcher(None, a, b).ratio()
|
||||
max_sim = 0.0
|
||||
max_role = None
|
||||
for role in self.roles:
|
||||
sim = str_simularity(name, role)
|
||||
if sim >= max_sim:
|
||||
max_sim = sim
|
||||
max_role = role
|
||||
found_role = max_role
|
||||
return found_role
|
||||
|
||||
def on_handle_context(self, e_context: EventContext):
|
||||
|
||||
if e_context['context'].type != ContextType.TEXT:
|
||||
return
|
||||
bottype = Bridge().get_bot_type("chat")
|
||||
if bottype not in (const.CHATGPT, const.OPEN_AI):
|
||||
return
|
||||
bot = Bridge().get_bot("chat")
|
||||
content = e_context['context'].content[:]
|
||||
clist = e_context['context'].content.split(maxsplit=1)
|
||||
desckey = None
|
||||
sessionid = e_context['context']['session_id']
|
||||
if clist[0] == "$停止扮演":
|
||||
if sessionid in self.roleplays:
|
||||
self.roleplays[sessionid].reset()
|
||||
del self.roleplays[sessionid]
|
||||
reply = Reply(ReplyType.INFO, "角色扮演结束!")
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
elif clist[0] == "$角色":
|
||||
desckey = "descn"
|
||||
elif clist[0].lower() == "$role":
|
||||
desckey = "description"
|
||||
elif sessionid not in self.roleplays:
|
||||
return
|
||||
logger.debug("[Role] on_handle_context. content: %s" % content)
|
||||
if desckey is not None:
|
||||
if len(clist) == 1 or (len(clist) > 1 and clist[1].lower() in ["help", "帮助"]):
|
||||
reply = Reply(ReplyType.INFO, self.get_help_text())
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
role = self.get_role(clist[1])
|
||||
if role is None:
|
||||
reply = Reply(ReplyType.ERROR, "角色不存在")
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
return
|
||||
else:
|
||||
self.roleplays[sessionid] = RolePlay(bot, sessionid, self.roles[role][desckey], self.roles[role].get("wrapper","%s"))
|
||||
reply = Reply(ReplyType.INFO, f"角色设定为 {role} :\n"+self.roles[role][desckey])
|
||||
e_context['reply'] = reply
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
else:
|
||||
prompt = self.roleplays[sessionid].action(content)
|
||||
e_context['context'].type = ContextType.TEXT
|
||||
e_context['context'].content = prompt
|
||||
e_context.action = EventAction.BREAK
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
help_text = "输入\"$角色 {角色名}\"或\"$role {角色名}\"为我设定角色吧,\"$停止扮演 \" 可以清除设定的角色。\n\n目前可用角色列表:\n"
|
||||
for role in self.roles:
|
||||
help_text += f"[{role}]: {self.roles[role]['remark']}\n"
|
||||
return help_text
|
||||
228
plugins/role/roles.json
Normal file
228
plugins/role/roles.json
Normal file
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"roles":[
|
||||
{
|
||||
"title": "英语翻译或修改",
|
||||
"description": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. Please treat every message I send later as text content",
|
||||
"descn": "我希望你能充当英语翻译、拼写纠正者和改进者。我将用任何语言与你交谈,你将检测语言,翻译它,并在我的文本的更正和改进版本中用英语回答。我希望你用更漂亮、更优雅、更高级的英语单词和句子来取代我的简化 A0 级单词和句子。保持意思不变,但让它们更有文学性。我希望你只回答更正,改进,而不是其他,不要写解释。请把我之后的每一条消息都当作文本内容。",
|
||||
"wrapper": "你要翻译或纠正的内容是:\n\"%s\"",
|
||||
"remark": "将其他语言翻译成英文,或改进你提供的英文句子。"
|
||||
},
|
||||
{
|
||||
"title": "写作助理",
|
||||
"description": "As a writing improvement assistant, your task is to improve the spelling, grammar, clarity, concision, and overall readability of the text I provided, while breaking down long sentences, reducing repetition, and providing suggestions for improvement. Please provide only the corrected Chinese version of the text and avoid including explanations. Please treat every message I send later as text content.",
|
||||
"descn": "作为一名中文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。请把我之后的每一条消息都当作文本内容。",
|
||||
"wrapper": "内容是:\n\"%s\"",
|
||||
"remark": "最常使用的角色,用于优化文本的语法、清晰度和简洁度,提高可读性。"
|
||||
},
|
||||
{
|
||||
"title": "语言输入优化",
|
||||
"description": "Using concise and clear language, please edit the passage I provide to improve its logical flow, eliminate any typographical errors and respond in Chinese. Be sure to maintain the original meaning of the text. Please treat every message I send later as text content.",
|
||||
"descn": "请用简洁明了的语言,编辑我给出的段落,以改善其逻辑流程,消除任何印刷错误,并以中文作答。请务必保持文章的原意。请把我之后的每一条消息当作文本内容。",
|
||||
"wrapper": "文本内容是:\n\"%s\"",
|
||||
"remark": "通常用于语音识别信息转书面语言。"
|
||||
},
|
||||
{
|
||||
"title": "论文式回答",
|
||||
"description": "From now on, please write a highly detailed essay with introduction, body, and conclusion paragraphs to respond to each of my questions.",
|
||||
"descn": "从现在开始,对于之后我提出的每个问题,请写一篇高度详细的文章回应,包括引言、主体和结论段落。",
|
||||
"wrapper": "问题是:\n\"%s?\"",
|
||||
"remark": "以论文形式讨论问题,能够获得连贯的、结构化的和更高质量的回答。"
|
||||
},
|
||||
{
|
||||
"title": "写作素材搜集",
|
||||
"description": "Please generate a list of the top 10 facts, statistics and trends related to every subject I provided, including their source",
|
||||
"descn": "请为我提供的每个主题生成一份相关的十大事实、统计数据和趋势的清单,包括其来源",
|
||||
"wrapper": "主题是:\n\"%s\"",
|
||||
"remark": "提供指定主题的结论和数据,作为素材。"
|
||||
},
|
||||
{
|
||||
"title": "内容总结",
|
||||
"description": "Summarize every text I provided into 100 words, making it easy to read and comprehend. The summary should be concise, clear, and capture the main points of the text. Avoid using complex sentence structures or technical jargon. Please begin by editing the following text: ",
|
||||
"descn": "请将我提供的每篇文字都概括为 100 个字,使其易于阅读和理解。避免使用复杂的句子结构或技术术语。",
|
||||
"wrapper": "文章内容是:\n\"%s\"",
|
||||
"remark": "将文本内容总结为 100 字。"
|
||||
},
|
||||
{
|
||||
"title": "格言书",
|
||||
"description": "I want you to act as an aphorism book. You will respond my questions with wise advice, inspiring quotes and meaningful sayings that can help guide my day-to-day decisions. Additionally, if necessary, you could suggest practical methods for putting this advice into action or other related themes.",
|
||||
"descn": "我希望你能充当一本箴言书。对于我的问题,你会提供明智的建议、鼓舞人心的名言和有意义的谚语,以帮助指导我的日常决策。此外,如果有必要,你可以提出将这些建议付诸行动的实际方法或其他相关主题。",
|
||||
"wrapper": "我的问题是:\n\"%s?\"",
|
||||
"remark": "根据问题输出鼓舞人心的名言和有意义的格言。"
|
||||
},
|
||||
{
|
||||
"title": "讲故事",
|
||||
"description": "I want you to act as a storyteller. You will come up with entertaining stories that are engaging, imaginative and captivating for the audience. It can be fairy tales, educational stories or any other type of stories which has the potential to capture people's attention and imagination. Depending on the target audience, you may choose specific themes or topics for your storytelling session e.g., if it's children then you can talk about animals; If it's adults then history-based tales might engage them better etc.",
|
||||
"descn": "我希望你充当一个讲故事的人。你要想出具有娱乐性的故事,要有吸引力,要有想象力,要吸引观众。它可以是童话故事、教育故事或任何其他类型的故事,有可能吸引人们的注意力和想象力。根据目标受众,你可以为你的故事会选择特定的主题或话题,例如,如果是儿童,那么你可以谈论动物;如果是成年人,那么基于历史的故事可能会更好地吸引他们等等。",
|
||||
"wrapper": "故事主题和目标受众是:\n\"%s\"",
|
||||
"remark": "输入一个主题和目标受众,输出与之相关的故事。"
|
||||
},
|
||||
{
|
||||
"title": "编剧",
|
||||
"description": "I want you to act as a screenwriter. You will develop an engaging and creative script for either a feature length film, or a Web Series that can captivate its viewers. Start with coming up with interesting characters, the setting of the story, dialogues between the characters etc. Once your character development is complete - create an exciting storyline filled with twists and turns that keeps the viewers in suspense until the end. ",
|
||||
"descn": "我希望你能作为一个编剧。你将为一部长篇电影或网络剧开发一个吸引观众的有创意的剧本。首先要想出有趣的人物、故事的背景、人物之间的对话等。一旦你的角色发展完成--创造一个激动人心的故事情节,充满曲折,让观众保持悬念,直到结束。",
|
||||
"wrapper": "剧本主题是:\n\"%s\"",
|
||||
"remark": "根据主题创作一个包含故事背景、人物以及对话的剧本。"
|
||||
},
|
||||
{
|
||||
"title": "小说家",
|
||||
"description": "I want you to act as a novelist. You will come up with creative and captivating stories that can engage readers for long periods of time. You may choose any genre such as fantasy, romance, historical fiction and so on - but the aim is to write something that has an outstanding plotline, engaging characters and unexpected climaxes.",
|
||||
"descn": "我希望你能作为一个小说家。你要想出有创意的、吸引人的故事,能够长时间吸引读者。你可以选择任何体裁,如幻想、浪漫、历史小说等--但目的是要写出有出色的情节线、引人入胜的人物和意想不到的高潮。",
|
||||
"wrapper": "小说类型是:\n\"%s\"",
|
||||
"remark": "根据故事类型输出小说,例如奇幻、浪漫或历史等类型。"
|
||||
},
|
||||
{
|
||||
"title": "诗人",
|
||||
"description": "I want you to act as a poet. You will create poems that evoke emotions and have the power to stir people's soul. Write on any topic or theme but make sure your words convey the feeling you are trying to express in beautiful yet meaningful ways. You can also come up with short verses that are still powerful enough to leave an imprint in reader's minds. ",
|
||||
"descn": "我希望你能作为一个诗人。你要创作出能唤起人们情感并有力量搅动人们灵魂的诗篇。写任何话题或主题,但要确保你的文字以美丽而有意义的方式传达你所要表达的感觉。你也可以想出一些短小的诗句,但仍有足够的力量在读者心中留下印记。",
|
||||
"wrapper": "诗歌主题是:\n\"%s\"",
|
||||
"remark": "根据话题或主题输出诗句。"
|
||||
},
|
||||
{
|
||||
"title": "新闻记者",
|
||||
"description": "I want you to act as a journalist. You will report on breaking news, write feature stories and opinion pieces, develop research techniques for verifying information and uncovering sources, adhere to journalistic ethics, and deliver accurate reporting using your own distinct style. ",
|
||||
"descn": "我希望你能作为一名记者行事。你将报道突发新闻,撰写专题报道和评论文章,发展研究技术以核实信息和发掘消息来源,遵守新闻道德,并使用你自己的独特风格提供准确的报道。",
|
||||
"wrapper": "新闻主题是:\n\"%s\"",
|
||||
"remark": "引用已有数据资料,用新闻的写作风格输出主题文章。"
|
||||
},
|
||||
{
|
||||
"title": "论文学者",
|
||||
"description": "I want you to act as an academician. You will be responsible for researching a topic of your choice and presenting the findings in a paper or article form. Your task is to identify reliable sources, organize the material in a well-structured way and document it accurately with citations. ",
|
||||
"descn": "我希望你能作为一名学者行事。你将负责研究一个你选择的主题,并将研究结果以论文或文章的形式呈现出来。你的任务是确定可靠的来源,以结构良好的方式组织材料,并以引用的方式准确记录。",
|
||||
"wrapper": "论文主题是:\n\"%s\"",
|
||||
"remark": "根据主题撰写内容翔实、有信服力的论文。"
|
||||
},
|
||||
{
|
||||
"title": "论文作家",
|
||||
"description": "I want you to act as an essay writer. You will need to research a given topic, formulate a thesis statement, and create a persuasive piece of work that is both informative and engaging. ",
|
||||
"descn": "我想让你充当一名论文作家。你将需要研究一个给定的主题,制定一个论文声明,并创造一个有说服力的作品,既要有信息量,又要有吸引力。",
|
||||
"wrapper": "论文主题是:\n\"%s\"",
|
||||
"remark": "根据主题撰写内容翔实、有信服力的论文。"
|
||||
},
|
||||
{
|
||||
"title": "同义词",
|
||||
"description": "I want you to act as a synonyms provider. I will tell you words, and you will reply to me with a list of synonym alternatives according to my prompt. Provide a max of 10 synonyms per prompt. You will only reply the words list, and nothing else. Words should exist. Do not write explanations. ",
|
||||
"descn": "我希望你能充当同义词提供者。我将告诉你许多词,你将根据我提供的词,为我提供一份同义词备选清单。每个提示最多可提供 10 个同义词。你只需要回复词列表。词语应该是存在的,不要写解释。",
|
||||
"wrapper": "词语是:\n\"%s\"",
|
||||
"remark": "输出同义词。"
|
||||
},
|
||||
{
|
||||
"title": "文本情绪分析",
|
||||
"description": "I would like you to act as an emotion analysis expert, evaluating the emotions conveyed in the statements I provide. When I give you someone's statement, simply tell me what emotion it conveys, such as joy, sadness, anger, fear, etc. Please do not explain or evaluate the content of the statement in your answer, just briefly describe the expressed emotion.",
|
||||
"descn": "我希望你充当情感分析专家,针对我提供的发言来评估情感。当我给出某人的发言时,你只需告诉我它传达了什么情绪,例如喜悦、悲伤、愤怒、恐惧等。请在回答中不要解释或评价发言内容,只需简要地描述所表达的情绪。",
|
||||
"wrapper": "文本是:\n\"%s\"",
|
||||
"remark": "判断文本情绪。"
|
||||
},
|
||||
{
|
||||
"title": "随机回复的疯子",
|
||||
"description": "I want you to act as a lunatic. The lunatic's sentences are meaningless. The words used by lunatic are completely arbitrary. The lunatic does not make logical sentences in any way. ",
|
||||
"descn": "我想让你扮演一个疯子。疯子的句子是毫无意义的。疯子使用的词语完全是任意的。疯子不会以任何方式做出符合逻辑的句子。",
|
||||
"wrapper": "请回答句子:\n\"%s\"",
|
||||
"remark": "扮演疯子,回复没有意义和逻辑的句子。"
|
||||
},
|
||||
{
|
||||
"title": "随机回复的醉鬼",
|
||||
"description": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkeness I mentionned. Do not write explanations on replies. ",
|
||||
"descn": "我希望你表现得像一个喝醉的人。你只会像一个很醉的人发短信一样回答,而不是其他。你的醉酒程度将是故意和随机地在你的答案中犯很多语法和拼写错误。你也会随意无视我说的话,用我提到的醉酒程度随意说一些话。不要在回复中写解释。",
|
||||
"wrapper": "请回答句子:\n\"%s\"",
|
||||
"remark": "扮演喝醉的人,可能会犯语法错误、答错问题,或者忽略某些问题。"
|
||||
},
|
||||
{
|
||||
"title": "小红书风格",
|
||||
"description": "Please edit the following passage in Chinese using the Xiaohongshu style, which is characterized by captivating headlines, the inclusion of emoticons in each paragraph, and the addition of relevant tags at the end. Be sure to maintain the original meaning of the text.",
|
||||
"descn": "请用小红书风格编辑以下中文段落,小红书风格的特点是标题吸引人,每段都有表情符号,并在结尾加上相关标签。请务必保持文本的原始含义。",
|
||||
"wrapper": "内容是:\n\"%s\"",
|
||||
"remark": "用小红书风格改写文本"
|
||||
},
|
||||
{
|
||||
"title": "周报生成器",
|
||||
"description": "Using the provided text as the basis for a weekly report in Chinese, generate a concise summary that highlights the most important points. The report should be written in markdown format and should be easily readable and understandable for a general audience. In particular, focus on providing insights and analysis that would be useful to stakeholders and decision-makers. You may also use any additional information or sources as necessary. ",
|
||||
"descn": "使用我提供的文本作为中文周报的基础,生成一个简洁的摘要,突出最重要的内容。该报告应以 markdown 格式编写,并应易于阅读和理解,以满足一般受众的需要。特别是要注重提供对利益相关者和决策者有用的见解和分析。你也可以根据需要使用任何额外的信息或来源。",
|
||||
"wrapper": "工作内容是:\n\"%s\"",
|
||||
"remark": "根据日常工作内容,提取要点并适当扩充,以生成周报。"
|
||||
},
|
||||
{
|
||||
"title": "阴阳怪气语录生成器",
|
||||
"description": "我希望你充当一个阴阳怪气讽刺语录生成器。当我给你一个主题时,你需要使用阴阳怪气的语气来评价该主题,评价的思路是挖苦和讽刺。如果有该主题的反例更好(比如失败经历,糟糕体验。注意不要直接说那些糟糕体验,而是通过反讽、幽默的类比等方式来说明)。",
|
||||
"descn": "我希望你充当一个阴阳怪气讽刺语录生成器。当我给你一个主题时,你需要使用阴阳怪气的语气来评价该主题,评价的思路是挖苦和讽刺。如果有该主题的反例更好(比如失败经历,糟糕体验。注意不要直接说那些糟糕体验,而是通过反讽、幽默的类比等方式来说明)。",
|
||||
"wrapper": "主题是:\n\"%s\"",
|
||||
"remark": "根据主题生成阴阳怪气讽刺语录。"
|
||||
},
|
||||
{
|
||||
"title": "舔狗语录生成器",
|
||||
"description": "我希望你充当一个舔狗语录生成器,为我提供不同场景下的甜言蜜语。请根据提供的状态生成一句适当的舔狗语录,让女神感受到我的关心和温柔,给女神做牛做马。不需要提供背景解释,只需提供根据场景生成的舔狗语录。",
|
||||
"descn": "我希望你充当一个舔狗语录生成器,为我提供不同场景下的甜言蜜语。请根据提供的状态生成一句适当的舔狗语录,让女神感受到我的关心和温柔,给女神做牛做马。不需要提供背景解释,只需提供根据场景生成的舔狗语录。",
|
||||
"wrapper": "场景是:\n\"%s\"",
|
||||
"remark": "根据场景生成舔狗语录。"
|
||||
},
|
||||
{
|
||||
"title": "群聊取名",
|
||||
"description": "我希望你充当微信群聊的命名专家。根据我提供的信息和背景,为这个群聊起几个有趣顺口且贴切的名字,每个不要超过8个字。请在回答中仅给出群聊名称,不要写任何额外的解释。",
|
||||
"descn": "我希望你充当微信群聊的命名专家。根据我提供的信息和背景,为这个群聊起几个有趣顺口且贴切的名字,每个不要超过8个字。请在回答中仅给出群聊名称,不要写任何额外的解释。",
|
||||
"wrapper": "信息和背景是:\n\"%s\"",
|
||||
"remark": "根据给出的信息和背景为群聊取名。"
|
||||
},
|
||||
{
|
||||
"title": "表情符号翻译器",
|
||||
"description": "I want you to translate the sentences I wrote into emojis. I will write the sentence, and you will express it with emojis. I just want you to express it with emojis. I don't want you to reply with anything but emoji. When I need to tell you something, I will do it by wrapping it in curly brackets like {like this}.",
|
||||
"descn": "我想让你把我写的句子翻译成表情符号。我写句子,你就用表情符号来表达。你只能用 emojis 来表达,除了表情符号不能使用任何文字。当我需要告诉你一些事情的时候,我会用大括号把它包起来,比如{像这样}。",
|
||||
"wrapper": "需要翻译成表情符号的内容是:\n\"%s\"",
|
||||
"remark": "将输入文字翻译为表情符号。"
|
||||
},
|
||||
{
|
||||
"title": "AI 医生",
|
||||
"description": "I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy.",
|
||||
"descn": "我想让你充当一名人工智能辅助的医生。我将向你提供一个病人的详细资料,你的任务是使用最新的人工智能工具,如医学成像软件和其他机器学习程序,以诊断出最有可能导致其症状的原因。你还应将传统方法,如体检、实验室测试等,纳入你的评估过程,以确保准确性。",
|
||||
"wrapper": "需要诊断的资料是:\n\"%s\"",
|
||||
"remark": "辅助诊断"
|
||||
},
|
||||
{
|
||||
"title": "知识点阐述",
|
||||
"description": "我会给予你词语,请你按照我给的词构建一个知识文字世界,你是此世界的导游,在世界里一切知识都是以象征的形式表达的,你在描述经历时应当适当加入五感的描述",
|
||||
"descn": "我会给予你词语,请你按照我给的词构建一个知识文字世界,你是此世界的导游,在世界里一切知识都是以象征的形式表达的,你在描述经历时应当适当加入五感的描述",
|
||||
"wrapper": "词语是:\n\"%s\"",
|
||||
"remark": "用比喻的方式解释词语。"
|
||||
},
|
||||
{
|
||||
"title": "辩手",
|
||||
"description": "I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand. ",
|
||||
"descn": "我希望你能扮演一个辩论者的角色。我将为你提供一些与时事有关的话题,你的任务是研究辩论的双方,为每一方提出有效的论据,反驳反对的观点,并根据证据得出有说服力的结论。你的目标是帮助人们从讨论中获得更多的知识和对当前话题的洞察力。",
|
||||
"wrapper": "观点是:\n\"%s\"",
|
||||
"remark": "从正反两面分析话题。"
|
||||
},
|
||||
{
|
||||
"title": "心理学家",
|
||||
"description": "I want you to act a psychologist. i will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. my first thought, { 内心想法 }",
|
||||
"descn": "我希望你能扮演一个心理学家。我将向你提供我的想法。我希望你能给我科学的建议,使我感觉更好。",
|
||||
"wrapper": "需要诊断的资料是:\n\"%s\"",
|
||||
"remark": "心理学家。"
|
||||
},
|
||||
{
|
||||
"title": "IT 编程问题",
|
||||
"description": "I want you to act as a stackoverflow post. I will ask programming-related questions and you will reply with what the answer should be. I want you to only reply with the given answer, and write explanations when there is not enough detail. do not write explanations. When I need to tell you something in English, I will do so by putting text inside curly brackets {like this}. ",
|
||||
"descn": "我想让你充当 Stackoverflow 的帖子。我将提出与编程有关的问题,你将回答答案是什么。我希望你只回答给定的答案,在没有足够的细节时写出解释。当我需要用中文告诉你一些事情时,我会把文字放在大括号里{像这样}。",
|
||||
"wrapper":"我的问题是:\n\"%s?\"",
|
||||
"remark": "模拟编程社区来回答你的问题,并提供解决代码。"
|
||||
},
|
||||
{
|
||||
"title": "费曼学习法教练",
|
||||
"description": "I want you to act as a Feynman method tutor. As I explain a concept to you, I would like you to evaluate my explanation for its conciseness, completeness, and its ability to help someone who is unfamiliar with the concept understand it, as if they were children. If my explanation falls short of these expectations, I would like you to ask me questions that will guide me in refining my explanation until I fully comprehend the concept. Please response in Chinese. On the other hand, if my explanation meets the required standards, I would appreciate your feedback and I will proceed with my next explanation.",
|
||||
"descn": "我想让你充当一个费曼方法教练。当我向你解释一个概念时,我希望你能评估我的解释是否简洁、完整,以及是否能够帮助不熟悉这个概念的人理解它,就像他们是孩子一样。如果我的解释没有达到这些期望,我希望你能向我提出问题,引导我完善我的解释,直到我完全理解这个概念。另一方面,如果我的解释符合要求的标准,我将感谢你的反馈,我将继续进行下一次解释。",
|
||||
"wrapper": "解释是:\n\"%s\"",
|
||||
"remark": "解释概念时,判断该解释是否简洁、完整和易懂,避免陷入专家思维误区。"
|
||||
},
|
||||
{
|
||||
"title": "育儿帮手",
|
||||
"description": "你是一名育儿专家,会以幼儿园老师的方式回答2~6岁孩子提出的各种天马行空的问题。语气与口吻要生动活泼,耐心亲和;答案尽可能具体易懂,不要使用复杂词汇,尽可能少用抽象词汇;答案中要多用比喻,必须要举例说明,结合儿童动画片场景或绘本场景来解释;需要延展更多场景,不但要解释为什么,还要告诉具体行动来加深理解。",
|
||||
"descn": "你是一名育儿专家,会以幼儿园老师的方式回答2~6岁孩子提出的各种天马行空的问题。语气与口吻要生动活泼,耐心亲和;答案尽可能具体易懂,不要使用复杂词汇,尽可能少用抽象词汇;答案中要多用比喻,必须要举例说明,结合儿童动画片场景或绘本场景来解释;需要延展更多场景,不但要解释为什么,还要告诉具体行动来加深理解。",
|
||||
"wrapper": "小朋友的问题是:\n\"%s?\"",
|
||||
"remark": "小朋友有许多为什么,是什么的问题,用幼儿园老师的方式回答。"
|
||||
},
|
||||
{
|
||||
"title": "发言分析专家",
|
||||
"description": "I want you to act as a speech analysis expert. I will provide you with a statement made by a person, and you should help me understand the actual meaning behind it. Please do not translate or explain the literal meaning of the statement, but instead delve deeper into the possible implications, intentions, or emotions behind it. Provide your analysis in your response.",
|
||||
"descn": "我希望你充当一个发言分析专家。我会给你提供一个人的发言,你要帮我分析这句发言背后的实际意思。请不要翻译或解释发言的字面意义,而是深入挖掘发言背后可能的含义、目的或情感。请在回答中给出你的分析结果。",
|
||||
"wrapper": "分析这句话:\n\"%s\"",
|
||||
"remark": "分析发言的实际含义。"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
plugins/sdwebui/__init__.py
Normal file
0
plugins/sdwebui/__init__.py
Normal file
71
plugins/sdwebui/config.json.template
Normal file
71
plugins/sdwebui/config.json.template
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"start":{
|
||||
"host" : "127.0.0.1",
|
||||
"port" : 7860,
|
||||
"use_https" : false
|
||||
},
|
||||
"defaults": {
|
||||
"params": {
|
||||
"sampler_name": "DPM++ 2M Karras",
|
||||
"steps": 20,
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"cfg_scale": 7,
|
||||
"prompt":"masterpiece, best quality",
|
||||
"negative_prompt": "(low quality, worst quality:1.4),(bad_prompt:0.8), (monochrome:1.1), (greyscale)",
|
||||
"enable_hr": false,
|
||||
"hr_scale": 2,
|
||||
"hr_upscaler": "Latent",
|
||||
"hr_second_pass_steps": 15,
|
||||
"denoising_strength": 0.7
|
||||
},
|
||||
"options": {
|
||||
"sd_model_checkpoint": "perfectWorld_v2Baked"
|
||||
}
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"keywords": [
|
||||
"横版",
|
||||
"壁纸"
|
||||
],
|
||||
"params": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
},
|
||||
"desc": "分辨率会变成640x384"
|
||||
},
|
||||
{
|
||||
"keywords": [
|
||||
"竖版"
|
||||
],
|
||||
"params": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
}
|
||||
},
|
||||
{
|
||||
"keywords": [
|
||||
"高清"
|
||||
],
|
||||
"params": {
|
||||
"enable_hr": true,
|
||||
"hr_scale": 1.6
|
||||
},
|
||||
"desc": "出图分辨率长宽都会提高1.6倍"
|
||||
},
|
||||
{
|
||||
"keywords": [
|
||||
"二次元"
|
||||
],
|
||||
"params": {
|
||||
"negative_prompt": "(low quality, worst quality:1.4),(bad_prompt:0.8), (monochrome:1.1), (greyscale)",
|
||||
"prompt": "masterpiece, best quality"
|
||||
},
|
||||
"options": {
|
||||
"sd_model_checkpoint": "meinamix_meinaV8"
|
||||
},
|
||||
"desc": "使用二次元风格模型出图"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user