diff --git a/.github/ISSUE_TEMPLATE/bug--.md b/.github/ISSUE_TEMPLATE/bug--.md
index 69a0bba..9135cd0 100644
--- a/.github/ISSUE_TEMPLATE/bug--.md
+++ b/.github/ISSUE_TEMPLATE/bug--.md
@@ -7,11 +7,20 @@ assignees: ''
---
+
+
**环境信息**
请根据实际使用环境修改以下信息
-go-cqhttp版本: v0.9.10
-运行环境: windows_amd64
-连接方式: 反向WS
+go-cqhttp版本:
+运行环境:
+连接方式:
+使用协议:
**bug内容**
请在这里详细描述bug的内容
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6bca0fe..336a3cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,21 +14,27 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64
+ # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64
goos: [linux, windows, darwin]
- goarch: ["386", amd64, arm]
+ goarch: ["386", amd64, arm, arm64]
exclude:
- goos: darwin
goarch: arm
+ - goos: darwin
+ goarch: arm64
+ - goos: darwin
+ goarch: "386"
+ - goos: windows
+ goarch: arm64
fail-fast: true
steps:
- uses: actions/checkout@v2
- name: Setup Go environment
- uses: actions/setup-go@v2.1.1
+ uses: actions/setup-go@v2.1.3
with:
- go-version: 1.14
+ go-version: 1.15
- name: Build binary file
env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 48c5184..589d51a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,21 +10,28 @@ jobs:
matrix:
# build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64
goos: [linux, windows, darwin]
- goarch: ["386", amd64, arm]
+ goarch: ["386", amd64, arm, arm64]
exclude:
- goos: darwin
goarch: arm
+ - goos: darwin
+ goarch: arm64
+ - goos: darwin
+ goarch: "386"
+ - goos: windows
+ goarch: arm64
steps:
- uses: actions/checkout@v2
- name: Set RELEASE_VERSION env
- run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- - uses: wangyoucao577/go-release-action@master
+ run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV
+ - uses: pcrbot/go-release-action@master
env:
CGO_ENABLED: 0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
+ goversion: "https://golang.org/dl/go1.15.3.linux-amd64.tar.gz"
ldflags: -w -s -X "github.com/Mrs4s/go-cqhttp/coolq.Version=${{ env.RELEASE_VERSION }}"
-
\ No newline at end of file
+
diff --git a/.gitignore b/.gitignore
index 31e3ac6..4b43257 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
vendor/
.idea
+config.hjson
+device.json
+codec/
+data/
+logs/
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 478327d..13bb038 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,16 @@
-FROM golang:1.14.7-alpine AS builder
+FROM golang:1.15.5-alpine AS builder
RUN go env -w GO111MODULE=auto \
&& go env -w CGO_ENABLED=0 \
- && mkdir /build
+ && go env -w GOPROXY=https://goproxy.cn,direct
WORKDIR /build
COPY ./ .
-RUN cd /build \
- && go build -ldflags "-s -w -extldflags '-static'" -o cqhttp
+RUN set -ex \
+ && cd /build \
+ && go build -ldflags "-s -w -extldflags '-static'" -o cqhttp
FROM alpine:latest
diff --git a/README.md b/README.md
index 4cd982b..db63ab3 100644
--- a/README.md
+++ b/README.md
@@ -1,107 +1,282 @@
+
+
+
+
+
+
# go-cqhttp
-使用 [mirai](https://github.com/mamoe/mirai) 以及 [MiraiGo](https://github.com/Mrs4s/MiraiGo) 开发的cqhttp golang原生实现, 并在[cqhttp原版](https://github.com/richardchien/coolq-http-api)的基础上做了部分修改和拓展.
-文档暂时可查看 `docs` 目录, 目前还在撰写中.
-测试版可前往 Release 下载
+_✨ 基于 [Mirai](https://github.com/mamoe/mirai) 以及 [MiraiGo](https://github.com/Mrs4s/MiraiGo) 的 [cqhttp](https://github.com/howmanybots/onebot/blob/master/README.md) golang 原生实现 ✨_
-# 兼容性
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 文档
+ ·
+ 下载
+ ·
+ 开始使用
+
+
+---
+
+go-cqhttp 在[原版 cqhttp](https://github.com/richardchien/coolq-http-api)的基础上做了部分修改和拓展.
+
+---
+
+## 兼容性
+
+### 接口
-#### 接口
- [x] HTTP API
-- [x] 反向HTTP POST
-- [x] 正向Websocket
-- [x] 反向Websocket
+- [x] 反向 HTTP POST
+- [x] 正向 Websocket
+- [x] 反向 Websocket
-#### 拓展支持
-> 拓展API可前往 [文档](docs/cqhttp.md) 查看
-- [x] HTTP POST多点上报
-- [x] 反向WS多点连接
+### 拓展支持
+
+> 拓展 API 可前往 [文档](docs/cqhttp.md) 查看
+
+- [x] HTTP POST 多点上报
+- [x] 反向 WS 多点连接
- [x] 修改群名
- [x] 消息撤回事件
- [x] 解析/发送 回复消息
- [x] 解析/发送 合并转发
-- [ ] 使用代理请求网络图片
+- [x] 使用代理请求网络图片
+
+### 实现
-#### 实现
已实现CQ码
-- [CQ:image]
-- [CQ:record]
-- [CQ:video]
-- [CQ:face]
-- [CQ:at]
-- [CQ:share]
-- [CQ:reply]
-- [CQ:forward]
-- [CQ:node]
+#### 符合 Onebot 标准的 CQ 码
+
+| CQ 码 | 功能 |
+| ------------ | --------------------------- |
+| [CQ:face] | [QQ 表情] |
+| [CQ:record] | [语音] |
+| [CQ:video] | [短视频] |
+| [CQ:at] | [@某人] |
+| [CQ:share] | [链接分享] |
+| [CQ:music] | [音乐分享] [音乐自定义分享] |
+| [CQ:reply] | [回复] |
+| [CQ:forward] | [合并转发] |
+| [CQ:node] | [合并转发节点] |
+| [CQ:xml] | [XML 消息] |
+| [CQ:json] | [JSON 消息] |
+
+[qq 表情]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#qq-%E8%A1%A8%E6%83%85
+[语音]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E8%AF%AD%E9%9F%B3
+[短视频]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E7%9F%AD%E8%A7%86%E9%A2%91
+[@某人]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E6%9F%90%E4%BA%BA
+[链接分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%93%BE%E6%8E%A5%E5%88%86%E4%BA%AB
+[音乐分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%9F%B3%E4%B9%90%E5%88%86%E4%BA%AB-
+[音乐自定义分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%9F%B3%E4%B9%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%86%E4%BA%AB-
+[回复]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%9B%9E%E5%A4%8D
+[合并转发]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-
+[合并转发节点]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E8%8A%82%E7%82%B9-
+[xml 消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#xml-%E6%B6%88%E6%81%AF
+[json 消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#json-%E6%B6%88%E6%81%AF
+
+#### 拓展 CQ 码及与 Onebot 标准有略微差异的 CQ 码
+
+| 拓展 CQ 码 | 功能 |
+| -------------- | --------------------------------- |
+| [CQ:image] | [图片] |
+| [CQ:redbag] | [红包] |
+| [CQ:poke] | [戳一戳] |
+| [CQ:gift] | [礼物] |
+| [CQ:node] | [合并转发消息节点] |
+| [CQ:cardimage] | [一种 xml 的图片消息(装逼大图)] |
+| [CQ:tts] | [文本转语音] |
+
+[图片]: docs/cqhttp.md#%E5%9B%BE%E7%89%87
+[红包]: docs/cqhttp.md#%E7%BA%A2%E5%8C%85
+[戳一戳]: docs/cqhttp.md#%E6%88%B3%E4%B8%80%E6%88%B3
+[礼物]: docs/cqhttp.md#%E7%A4%BC%E7%89%A9
+[合并转发消息节点]: docs/cqhttp.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9
+[一种 xml 的图片消息(装逼大图)]: docs/cqhttp.md#cardimage-%E4%B8%80%E7%A7%8Dxml%E7%9A%84%E5%9B%BE%E7%89%87%E6%B6%88%E6%81%AF%E8%A3%85%E9%80%BC%E5%A4%A7%E5%9B%BE
+[文本转语音]: docs/cqhttp.md#%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3
已实现API
-##### 注意: 部分API实现与CQHTTP原版略有差异,请参考文档
-| API | 功能 |
-| ------------------------ | ------------------------------------------------------------ |
-| /get_login_info | [获取登录号信息](https://cqhttp.cc/docs/4.15/#/API?id=get_login_info-获取登录号信息) |
-| /get_friend_list | [获取好友列表](https://cqhttp.cc/docs/4.15/#/API?id=get_friend_list-获取好友列表) |
-| /get_group_list | [获取群列表](https://cqhttp.cc/docs/4.15/#/API?id=get_group_list-获取群列表) |
-| /get_group_info | [获取群信息](https://cqhttp.cc/docs/4.15/#/API?id=get_group_info-获取群信息) |
-| /get_group_member_list | [获取群成员列表](https://cqhttp.cc/docs/4.15/#/API?id=get_group_member_list-获取群成员列表) |
-| /get_group_member_info | [获取群成员信息](https://cqhttp.cc/docs/4.15/#/API?id=get_group_member_info-获取群成员信息) |
-| /send_msg | [发送消息](https://cqhttp.cc/docs/4.15/#/API?id=send_msg-发送消息) |
-| /send_group_msg | [发送群消息](https://cqhttp.cc/docs/4.15/#/API?id=send_group_msg-发送群消息) |
-| /send_private_msg | [发送私聊消息](https://cqhttp.cc/docs/4.15/#/API?id=send_private_msg-发送私聊消息) |
-| /delete_msg | [撤回信息](https://cqhttp.cc/docs/4.15/#/API?id=delete_msg-撤回消息) |
-| /set_friend_add_request | [处理加好友请求](https://cqhttp.cc/docs/4.15/#/API?id=set_friend_add_request-处理加好友请求) |
-| /set_group_add_request | [处理加群请求/邀请](https://cqhttp.cc/docs/4.15/#/API?id=set_group_add_request-处理加群请求/邀请) |
-| /set_group_card | [设置群名片(群备注)](https://cqhttp.cc/docs/4.15/#/API?id=set_group_card-设置群名片(群备注)) |
-| /set_group_special_title | [设置群组专属头衔](https://cqhttp.cc/docs/4.15/#/API?id=set_group_special_title-设置群组专属头衔) |
-| /set_group_kick | [群组T人](https://cqhttp.cc/docs/4.15/#/API?id=set_group_kick-群组踢人) |
-| /set_group_ban | [群组单人禁言](https://cqhttp.cc/docs/4.15/#/API?id=set_group_ban-群组单人禁言) |
-| /set_group_whole_ban | [群组全员禁言](https://cqhttp.cc/docs/4.15/#/API?id=set_group_whole_ban-群组全员禁言) |
-| /set_group_leave | [退出群组](https://cqhttp.cc/docs/4.15/#/API?id=set_group_leave-退出群组) |
-| /set_group_name | 设置群组名(拓展API) |
-| /get_image | 获取图片信息(拓展API) |
-| /get_group_msg | 获取群组消息(拓展API) |
-| /can_send_image | [检查是否可以发送图片](https://cqhttp.cc/docs/4.15/#/API?id=can_send_image-检查是否可以发送图片) |
-| /can_send_record | [检查是否可以发送语音](https://cqhttp.cc/docs/4.15/#/API?id=can_send_record-检查是否可以发送语音) |
-| /get_status | [获取插件运行状态](https://cqhttp.cc/docs/4.15/#/API?id=get_status-获取插件运行状态) |
-| /get_version_info | [获取 酷Q 及 CQHTTP插件的版本信息](https://cqhttp.cc/docs/4.15/#/API?id=get_version_info-获取-酷q-及-cqhttp-插件的版本信息) |
+#### 符合 Onebot 标准的 API
+
+| API | 功能 |
+| ------------------------ | ---------------------- |
+| /send_private_msg | [发送私聊消息] |
+| /send_group_msg | [发送群消息] |
+| /send_msg | [发送消息] |
+| /delete_msg | [撤回信息] |
+| /set_group_kick | [群组踢人] |
+| /set_group_ban | [群组单人禁言] |
+| /set_group_whole_ban | [群组全员禁言] |
+| /set_group_admin | [群组设置管理员] |
+| /set_group_card | [设置群名片(群备注)] |
+| /set_group_name | [设置群名] |
+| /set_group_leave | [退出群组] |
+| /set_group_special_title | [设置群组专属头衔] |
+| /set_friend_add_request | [处理加好友请求] |
+| /set_group_add_request | [处理加群请求/邀请] |
+| /get_login_info | [获取登录号信息] |
+| /get_stranger_info | [获取陌生人信息] |
+| /get_friend_list | [获取好友列表] |
+| /get_group_info | [获取群信息] |
+| /get_group_list | [获取群列表] |
+| /get_group_member_info | [获取群成员信息] |
+| /get_group_member_list | [获取群成员列表] |
+| /get_group_honor_info | [获取群荣誉信息] |
+| /can_send_image | [检查是否可以发送图片] |
+| /can_send_record | [检查是否可以发送语音] |
+| /get_version_info | [获取版本信息] |
+| /set_restart | [重启 go-cqhttp] |
+| /.handle_quick_operation | [对事件执行快速操作] |
+
+[发送私聊消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
+[发送群消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
+[发送消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
+[撤回信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
+[群组踢人]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
+[群组单人禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
+[群组全员禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
+[群组设置管理员]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
+[设置群名片(群备注)]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
+[设置群名]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
+[退出群组]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
+[设置群组专属头衔]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
+[处理加好友请求]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
+[处理加群请求/邀请]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
+[获取登录号信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
+[获取陌生人信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
+[获取好友列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
+[获取群信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
+[获取群列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
+[获取群成员信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
+[获取群成员列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
+[获取群荣誉信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
+[检查是否可以发送图片]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
+[检查是否可以发送语音]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
+[获取版本信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
+[重启 go-cqhttp]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
+[对事件执行快速操作]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/hidden.md#handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
+
+#### 拓展 API 及与 Onebot 标准有略微差异的 API
+
+| 拓展 API | 功能 |
+| --------------------------- | ---------------------- |
+| /set_group_portrait | [设置群头像] |
+| /get_image | [获取图片信息] |
+| /get_msg | [获取消息] |
+| /get_forward_msg | [获取合并转发内容] |
+| /send_group_forward_msg | [发送合并转发(群)] |
+| /.get_word_slices | [获取中文分词] |
+| /.ocr_image | [图片 OCR] |
+| /get_group_system_msg | [获取群系统消息] |
+| /get_group_file_system_info | [获取群文件系统信息] |
+| /get_group_root_files | [获取群根目录文件列表] |
+| /get_group_files_by_folder | [获取群子目录文件列表] |
+| /get_group_file_url | [获取群文件资源链接] |
+| /get_status | [获取状态] |
+
+[设置群头像]: docs/cqhttp.md#%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%A4%B4%E5%83%8F
+[获取图片信息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87%E4%BF%A1%E6%81%AF
+[获取消息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
+[获取合并转发内容]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E5%86%85%E5%AE%B9
+[发送合并转发(群)]: docs/cqhttp.md#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E7%BE%A4
+[获取中文分词]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D
+[图片 ocr]: docs/cqhttp.md#%E5%9B%BE%E7%89%87ocr
+[获取群系统消息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF
+[获取群文件系统信息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF
+[获取群根目录文件列表]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%A0%B9%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
+[获取群子目录文件列表]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%AD%90%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
+[获取群文件资源链接]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E8%B5%84%E6%BA%90%E9%93%BE%E6%8E%A5
+[获取状态]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%8A%B6%E6%80%81
已实现Event
-##### 注意: 部分Event数据与CQHTTP原版略有差异,请参考文档
-| Event |
-| ------------------------------------------------------------ |
-| [私聊信息](https://cqhttp.cc/docs/4.15/#/Post?id=私聊消息) |
-| [群消息](https://cqhttp.cc/docs/4.15/#/Post?id=群消息) |
-| [群消息撤回(拓展Event)](docs/cqhttp.md#群消息撤回) |
-| [好友消息撤回(拓展Event)](docs/cqhttp.md#好友消息撤回) |
-| [群管理员变动](https://cqhttp.cc/docs/4.15/#/Post?id=群管理员变动) |
-| [群成员减少](https://cqhttp.cc/docs/4.15/#/Post?id=群成员减少) |
-| [群成员增加](https://cqhttp.cc/docs/4.15/#/Post?id=群成员增加) |
-| [群禁言](https://cqhttp.cc/docs/4.15/#/Post?id=群禁言) |
-| [群文件上传](https://cqhttp.cc/docs/4.15/#/Post?id=群文件上传)|
-| [加好友请求](https://cqhttp.cc/docs/4.15/#/Post?id=加好友请求) |
-| [加群请求/邀请](https://cqhttp.cc/docs/4.15/#/Post?id=加群请求/邀请) |
+#### 符合 Onebot 标准的 Event(部分 Event 比 Onebot 标准多上报几个字段,不影响使用)
+
+| 事件类型 | Event |
+| -------- | ---------------- |
+| 消息事件 | [私聊信息] |
+| 消息事件 | [群消息] |
+| 通知事件 | [群文件上传] |
+| 通知事件 | [群管理员变动] |
+| 通知事件 | [群成员减少] |
+| 通知事件 | [群成员增加] |
+| 通知事件 | [群禁言] |
+| 通知事件 | [好友添加] |
+| 通知事件 | [群消息撤回] |
+| 通知事件 | [好友消息撤回] |
+| 通知事件 | [群内戳一戳] |
+| 通知事件 | [群红包运气王] |
+| 通知事件 | [群成员荣誉变更] |
+| 请求事件 | [加好友请求] |
+| 请求事件 | [加群请求/邀请] |
+
+[私聊信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
+[群消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#%E7%BE%A4%E6%B6%88%E6%81%AF
+[群文件上传]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0
+[群管理员变动]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%AE%A1%E7%90%86%E5%91%98%E5%8F%98%E5%8A%A8
+[群成员减少]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%87%8F%E5%B0%91
+[群成员增加]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%A2%9E%E5%8A%A0
+[群禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%A6%81%E8%A8%80
+[好友添加]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B7%BB%E5%8A%A0
+[群消息撤回]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
+[好友消息撤回]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
+[群内戳一戳]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
+[群红包运气王]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%BA%A2%E5%8C%85%E8%BF%90%E6%B0%94%E7%8E%8B
+[群成员荣誉变更]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E8%8D%A3%E8%AA%89%E5%8F%98%E6%9B%B4
+[加好友请求]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md#%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
+[加群请求/邀请]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md#%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
+
+#### 拓展 Event
+
+| 事件类型 | 拓展 Event |
+| -------- | ---------------- |
+| 通知事件 | [好友戳一戳] |
+| 通知事件 | [群成员名片更新] |
+| 通知事件 | [接收到离线文件] |
+
+[好友戳一戳]: docs/cqhttp.md#%E5%A5%BD%E5%8F%8B%E6%88%B3%E4%B8%80%E6%88%B3
+[群成员名片更新]: docs/cqhttp.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%90%8D%E7%89%87%E6%9B%B4%E6%96%B0
+[接收到离线文件]: docs/cqhttp.md#%E6%8E%A5%E6%94%B6%E5%88%B0%E7%A6%BB%E7%BA%BF%E6%96%87%E4%BB%B6
-# 关于ISSUE
+## 关于 ISSUE
-以下ISSUE会被直接关闭
-- 提交BUG不使用Template
+以下 ISSUE 会被直接关闭
+
+- 提交 BUG 不使用 Template
- 询问已知问题
- 提问找不到重点
- 重复提问
> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。
-# 性能
+## 性能
-在关闭数据库的情况下, 加载25个好友128个群运行24小时后内存使用为10MB左右. 开启数据库后内存使用将根据消息量增加10-20MB, 如果系统内存小于128M建议关闭数据库使用.
+在关闭数据库的情况下, 加载 25 个好友 128 个群运行 24 小时后内存使用为 10MB 左右. 开启数据库后内存使用将根据消息量增加 10-20MB, 如果系统内存小于 128M 建议关闭数据库使用.
diff --git a/coolq/api.go b/coolq/api.go
index 486c069..19b5770 100644
--- a/coolq/api.go
+++ b/coolq/api.go
@@ -1,11 +1,16 @@
package coolq
import (
+ "crypto/md5"
+ "encoding/hex"
"io/ioutil"
+ "math"
"os"
"path"
+ "path/filepath"
"runtime"
"strconv"
+ "strings"
"time"
"github.com/Mrs4s/MiraiGo/binary"
@@ -54,10 +59,32 @@ func (bot *CQBot) CQGetGroupList(noCache bool) MSG {
}
// https://cqhttp.cc/docs/4.15/#/API?id=get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
-func (bot *CQBot) CQGetGroupInfo(groupId int64) MSG {
+func (bot *CQBot) CQGetGroupInfo(groupId int64, noCache bool) MSG {
group := bot.Client.FindGroup(groupId)
if group == nil {
- return Failed(100)
+ gid := strconv.FormatInt(groupId, 10)
+ info, err := bot.Client.SearchGroupByKeyword(gid)
+ if err != nil {
+ return Failed(100, "GROUP_SEARCH_ERROR", "群聊搜索失败")
+ }
+ for _, g := range info {
+ if g.Code == groupId {
+ return OK(MSG{
+ "group_id": g.Code,
+ "group_name": g.Name,
+ "max_member_count": 0,
+ "member_count": 0,
+ })
+ }
+ }
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在失败")
+ }
+ if noCache {
+ var err error
+ group, err = bot.Client.GetGroupInfo(groupId)
+ if err != nil {
+ return Failed(100, "GET_GROUP_INFO_API_ERROR", err.Error())
+ }
}
return OK(MSG{
"group_id": group.Code,
@@ -71,13 +98,13 @@ func (bot *CQBot) CQGetGroupInfo(groupId int64) MSG {
func (bot *CQBot) CQGetGroupMemberList(groupId int64, noCache bool) MSG {
group := bot.Client.FindGroup(groupId)
if group == nil {
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
if noCache {
t, err := bot.Client.GetGroupMembers(group)
if err != nil {
log.Warnf("刷新群 %v 成员列表失败: %v", groupId, err)
- return Failed(100)
+ return Failed(100, "GET_MEMBERS_API_ERROR", err.Error())
}
group.Members = t
}
@@ -92,17 +119,105 @@ func (bot *CQBot) CQGetGroupMemberList(groupId int64, noCache bool) MSG {
func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64) MSG {
group := bot.Client.FindGroup(groupId)
if group == nil {
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
member := group.FindMember(userId)
if member == nil {
- return Failed(102)
+ return Failed(100, "MEMBER_NOT_FOUND", "群员不存在")
}
return OK(convertGroupMemberInfo(groupId, member))
}
+func (bot *CQBot) CQGetGroupFileSystemInfo(groupId int64) MSG {
+ fs, err := bot.Client.GetGroupFileSystem(groupId)
+ if err != nil {
+ log.Errorf("获取群 %v 文件系统信息失败: %v", groupId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ return OK(fs)
+}
+
+func (bot *CQBot) CQGetGroupRootFiles(groupId int64) MSG {
+ fs, err := bot.Client.GetGroupFileSystem(groupId)
+ if err != nil {
+ log.Errorf("获取群 %v 文件系统信息失败: %v", groupId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ files, folders, err := fs.Root()
+ if err != nil {
+ log.Errorf("获取群 %v 根目录文件失败: %v", groupId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ return OK(MSG{
+ "files": files,
+ "folders": folders,
+ })
+}
+
+func (bot *CQBot) CQGetGroupFilesByFolderId(groupId int64, folderId string) MSG {
+ fs, err := bot.Client.GetGroupFileSystem(groupId)
+ if err != nil {
+ log.Errorf("获取群 %v 文件系统信息失败: %v", groupId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ files, folders, err := fs.GetFilesByFolder(folderId)
+ if err != nil {
+ log.Errorf("获取群 %v 根目录 %v 子文件失败: %v", groupId, folderId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ return OK(MSG{
+ "files": files,
+ "folders": folders,
+ })
+}
+
+func (bot *CQBot) CQGetGroupFileUrl(groupId int64, fileId string, busId int32) MSG {
+ url := bot.Client.GetGroupFileUrl(groupId, fileId, busId)
+ if url == "" {
+ return Failed(100, "FILE_SYSTEM_API_ERROR")
+ }
+ return OK(MSG{
+ "url": url,
+ })
+}
+
+func (bot *CQBot) CQUploadGroupFile(groupId int64, file, name, folder string) MSG {
+ if !global.PathExists(file) {
+ log.Errorf("上传群文件 %v 失败: 文件不存在", file)
+ return Failed(100, "FILE_NOT_FOUND", "文件不存在")
+ }
+ fs, err := bot.Client.GetGroupFileSystem(groupId)
+ if err != nil {
+ log.Errorf("获取群 %v 文件系统信息失败: %v", groupId, err)
+ return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
+ }
+ if folder == "" {
+ folder = "/"
+ }
+ if err = fs.UploadFile(file, name, folder); err != nil {
+ log.Errorf("上传群 %v 文件 %v 失败: %v", groupId, file, err)
+ return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error())
+ }
+ return OK(nil)
+}
+
+func (bot *CQBot) CQGetWordSlices(content string) MSG {
+ slices, err := bot.Client.GetWordSegmentation(content)
+ if err != nil {
+ return Failed(100, "WORD_SEGMENTATION_API_ERROR", err.Error())
+ }
+ for i := 0; i < len(slices); i++ {
+ slices[i] = strings.ReplaceAll(slices[i], "\u0000", "")
+ }
+ return OK(MSG{"slices": slices})
+}
+
// https://cqhttp.cc/docs/4.15/#/API?id=send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bool) MSG {
+ if bot.Client.FindGroup(groupId) == nil {
+ log.Warnf("群消息发送失败: 群 %v 不存在", groupId)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+ }
var str string
fixAt := func(elem []message.IMessageElement) {
for _, e := range elem {
@@ -123,8 +238,9 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bo
fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 {
- return Failed(100)
+ return Failed(100, "SEND_MSG_API_ERROR", "请参考输出")
}
+ log.Infof("发送群 %v(%v) 的消息: %v (%v)", groupId, groupId, limitedString(m.String()), mid)
return OK(MSG{"message_id": mid})
}
str = func() string {
@@ -138,7 +254,7 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bo
}
if str == "" {
log.Warnf("群消息发送失败: 信息为空. MSG: %v", i)
- return Failed(100)
+ return Failed(100, "EMPTY_MSG_ERROR", "消息为空")
}
var elem []message.IMessageElement
if autoEscape {
@@ -149,8 +265,9 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bo
fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 {
- return Failed(100)
+ return Failed(100, "SEND_MSG_API_ERROR", "请参考输出")
}
+ log.Infof("发送群 %v(%v) 的消息: %v (%v)", groupId, groupId, limitedString(str), mid)
return OK(MSG{"message_id": mid})
}
@@ -158,7 +275,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
if m.Type != gjson.JSON {
return Failed(100)
}
- var nodes []*message.ForwardNode
+ var sendNodes []*message.ForwardNode
ts := time.Now().Add(-time.Minute * 5)
hasCustom := func() bool {
for _, item := range m.Array() {
@@ -168,24 +285,26 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
}
return false
}()
- convert := func(e gjson.Result) {
+ var convert func(e gjson.Result) []*message.ForwardNode
+ convert = func(e gjson.Result) (nodes []*message.ForwardNode) {
if e.Get("type").Str != "node" {
- return
+ return nil
}
ts.Add(time.Second)
if e.Get("data.id").Exists() {
- i, _ := strconv.Atoi(e.Get("data.id").Str)
- m := bot.GetGroupMessage(int32(i))
+ i, _ := strconv.Atoi(e.Get("data.id").String())
+ m := bot.GetMessage(int32(i))
if m != nil {
sender := m["sender"].(message.Sender)
nodes = append(nodes, &message.ForwardNode{
SenderId: sender.Uin,
SenderName: (&sender).DisplayName(),
Time: func() int32 {
- if hasCustom {
+ msgTime := m["time"].(int32)
+ if hasCustom && msgTime == 0 {
return int32(ts.Unix())
}
- return m["time"].(int32)
+ return msgTime
}(),
Message: bot.ConvertStringMessage(m["message"].(string), true),
})
@@ -195,13 +314,41 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
return
}
uin, _ := strconv.ParseInt(e.Get("data.uin").Str, 10, 64)
+ msgTime, err := strconv.ParseInt(e.Get("data.time").Str, 10, 64)
+ if err != nil {
+ msgTime = ts.Unix()
+ }
name := e.Get("data.name").Str
+ c := e.Get("data.content")
+ if c.IsArray() {
+ flag := false
+ c.ForEach(func(_, value gjson.Result) bool {
+ if value.Get("type").String() == "node" {
+ flag = true
+ return false
+ }
+ return true
+ })
+ if flag {
+ var taowa []*message.ForwardNode
+ for _, item := range c.Array() {
+ taowa = append(taowa, convert(item)...)
+ }
+ nodes = append(nodes, &message.ForwardNode{
+ SenderId: uin,
+ SenderName: name,
+ Time: int32(msgTime),
+ Message: []message.IMessageElement{bot.Client.UploadGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: taowa})},
+ })
+ return
+ }
+ }
content := bot.ConvertObjectMessage(e.Get("data.content"), true)
if uin != 0 && name != "" && len(content) > 0 {
var newElem []message.IMessageElement
for _, elem := range content {
- if img, ok := elem.(*message.ImageElement); ok {
- gm, err := bot.Client.UploadGroupImage(groupId, img.Data)
+ if img, ok := elem.(*LocalImageElement); ok {
+ gm, err := bot.UploadLocalImageAsGroup(groupId, img)
if err != nil {
log.Warnf("警告:群 %v 图片上传失败: %v", groupId, err)
continue
@@ -209,29 +356,39 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
newElem = append(newElem, gm)
continue
}
+ if video, ok := elem.(*LocalVideoElement); ok {
+ gm, err := bot.UploadLocalVideo(groupId, video)
+ if err != nil {
+ log.Warnf("警告:群 %v 视频上传失败: %v", groupId, err)
+ continue
+ }
+ newElem = append(newElem, gm)
+ continue
+ }
newElem = append(newElem, elem)
}
nodes = append(nodes, &message.ForwardNode{
SenderId: uin,
SenderName: name,
- Time: int32(ts.Unix()),
+ Time: int32(msgTime),
Message: newElem,
})
return
}
log.Warnf("警告: 非法 Forward node 将跳过")
+ return
}
if m.IsArray() {
for _, item := range m.Array() {
- convert(item)
+ sendNodes = append(sendNodes, convert(item)...)
}
} else {
- convert(m)
+ sendNodes = convert(m)
}
- if len(nodes) > 0 {
- gm := bot.Client.SendGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: nodes})
+ if len(sendNodes) > 0 {
+ gm := bot.Client.SendGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: sendNodes})
return OK(MSG{
- "message_id": ToGlobalId(groupId, gm.Id),
+ "message_id": bot.InsertGroupMessage(gm),
})
}
return Failed(100)
@@ -242,11 +399,12 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b
var str string
if m, ok := i.(gjson.Result); ok {
if m.Type == gjson.JSON {
- elem := bot.ConvertObjectMessage(m, true)
+ elem := bot.ConvertObjectMessage(m, false)
mid := bot.SendPrivateMessage(userId, &message.SendingMessage{Elements: elem})
if mid == -1 {
- return Failed(100)
+ return Failed(100, "SEND_MSG_API_ERROR", "请参考输出")
}
+ log.Infof("发送好友 %v(%v) 的消息: %v (%v)", userId, userId, limitedString(m.String()), mid)
return OK(MSG{"message_id": mid})
}
str = func() string {
@@ -259,7 +417,7 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b
str = s
}
if str == "" {
- return Failed(100)
+ return Failed(100, "EMPTY_MSG_ERROR", "消息为空")
}
var elem []message.IMessageElement
if autoEscape {
@@ -269,8 +427,9 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b
}
mid := bot.SendPrivateMessage(userId, &message.SendingMessage{Elements: elem})
if mid == -1 {
- return Failed(100)
+ return Failed(100, "SEND_MSG_API_ERROR", "请参考输出")
}
+ log.Infof("发送好友 %v(%v) 的消息: %v (%v)", userId, userId, limitedString(str), mid)
return OK(MSG{"message_id": mid})
}
@@ -282,7 +441,7 @@ func (bot *CQBot) CQSetGroupCard(groupId, userId int64, card string) MSG {
return OK(nil)
}
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
@@ -293,7 +452,7 @@ func (bot *CQBot) CQSetGroupSpecialTitle(groupId, userId int64, title string) MS
return OK(nil)
}
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
func (bot *CQBot) CQSetGroupName(groupId int64, name string) MSG {
@@ -301,7 +460,7 @@ func (bot *CQBot) CQSetGroupName(groupId int64, name string) MSG {
g.UpdateName(name)
return OK(nil)
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
func (bot *CQBot) CQSetGroupMemo(groupId int64, msg string) MSG {
@@ -309,18 +468,18 @@ func (bot *CQBot) CQSetGroupMemo(groupId int64, msg string) MSG {
g.UpdateMemo(msg)
return OK(nil)
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
-func (bot *CQBot) CQSetGroupKick(groupId, userId int64, msg string) MSG {
+func (bot *CQBot) CQSetGroupKick(groupId, userId int64, msg string, block bool) MSG {
if g := bot.Client.FindGroup(groupId); g != nil {
if m := g.FindMember(userId); m != nil {
- m.Kick(msg)
+ m.Kick(msg, block)
return OK(nil)
}
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
@@ -331,7 +490,7 @@ func (bot *CQBot) CQSetGroupBan(groupId, userId int64, duration uint32) MSG {
return OK(nil)
}
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
@@ -340,7 +499,7 @@ func (bot *CQBot) CQSetGroupWholeBan(groupId int64, enable bool) MSG {
g.MuteAll(enable)
return OK(nil)
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
@@ -349,14 +508,25 @@ func (bot *CQBot) CQSetGroupLeave(groupId int64) MSG {
g.Quit()
return OK(nil)
}
- return Failed(100)
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+}
+
+func (bot *CQBot) CQGetAtAllRemain(groupId int64) MSG {
+ if g := bot.Client.FindGroup(groupId); g != nil {
+ i, err := bot.Client.GetAtAllRemain(groupId)
+ if err != nil {
+ return Failed(100, "GROUP_REMAIN_API_ERROR", err.Error())
+ }
+ return OK(i)
+ }
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) MSG {
req, ok := bot.friendReqCache.Load(flag)
if !ok {
- return Failed(100)
+ return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在")
}
if approve {
req.(*client.NewFriendRequest).Accept()
@@ -368,39 +538,67 @@ func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) MSG {
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%EF%BC%8F%E9%82%80%E8%AF%B7
func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bool) MSG {
+ msgs, err := bot.Client.GetGroupSystemMessages()
+ if err != nil {
+ log.Errorf("获取群系统消息失败: %v", err)
+ return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error())
+ }
if subType == "add" {
- req, ok := bot.joinReqCache.Load(flag)
- if !ok {
- return Failed(100)
+ for _, req := range msgs.JoinRequests {
+ if strconv.FormatInt(req.RequestId, 10) == flag {
+ if req.Checked {
+ log.Errorf("处理群系统消息失败: 无法操作已处理的消息.")
+ return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
+ }
+ if approve {
+ req.Accept()
+ } else {
+ req.Reject(false, reason)
+ }
+ return OK(nil)
+ }
}
- bot.joinReqCache.Delete(flag)
- if approve {
- req.(*client.UserJoinGroupRequest).Accept()
- } else {
- req.(*client.UserJoinGroupRequest).Reject(false, reason)
+ } else {
+ for _, req := range msgs.InvitedRequests {
+ if strconv.FormatInt(req.RequestId, 10) == flag {
+ if req.Checked {
+ log.Errorf("处理群系统消息失败: 无法操作已处理的消息.")
+ return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
+ }
+ if approve {
+ req.Accept()
+ } else {
+ req.Reject(false, reason)
+ }
+ return OK(nil)
+ }
}
- return OK(nil)
}
- req, ok := bot.invitedReqCache.Load(flag)
- if ok {
- bot.invitedReqCache.Delete(flag)
- if approve {
- req.(*client.GroupInvitedRequest).Accept()
- } else {
- req.(*client.GroupInvitedRequest).Reject(false, reason)
- }
- return OK(nil)
- }
- return Failed(100)
+ log.Errorf("处理群系统消息失败: 消息 %v 不存在.", flag)
+ return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在")
}
// https://cqhttp.cc/docs/4.15/#/API?id=delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
func (bot *CQBot) CQDeleteMessage(messageId int32) MSG {
- msg := bot.GetGroupMessage(messageId)
+ msg := bot.GetMessage(messageId)
if msg == nil {
- return Failed(100)
+ return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
+ }
+ if _, ok := msg["group"]; ok {
+ if err := bot.Client.RecallGroupMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
+ log.Warnf("撤回 %v 失败: %v", messageId, err)
+ return Failed(100, "RECALL_API_ERROR", err.Error())
+ }
+ } else {
+ if msg["sender"].(message.Sender).Uin != bot.Client.Uin {
+ log.Warnf("撤回 %v 失败: 好友会话无法撤回对方消息.", messageId)
+ return Failed(100, "CANNOT_RECALL_FRIEND_MSG", "无法撤回对方消息")
+ }
+ if err := bot.Client.RecallPrivateMessage(msg["target"].(int64), int64(msg["time"].(int32)), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
+ log.Warnf("撤回 %v 失败: %v", messageId, err)
+ return Failed(100, "RECALL_API_ERROR", err.Error())
+ }
}
- bot.Client.RecallGroupMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32))
return OK(nil)
}
@@ -408,29 +606,28 @@ func (bot *CQBot) CQDeleteMessage(messageId int32) MSG {
func (bot *CQBot) CQSetGroupAdmin(groupId, userId int64, enable bool) MSG {
group := bot.Client.FindGroup(groupId)
if group == nil || group.OwnerUin != bot.Client.Uin {
- return Failed(100)
+ return Failed(100, "PERMISSION_DENIED", "群不存在或权限不足")
}
mem := group.FindMember(userId)
if mem == nil {
- return Failed(100)
+ return Failed(100, "GROUP_MEMBER_NOT_FOUND", "群成员不存在")
}
mem.SetAdmin(enable)
t, err := bot.Client.GetGroupMembers(group)
if err != nil {
log.Warnf("刷新群 %v 成员列表失败: %v", groupId, err)
- return Failed(100)
+ return Failed(100, "GET_MEMBERS_API_ERROR", err.Error())
}
group.Members = t
return OK(nil)
}
func (bot *CQBot) CQGetVipInfo(userId int64) MSG {
- msg := MSG{}
vip, err := bot.Client.GetVipInfo(userId)
if err != nil {
- return Failed(100)
+ return Failed(100, "VIP_API_ERROR", err.Error())
}
- msg = MSG{
+ msg := MSG{
"user_id": vip.Uin,
"nickname": vip.Name,
"level": vip.Level,
@@ -497,6 +694,31 @@ func (bot *CQBot) CQGetGroupHonorInfo(groupId int64, t string) MSG {
return OK(msg)
}
+// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
+func (bot *CQBot) CQGetStrangerInfo(userId int64) MSG {
+ info, err := bot.Client.GetSummaryInfo(userId)
+ if err != nil {
+ return Failed(100, "SUMMARY_API_ERROR", err.Error())
+ }
+ return OK(MSG{
+ "user_id": info.Uin,
+ "nickname": info.Nickname,
+ "qid": info.Qid,
+ "sex": func() string {
+ if info.Sex == 1 {
+ return "female"
+ } else if info.Sex == 0 {
+ return "male"
+ }
+ // unknown = 0x2
+ return "unknown"
+ }(),
+ "age": info.Age,
+ "level": info.Level,
+ "login_days": info.LoginDays,
+ })
+}
+
// https://cqhttp.cc/docs/4.15/#/API?id=-handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
// https://github.com/richardchien/coolq-http-api/blob/master/src/cqhttp/plugins/web/http.cpp#L376
func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
@@ -528,7 +750,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
bot.CQDeleteMessage(int32(context.Get("message_id").Int()))
}
if operation.Get("kick").Bool() && !isAnonymous {
- bot.CQSetGroupKick(context.Get("group_id").Int(), context.Get("user_id").Int(), "")
+ bot.CQSetGroupKick(context.Get("group_id").Int(), context.Get("user_id").Int(), "", operation.Get("reject_add_request").Bool())
}
if operation.Get("ban").Bool() {
var duration uint32 = 30 * 60
@@ -556,10 +778,10 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
}
func (bot *CQBot) CQGetImage(file string) MSG {
- if !global.PathExists(path.Join(global.IMAGE_PATH, file)) {
+ if !global.PathExists(path.Join(global.ImagePath, file)) {
return Failed(100)
}
- if b, err := ioutil.ReadFile(path.Join(global.IMAGE_PATH, file)); err == nil {
+ if b, err := ioutil.ReadFile(path.Join(global.ImagePath, file)); err == nil {
r := binary.NewReader(b)
r.ReadBytes(16)
msg := MSG{
@@ -567,7 +789,7 @@ func (bot *CQBot) CQGetImage(file string) MSG {
"filename": r.ReadString(),
"url": r.ReadString(),
}
- local := path.Join(global.CACHE_PATH, file+"."+path.Ext(msg["filename"].(string)))
+ local := path.Join(global.CachePath, file+"."+path.Ext(msg["filename"].(string)))
if !global.PathExists(local) {
if data, err := global.GetBytes(msg["url"].(string)); err == nil {
_ = ioutil.WriteFile(local, data, 0644)
@@ -575,14 +797,34 @@ func (bot *CQBot) CQGetImage(file string) MSG {
}
msg["file"] = local
return OK(msg)
+ } else {
+ return Failed(100, "LOAD_FILE_ERROR", err.Error())
}
- return Failed(100)
+}
+
+func (bot *CQBot) CQDownloadFile(url string, headers map[string]string, threadCount int) MSG {
+ hash := md5.Sum([]byte(url))
+ file := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
+ if global.PathExists(file) {
+ if err := os.Remove(file); err != nil {
+ log.Warnf("删除缓存文件 %v 时出现错误: %v", file, err)
+ return Failed(100, "DELETE_FILE_ERROR", err.Error())
+ }
+ }
+ if err := global.DownloadFileMultiThreading(url, file, 0, threadCount, headers); err != nil {
+ log.Warnf("下载链接 %v 时出现错误: %v", url, err)
+ return Failed(100, "DOWNLOAD_FILE_ERROR", err.Error())
+ }
+ abs, _ := filepath.Abs(file)
+ return OK(MSG{
+ "file": abs,
+ })
}
func (bot *CQBot) CQGetForwardMessage(resId string) MSG {
m := bot.Client.GetForwardMessage(resId)
if m == nil {
- return Failed(100)
+ return Failed(100, "MSG_NOT_FOUND", "消息不存在")
}
r := make([]MSG, 0)
for _, n := range m.Nodes {
@@ -601,21 +843,99 @@ func (bot *CQBot) CQGetForwardMessage(resId string) MSG {
})
}
-func (bot *CQBot) CQGetGroupMessage(messageId int32) MSG {
- msg := bot.GetGroupMessage(messageId)
+func (bot *CQBot) CQGetMessage(messageId int32) MSG {
+ msg := bot.GetMessage(messageId)
if msg == nil {
- return Failed(100)
+ return Failed(100, "MSG_NOT_FOUND", "消息不存在")
}
sender := msg["sender"].(message.Sender)
+ gid, isGroup := msg["group"]
+ raw := msg["message"].(string)
return OK(MSG{
- "message_id": messageId,
- "real_id": msg["message-id"],
+ "message_id": messageId,
+ "real_id": msg["message-id"],
+ "message_seq": msg["message-id"],
+ "group": isGroup,
+ "group_id": gid,
+ "message_type": func() string {
+ if isGroup {
+ return "group"
+ }
+ return "private"
+ }(),
"sender": MSG{
"user_id": sender.Uin,
"nickname": sender.Nickname,
},
- "time": msg["time"],
- "content": msg["message"],
+ "time": msg["time"],
+ "raw_message": raw,
+ "message": ToFormattedMessage(bot.ConvertStringMessage(raw, isGroup), func() int64 {
+ if isGroup {
+ return gid.(int64)
+ }
+ return sender.Uin
+ }(), false),
+ })
+}
+
+func (bot *CQBot) CQGetGroupSystemMessages() MSG {
+ msg, err := bot.Client.GetGroupSystemMessages()
+ if err != nil {
+ log.Warnf("获取群系统消息失败: %v", err)
+ return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error())
+ }
+ return OK(msg)
+}
+
+func (bot *CQBot) CQGetGroupMessageHistory(groupId int64, seq int64) MSG {
+ if g := bot.Client.FindGroup(groupId); g == nil {
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+ }
+ if seq == 0 {
+ g, err := bot.Client.GetGroupInfo(groupId)
+ if err != nil {
+ return Failed(100, "GROUP_INFO_API_ERROR", err.Error())
+ }
+ seq = g.LastMsgSeq
+ }
+ msg, err := bot.Client.GetGroupMessages(groupId, int64(math.Max(float64(seq-19), 1)), seq)
+ if err != nil {
+ log.Warnf("获取群历史消息失败: %v", err)
+ return Failed(100, "MESSAGES_API_ERROR", err.Error())
+ }
+ var ms []MSG
+ for _, m := range msg {
+ id := m.Id
+ bot.checkMedia(m.Elements)
+ if bot.db != nil {
+ id = bot.InsertGroupMessage(m)
+ }
+ t := bot.formatGroupMessage(m)
+ t["message_id"] = id
+ ms = append(ms, t)
+ }
+ return OK(MSG{
+ "messages": ms,
+ })
+}
+
+func (bot *CQBot) CQGetOnlineClients(noCache bool) MSG {
+ if noCache {
+ if err := bot.Client.RefreshStatus(); err != nil {
+ log.Warnf("刷新客户端状态时出现问题 %v", err)
+ return Failed(100, "REFRESH_STATUS_ERROR", err.Error())
+ }
+ }
+ var d []MSG
+ for _, oc := range bot.Client.OnlineClients {
+ d = append(d, MSG{
+ "app_id": oc.AppId,
+ "device_name": oc.DeviceName,
+ "device_kind": oc.DeviceKind,
+ })
+ }
+ return OK(MSG{
+ "clients": d,
})
}
@@ -627,11 +947,59 @@ func (bot *CQBot) CQCanSendRecord() MSG {
return OK(MSG{"yes": true})
}
+func (bot *CQBot) CQOcrImage(imageId string) MSG {
+ img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageId}, false, true)
+ if err != nil {
+ log.Warnf("load image error: %v", err)
+ return Failed(100, "LOAD_FILE_ERROR", err.Error())
+ }
+ rsp, err := bot.Client.ImageOcr(img)
+ if err != nil {
+ log.Warnf("ocr image error: %v", err)
+ return Failed(100, "OCR_API_ERROR", err.Error())
+ }
+ return OK(rsp)
+}
+
func (bot *CQBot) CQReloadEventFilter() MSG {
global.BootFilter()
return OK(nil)
}
+func (bot *CQBot) CQSetGroupPortrait(groupId int64, file, cache string) MSG {
+ if g := bot.Client.FindGroup(groupId); g != nil {
+ img, err := global.FindFile(file, cache, global.ImagePath)
+ if err != nil {
+ log.Warnf("set group portrait error: %v", err)
+ return Failed(100, "LOAD_FILE_ERROR", err.Error())
+ }
+ g.UpdateGroupHeadPortrait(img)
+ return OK(nil)
+ }
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+}
+
+func (bot *CQBot) CQSetGroupAnonymousBan(groupId int64, flag string, duration int32) MSG {
+ if flag == "" {
+ return Failed(100, "INVALID_FLAG", "无效的flag")
+ }
+ if g := bot.Client.FindGroup(groupId); g != nil {
+ s := strings.SplitN(flag, "|", 2)
+ if len(s) != 2 {
+ return Failed(100, "INVALID_FLAG", "无效的flag")
+ }
+ id := s[0]
+ nick := s[1]
+ if err := g.MuteAnonymous(id, nick, duration); err != nil {
+ log.Warnf("anonymous ban error: %v", err)
+ return Failed(100, "CALL_API_ERROR", err.Error())
+ }
+ return OK(nil)
+ }
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+}
+
+// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_status-%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81
func (bot *CQBot) CQGetStatus() MSG {
return OK(MSG{
"app_initialized": true,
@@ -639,7 +1007,76 @@ func (bot *CQBot) CQGetStatus() MSG {
"plugins_good": nil,
"app_good": true,
"online": bot.Client.Online,
- "good": true,
+ "good": bot.Client.Online,
+ "stat": bot.Client.GetStatistics(),
+ })
+}
+
+// CQSetEssenceMessage 设置精华消息
+func (bot *CQBot) CQSetEssenceMessage(messageID int32) MSG {
+ msg := bot.GetMessage(messageID)
+ if msg == nil {
+ return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
+ }
+ if _, ok := msg["group"]; ok {
+ if err := bot.Client.SetEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
+ log.Warnf("设置精华消息 %v 失败: %v", messageID, err)
+ return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error())
+ }
+ } else {
+ log.Warnf("设置精华消息 %v 失败: 非群聊", messageID)
+ return Failed(100, "SET_ESSENCE_MSG_ERROR", "非群聊")
+ }
+ return OK(nil)
+}
+
+// CQDeleteEssenceMessage 移出精华消息
+func (bot *CQBot) CQDeleteEssenceMessage(messageID int32) MSG {
+ msg := bot.GetMessage(messageID)
+ if msg == nil {
+ return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
+ }
+ if _, ok := msg["group"]; ok {
+ if err := bot.Client.DeleteEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
+ log.Warnf("移出精华消息 %v 失败: %v", messageID, err)
+ return Failed(100, "DEL_ESSENCE_MSG_ERROR", err.Error())
+ }
+ } else {
+ log.Warnf("移出精华消息 %v 失败: 非群聊", messageID)
+ return Failed(100, "DEL_ESSENCE_MSG_ERROR", "非群聊")
+ }
+ return OK(nil)
+}
+
+// CQGetEssenceMessageList 获取精华消息列表
+func (bot *CQBot) CQGetEssenceMessageList(groupCode int64) MSG {
+ g := bot.Client.FindGroup(groupCode)
+ if g == nil {
+ return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
+ }
+ msgList, err := bot.Client.GetGroupEssenceMsgList(groupCode)
+ if err != nil {
+ return Failed(100, "GET_ESSENCE_LIST_FOUND", err.Error())
+ }
+ list := make([]MSG, 0)
+ for _, m := range msgList {
+ var msg = MSG{
+ "sender_nick": m.SenderNick,
+ "sender_time": m.SenderTime,
+ "operator_time": m.AddDigestTime,
+ "operator_nick": m.AddDigestNick,
+ }
+ msg["sender_id"], _ = strconv.ParseUint(m.SenderUin, 10, 64)
+ msg["operator_id"], _ = strconv.ParseUint(m.AddDigestUin, 10, 64)
+ msg["message_id"] = ToGlobalId(groupCode, int32(m.MessageID))
+ list = append(list, msg)
+ }
+ return OK(list)
+}
+
+func (bot *CQBot) CQCheckUrlSafely(url string) MSG {
+ return OK(MSG{
+ "level": bot.Client.CheckUrlSafely(url),
})
}
@@ -655,6 +1092,20 @@ func (bot *CQBot) CQGetVersionInfo() MSG {
"runtime_version": runtime.Version(),
"runtime_os": runtime.GOOS,
"version": Version,
+ "protocol": func() int {
+ switch client.SystemDeviceInfo.Protocol {
+ case client.IPad:
+ return 0
+ case client.AndroidPhone:
+ return 1
+ case client.AndroidWatch:
+ return 2
+ case client.MacOS:
+ return 3
+ default:
+ return -1
+ }
+ }(),
})
}
@@ -662,17 +1113,33 @@ func OK(data interface{}) MSG {
return MSG{"data": data, "retcode": 0, "status": "ok"}
}
-func Failed(code int) MSG {
- return MSG{"data": nil, "retcode": code, "status": "failed"}
+func Failed(code int, msg ...string) MSG {
+ m := ""
+ w := ""
+ if len(msg) > 0 {
+ m = msg[0]
+ }
+ if len(msg) > 1 {
+ w = msg[1]
+ }
+ return MSG{"data": nil, "retcode": code, "msg": m, "wording": w, "status": "failed"}
}
func convertGroupMemberInfo(groupId int64, m *client.GroupMemberInfo) MSG {
return MSG{
- "group_id": groupId,
- "user_id": m.Uin,
- "nickname": m.Nickname,
- "card": m.CardName,
- "sex": "unknown",
+ "group_id": groupId,
+ "user_id": m.Uin,
+ "nickname": m.Nickname,
+ "card": m.CardName,
+ "sex": func() string {
+ if m.Gender == 1 {
+ return "female"
+ } else if m.Gender == 0 {
+ return "male"
+ }
+ // unknown = 0xff
+ return "unknown"
+ }(),
"age": 0,
"area": "",
"join_time": m.JoinTime,
@@ -694,3 +1161,12 @@ func convertGroupMemberInfo(groupId int64, m *client.GroupMemberInfo) MSG {
"card_changeable": false,
}
}
+
+func limitedString(str string) string {
+ if strings.Count(str, "") <= 10 {
+ return str
+ }
+ limited := []rune(str)
+ limited = limited[:10]
+ return string(limited) + " ..."
+}
diff --git a/coolq/bot.go b/coolq/bot.go
index 063b94d..bad338f 100644
--- a/coolq/bot.go
+++ b/coolq/bot.go
@@ -3,50 +3,54 @@ package coolq
import (
"bytes"
"encoding/gob"
- "encoding/json"
+ "encoding/hex"
"fmt"
"hash/crc32"
+ "io"
+ "os"
"path"
+ "runtime/debug"
"sync"
"time"
+ "github.com/Mrs4s/MiraiGo/utils"
+
+ "github.com/syndtr/goleveldb/leveldb"
+
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/go-cqhttp/global"
+ jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
- "github.com/xujiajun/nutsdb"
)
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
type CQBot struct {
Client *client.QQClient
- events []func(MSG)
- db *nutsdb.DB
- friendReqCache sync.Map
- invitedReqCache sync.Map
- joinReqCache sync.Map
- tempMsgCache sync.Map
- oneWayMsgCache sync.Map
+ events []func(MSG)
+ db *leveldb.DB
+ friendReqCache sync.Map
+ tempMsgCache sync.Map
+ oneWayMsgCache sync.Map
}
type MSG map[string]interface{}
var ForceFragmented = false
-func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
+func NewQQBot(cli *client.QQClient, conf *global.JSONConfig) *CQBot {
bot := &CQBot{
Client: cli,
}
if conf.EnableDB {
- opt := nutsdb.DefaultOptions
- opt.Dir = path.Join("data", "db")
- opt.EntryIdxMode = nutsdb.HintBPTSparseIdxMode
- db, err := nutsdb.Open(opt)
+ p := path.Join("data", "leveldb")
+ db, err := leveldb.OpenFile(p, nil)
if err != nil {
- log.Fatalf("打开数据库失败, 如果频繁遇到此问题请清理 data/db 文件夹或关闭数据库功能。")
+ log.Fatalf("打开数据库失败, 如果频繁遇到此问题请清理 data/leveldb 文件夹或关闭数据库功能。")
}
bot.db = db
gob.Register(message.Sender{})
@@ -59,16 +63,22 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot.Client.OnTempMessage(bot.tempMessageEvent)
bot.Client.OnGroupMuted(bot.groupMutedEvent)
bot.Client.OnGroupMessageRecalled(bot.groupRecallEvent)
+ bot.Client.OnGroupNotify(bot.groupNotifyEvent)
+ bot.Client.OnFriendNotify(bot.friendNotifyEvent)
bot.Client.OnFriendMessageRecalled(bot.friendRecallEvent)
+ bot.Client.OnReceivedOfflineFile(bot.offlineFileEvent)
bot.Client.OnJoinGroup(bot.joinGroupEvent)
bot.Client.OnLeaveGroup(bot.leaveGroupEvent)
bot.Client.OnGroupMemberJoined(bot.memberJoinEvent)
bot.Client.OnGroupMemberLeaved(bot.memberLeaveEvent)
bot.Client.OnGroupMemberPermissionChanged(bot.memberPermissionChangedEvent)
+ bot.Client.OnGroupMemberCardUpdated(bot.memberCardUpdatedEvent)
bot.Client.OnNewFriendRequest(bot.friendRequestEvent)
bot.Client.OnNewFriendAdded(bot.friendAddedEvent)
bot.Client.OnGroupInvited(bot.groupInvitedEvent)
bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent)
+ bot.Client.OnOtherClientStatusChanged(bot.otherClientStatusChangedEvent)
+ bot.Client.OnGroupDigest(bot.groupEssenceMsg)
go func() {
i := conf.HeartbeatInterval
if i < 0 {
@@ -85,7 +95,7 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
"self_id": bot.Client.Uin,
"post_type": "meta_event",
"meta_event_type": "heartbeat",
- "status": nil,
+ "status": bot.CQGetStatus()["data"],
"interval": 1000 * i,
})
}
@@ -97,31 +107,64 @@ func (bot *CQBot) OnEventPush(f func(m MSG)) {
bot.events = append(bot.events, f)
}
-func (bot *CQBot) GetGroupMessage(mid int32) MSG {
+func (bot *CQBot) GetMessage(mid int32) MSG {
if bot.db != nil {
m := MSG{}
- err := bot.db.View(func(tx *nutsdb.Tx) error {
- e, err := tx.Get("group-messages", binary.ToBytes(mid))
- if err != nil {
- return err
- }
- buff := new(bytes.Buffer)
- buff.Write(binary.GZipUncompress(e.Value))
- return gob.NewDecoder(buff).Decode(&m)
- })
+ data, err := bot.db.Get(binary.ToBytes(mid), nil)
if err == nil {
- return m
+ buff := new(bytes.Buffer)
+ buff.Write(binary.GZipUncompress(data))
+ err = gob.NewDecoder(buff).Decode(&m)
+ if err == nil {
+ return m
+ }
}
log.Warnf("获取信息时出现错误: %v id: %v", err, mid)
}
return nil
}
+func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElement) (*message.GroupImageElement, error) {
+ if img.Stream != nil {
+ return bot.Client.UploadGroupImage(groupCode, img.Stream)
+ }
+ return bot.Client.UploadGroupImageByFile(groupCode, img.File)
+}
+
+func (bot *CQBot) UploadLocalVideo(target int64, v *LocalVideoElement) (*message.ShortVideoElement, error) {
+ if v.File != "" {
+ video, err := os.Open(v.File)
+ if err != nil {
+ return nil, err
+ }
+ defer video.Close()
+ hash, _ := utils.ComputeMd5AndLength(io.MultiReader(video, v.thumb))
+ cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
+ _, _ = video.Seek(0, io.SeekStart)
+ _, _ = v.thumb.Seek(0, io.SeekStart)
+ return bot.Client.UploadGroupShortVideo(target, video, v.thumb, cacheFile)
+ }
+ return &v.ShortVideoElement, nil
+}
+
+func (bot *CQBot) UploadLocalImageAsPrivate(userId int64, img *LocalImageElement) (*message.FriendImageElement, error) {
+ if img.Stream != nil {
+ return bot.Client.UploadPrivateImage(userId, img.Stream)
+ }
+ // need update.
+ f, err := os.Open(img.File)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return bot.Client.UploadPrivateImage(userId, f)
+}
+
func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int32 {
var newElem []message.IMessageElement
for _, elem := range m.Elements {
- if i, ok := elem.(*message.ImageElement); ok {
- gm, err := bot.Client.UploadGroupImage(groupId, i.Data)
+ if i, ok := elem.(*LocalImageElement); ok {
+ gm, err := bot.UploadLocalImageAsGroup(groupId, i)
if err != nil {
log.Warnf("警告: 群 %v 消息图片上传失败: %v", groupId, err)
continue
@@ -130,7 +173,7 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
continue
}
if i, ok := elem.(*message.VoiceElement); ok {
- gv, err := bot.Client.UploadGroupPtt(groupId, i.Data)
+ gv, err := bot.Client.UploadGroupPtt(groupId, bytes.NewReader(i.Data))
if err != nil {
log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err)
continue
@@ -138,9 +181,43 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
newElem = append(newElem, gv)
continue
}
+ if i, ok := elem.(*LocalVideoElement); ok {
+ gv, err := bot.UploadLocalVideo(groupId, i)
+ if err != nil {
+ log.Warnf("警告: 群 %v 消息短视频上传失败: %v", groupId, err)
+ continue
+ }
+ newElem = append(newElem, gv)
+ continue
+ }
+ if i, ok := elem.(*PokeElement); ok {
+ if group := bot.Client.FindGroup(groupId); group != nil {
+ if mem := group.FindMember(i.Target); mem != nil {
+ mem.Poke()
+ return 0
+ }
+ }
+ }
+ if i, ok := elem.(*GiftElement); ok {
+ bot.Client.SendGroupGift(uint64(groupId), uint64(i.Target), i.GiftId)
+ return 0
+ }
+ if i, ok := elem.(*message.MusicShareElement); ok {
+ ret, err := bot.Client.SendGroupMusicShare(groupId, i)
+ if err != nil {
+ log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err)
+ return -1
+ }
+ return bot.InsertGroupMessage(ret)
+ }
newElem = append(newElem, elem)
}
+ if len(newElem) == 0 {
+ log.Warnf("群消息发送失败: 消息为空.")
+ return -1
+ }
m.Elements = newElem
+ bot.checkMedia(newElem)
ret := bot.Client.SendGroupMessage(groupId, m, ForceFragmented)
if ret == nil || ret.Id == -1 {
log.Warnf("群消息发送失败: 账号可能被风控.")
@@ -152,8 +229,8 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) int32 {
var newElem []message.IMessageElement
for _, elem := range m.Elements {
- if i, ok := elem.(*message.ImageElement); ok {
- fm, err := bot.Client.UploadPrivateImage(target, i.Data)
+ if i, ok := elem.(*LocalImageElement); ok {
+ fm, err := bot.UploadLocalImageAsPrivate(target, i)
if err != nil {
log.Warnf("警告: 私聊 %v 消息图片上传失败.", target)
continue
@@ -161,30 +238,61 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
newElem = append(newElem, fm)
continue
}
+ if i, ok := elem.(*PokeElement); ok {
+ bot.Client.SendFriendPoke(i.Target)
+ return 0
+ }
+ if i, ok := elem.(*message.VoiceElement); ok {
+ fv, err := bot.Client.UploadPrivatePtt(target, i.Data)
+ if err != nil {
+ log.Warnf("警告: 私聊 %v 消息语音上传失败: %v", target, err)
+ continue
+ }
+ newElem = append(newElem, fv)
+ continue
+ }
+ if i, ok := elem.(*LocalVideoElement); ok {
+ gv, err := bot.UploadLocalVideo(target, i)
+ if err != nil {
+ log.Warnf("警告: 私聊 %v 消息短视频上传失败: %v", target, err)
+ continue
+ }
+ newElem = append(newElem, gv)
+ continue
+ }
+ if i, ok := elem.(*message.MusicShareElement); ok {
+ bot.Client.SendFriendMusicShare(target, i)
+ return 0
+ }
newElem = append(newElem, elem)
}
+ if len(newElem) == 0 {
+ log.Warnf("好友消息发送失败: 消息为空.")
+ return -1
+ }
m.Elements = newElem
+ bot.checkMedia(newElem)
var id int32 = -1
- if bot.Client.FindFriend(target) != nil {
+ if bot.Client.FindFriend(target) != nil { // 双向好友
msg := bot.Client.SendPrivateMessage(target, m)
if msg != nil {
- id = msg.Id
+ id = bot.InsertPrivateMessage(msg)
}
- } else if code, ok := bot.tempMsgCache.Load(target); ok {
+ } else if code, ok := bot.tempMsgCache.Load(target); ok { // 临时会话
msg := bot.Client.SendTempMessage(code.(int64), target, m)
if msg != nil {
id = msg.Id
}
- } else if _, ok := bot.oneWayMsgCache.Load(target); ok {
+ } else if _, ok := bot.oneWayMsgCache.Load(target); ok { // 单向好友
msg := bot.Client.SendPrivateMessage(target, m)
if msg != nil {
- id = msg.Id
+ id = bot.InsertPrivateMessage(msg)
}
}
if id == -1 {
return -1
}
- return ToGlobalId(target, id)
+ return id
}
func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 {
@@ -199,14 +307,36 @@ func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 {
}
id := ToGlobalId(m.GroupCode, m.Id)
if bot.db != nil {
- err := bot.db.Update(func(tx *nutsdb.Tx) error {
- buf := new(bytes.Buffer)
- if err := gob.NewEncoder(buf).Encode(val); err != nil {
- return err
- }
- return tx.Put("group-messages", binary.ToBytes(id), binary.GZipCompress(buf.Bytes()), 0)
- })
- if err != nil {
+ buf := new(bytes.Buffer)
+ if err := gob.NewEncoder(buf).Encode(val); err != nil {
+ log.Warnf("记录聊天数据时出现错误: %v", err)
+ return -1
+ }
+ if err := bot.db.Put(binary.ToBytes(id), binary.GZipCompress(buf.Bytes()), nil); err != nil {
+ log.Warnf("记录聊天数据时出现错误: %v", err)
+ return -1
+ }
+ }
+ return id
+}
+
+func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage) int32 {
+ val := MSG{
+ "message-id": m.Id,
+ "internal-id": m.InternalId,
+ "target": m.Target,
+ "sender": m.Sender,
+ "time": m.Time,
+ "message": ToStringMessage(m.Elements, m.Sender.Uin, true),
+ }
+ id := ToGlobalId(m.Sender.Uin, m.Id)
+ if bot.db != nil {
+ buf := new(bytes.Buffer)
+ if err := gob.NewEncoder(buf).Encode(val); err != nil {
+ log.Warnf("记录聊天数据时出现错误: %v", err)
+ return -1
+ }
+ if err := bot.db.Put(binary.ToBytes(id), binary.GZipCompress(buf.Bytes()), nil); err != nil {
log.Warnf("记录聊天数据时出现错误: %v", err)
return -1
}
@@ -225,25 +355,78 @@ func (bot *CQBot) Release() {
}
func (bot *CQBot) dispatchEventMessage(m MSG) {
- payload := gjson.Parse(m.ToJson())
- filter := global.EventFilter
- if filter != nil && (*filter).Eval(payload) == false {
+ if global.EventFilter != nil && !global.EventFilter.Eval(global.MSG(m)) {
log.Debug("Event filtered!")
return
}
for _, f := range bot.events {
- fn := f
- go func() {
+ go func(fn func(MSG)) {
+ defer func() {
+ if pan := recover(); pan != nil {
+ log.Warnf("处理事件 %v 时出现错误: %v \n%s", m, pan, debug.Stack())
+ }
+ }()
start := time.Now()
fn(m)
end := time.Now()
if end.Sub(start) > time.Second*5 {
log.Debugf("警告: 事件处理耗时超过 5 秒 (%v), 请检查应用是否有堵塞.", end.Sub(start))
}
- }()
+ }(f)
}
}
+func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) MSG {
+ cqm := ToStringMessage(m.Elements, m.GroupCode, true)
+ gm := MSG{
+ "anonymous": nil,
+ "font": 0,
+ "group_id": m.GroupCode,
+ "message": ToFormattedMessage(m.Elements, m.GroupCode, false),
+ "message_type": "group",
+ "message_seq": m.Id,
+ "post_type": "message",
+ "raw_message": cqm,
+ "self_id": bot.Client.Uin,
+ "sender": MSG{
+ "age": 0,
+ "area": "",
+ "level": "",
+ "sex": "unknown",
+ "user_id": m.Sender.Uin,
+ },
+ "sub_type": "normal",
+ "time": time.Now().Unix(),
+ "user_id": m.Sender.Uin,
+ }
+ if m.Sender.IsAnonymous() {
+ gm["anonymous"] = MSG{
+ "flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
+ "id": m.Sender.Uin,
+ "name": m.Sender.AnonymousInfo.AnonymousNick,
+ }
+ gm["sender"].(MSG)["nickname"] = "匿名消息"
+ gm["sub_type"] = "anonymous"
+ } else {
+ mem := bot.Client.FindGroup(m.GroupCode).FindMember(m.Sender.Uin)
+ ms := gm["sender"].(MSG)
+ ms["role"] = func() string {
+ switch mem.Permission {
+ case client.Owner:
+ return "owner"
+ case client.Administrator:
+ return "admin"
+ default:
+ return "member"
+ }
+ }()
+ ms["nickname"] = mem.Nickname
+ ms["card"] = mem.CardName
+ ms["title"] = mem.SpecialTitle
+ }
+ return gm
+}
+
func formatGroupName(group *client.GroupInfo) string {
return fmt.Sprintf("%s(%d)", group.Name, group.Code)
}
diff --git a/coolq/cqcode.go b/coolq/cqcode.go
index 76929d5..cdfe09e 100644
--- a/coolq/cqcode.go
+++ b/coolq/cqcode.go
@@ -1,32 +1,98 @@
package coolq
import (
+ "bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
+ xml2 "encoding/xml"
"errors"
"fmt"
- "github.com/Mrs4s/MiraiGo/binary"
- "github.com/Mrs4s/MiraiGo/message"
- "github.com/Mrs4s/go-cqhttp/global"
- log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
+ "io"
"io/ioutil"
+ "math"
+ "math/rand"
"net/url"
+ "os"
"path"
- "regexp"
"runtime"
"strconv"
"strings"
+ "time"
+
+ "github.com/Mrs4s/MiraiGo/binary"
+ "github.com/Mrs4s/MiraiGo/message"
+ "github.com/Mrs4s/MiraiGo/utils"
+ "github.com/Mrs4s/go-cqhttp/global"
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
)
+/*
var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`)
var typeReg = regexp.MustCompile(`\[CQ:(\w+)`)
var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`)
+*/
var IgnoreInvalidCQCode = false
+var SplitUrl = false
+
+const maxImageSize = 1024 * 1024 * 30 // 30MB
+const maxVideoSize = 1024 * 1024 * 100 // 100MB
+
+type PokeElement struct {
+ Target int64
+}
+
+type GiftElement struct {
+ Target int64
+ GiftId message.GroupGift
+}
+
+type LocalImageElement struct {
+ message.ImageElement
+ Stream io.ReadSeeker
+ File string
+}
+
+type LocalVoiceElement struct {
+ message.VoiceElement
+ Stream io.ReadSeeker
+}
+
+type LocalVideoElement struct {
+ message.ShortVideoElement
+ File string
+ thumb io.ReadSeeker
+}
+
+func (e *GiftElement) Type() message.ElementType {
+ return message.At
+}
+
+var GiftId = [...]message.GroupGift{
+ message.SweetWink,
+ message.HappyCola,
+ message.LuckyBracelet,
+ message.Cappuccino,
+ message.CatWatch,
+ message.FleeceGloves,
+ message.RainbowCandy,
+ message.Stronger,
+ message.LoveMicrophone,
+ message.HoldingYourHand,
+ message.CuteCat,
+ message.MysteryMask,
+ message.ImBusy,
+ message.LoveMask,
+}
+
+func (e *PokeElement) Type() message.ElementType {
+ return message.At
+}
func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []MSG) {
+ r = []MSG{}
ur := false
if len(raw) != 0 {
ur = raw[0]
@@ -43,7 +109,7 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M
})
}
for _, elem := range e {
- m := MSG{}
+ var m MSG
switch o := elem.(type) {
case *message.TextElement:
m = MSG{
@@ -71,6 +137,11 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M
"data": map[string]string{"qq": fmt.Sprint(o.Target)},
}
}
+ case *message.RedBagElement:
+ m = MSG{
+ "type": "redbag",
+ "data": map[string]string{"title": o.Title},
+ }
case *message.ForwardElement:
m = MSG{
"type": "forward",
@@ -117,6 +188,40 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M
"data": map[string]string{"file": o.Filename, "url": o.Url},
}
}
+ case *message.GroupImageElement:
+ if ur {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"},
+ }
+ } else {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)},
+ }
+ }
+ case *message.FriendImageElement:
+ if ur {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"},
+ }
+ } else {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)},
+ }
+ }
+ case *message.GroupFlashImgElement:
+ return []MSG{{
+ "type": "image",
+ "data": map[string]string{"file": o.Filename, "type": "flash"},
+ }}
+ case *message.FriendFlashImgElement:
+ return []MSG{{
+ "type": "image",
+ "data": map[string]string{"file": o.Filename, "type": "flash"},
+ }}
case *message.ServiceElement:
if isOk := strings.Contains(o.Content, " si {
- text := m[si:idx[0]]
- r = append(r, message.NewText(CQCodeUnescapeText(text)))
+func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMessageElement) {
+ index := 0
+ stat := 0
+ rMsg := []rune(msg)
+ var tempText, cqCode []rune
+ hasNext := func() bool {
+ return index < len(rMsg)
+ }
+ next := func() rune {
+ r := rMsg[index]
+ index++
+ return r
+ }
+ move := func(steps int) {
+ index += steps
+ }
+ peekN := func(count int) string {
+ lastIdx := int(math.Min(float64(index+count), float64(len(rMsg))))
+ return string(rMsg[index:lastIdx])
+ }
+ isCQCodeBegin := func(r rune) bool {
+ return r == '[' && peekN(3) == "CQ:"
+ }
+ saveTempText := func() {
+ if len(tempText) != 0 {
+ if SplitUrl {
+ for _, t := range global.SplitURL(CQCodeUnescapeValue(string(tempText))) {
+ r = append(r, message.NewText(t))
+ }
+ } else {
+ r = append(r, message.NewText(CQCodeUnescapeValue(string(tempText))))
+ }
}
- code := m[idx[0]:idx[1]]
- si = idx[1]
- t := typeReg.FindAllStringSubmatch(code, -1)[0][1]
- ps := paramReg.FindAllStringSubmatch(code, -1)
- d := make(map[string]string)
- for _, p := range ps {
- d[p[1]] = CQCodeUnescapeValue(p[2])
+ tempText = []rune{}
+ cqCode = []rune{}
+ }
+ saveCQCode := func() {
+ defer func() {
+ cqCode = []rune{}
+ tempText = []rune{}
+ }()
+ s := strings.SplitN(string(cqCode), ",", -1)
+ if len(s) == 0 {
+ return
}
- if t == "reply" && group {
+ t := s[0]
+ params := make(map[string]string)
+ for i := 1; i < len(s); i++ {
+ p := s[i]
+ p = strings.TrimSpace(p)
+ if p == "" {
+ continue
+ }
+ data := strings.SplitN(p, "=", 2)
+ if len(data) == 2 {
+ params[data[0]] = CQCodeUnescapeValue(data[1])
+ } else {
+ params[p] = ""
+ }
+ }
+ if t == "reply" { // reply 特殊处理
if len(r) > 0 {
if _, ok := r[0].(*message.ReplyElement); ok {
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
- continue
+ return
}
}
- mid, err := strconv.Atoi(d["id"])
+ mid, err := strconv.Atoi(params["id"])
+ customText := params["text"]
if err == nil {
- org := bot.GetGroupMessage(int32(mid))
+ org := bot.GetMessage(int32(mid))
if org != nil {
r = append([]message.IMessageElement{
&message.ReplyElement{
@@ -230,25 +400,79 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
Elements: bot.ConvertStringMessage(org["message"].(string), group),
},
}, r...)
- continue
+ return
}
+ } else if customText != "" {
+ sender, err := strconv.ParseInt(params["qq"], 10, 64)
+ if err != nil {
+ log.Warnf("警告:自定义 Reply 元素中必须包含Uin")
+ return
+ }
+ msgTime, err := strconv.ParseInt(params["time"], 10, 64)
+ if err != nil {
+ msgTime = time.Now().Unix()
+ }
+ r = append([]message.IMessageElement{
+ &message.ReplyElement{
+ ReplySeq: int32(0),
+ Sender: sender,
+ Time: int32(msgTime),
+ Elements: bot.ConvertStringMessage(customText, group),
+ },
+ }, r...)
+ return
}
}
- elem, err := bot.ToElement(t, d, group)
+ if t == "forward" { // 单独处理转发
+ if id, ok := params["id"]; ok {
+ r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)}
+ return
+ }
+ }
+ elem, err := bot.ToElement(t, params, group)
if err != nil {
+ org := "[CQ:" + string(cqCode) + "]"
if !IgnoreInvalidCQCode {
- log.Warnf("转换CQ码 %v 到MiraiGo Element时出现错误: %v 将原样发送.", code, err)
- r = append(r, message.NewText(code))
+ log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err)
+ r = append(r, message.NewText(org))
} else {
- log.Warnf("转换CQ码 %v 到MiraiGo Element时出现错误: %v 将忽略.", code, err)
+ log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err)
}
- continue
+ return
+ }
+ switch i := elem.(type) {
+ case message.IMessageElement:
+ r = append(r, i)
+ case []message.IMessageElement:
+ r = append(r, i...)
}
- r = append(r, elem)
}
- if si != len(m) {
- r = append(r, message.NewText(CQCodeUnescapeText(m[si:])))
+ for hasNext() {
+ ch := next()
+ switch stat {
+ case 0:
+ if isCQCodeBegin(ch) {
+ saveTempText()
+ tempText = append(tempText, []rune("[CQ:")...)
+ move(3)
+ stat = 1
+ } else {
+ tempText = append(tempText, ch)
+ }
+ case 1:
+ if isCQCodeBegin(ch) {
+ move(-1)
+ stat = 0
+ } else if ch == ']' {
+ saveCQCode()
+ stat = 0
+ } else {
+ cqCode = append(cqCode, ch)
+ tempText = append(tempText, ch)
+ }
+ }
}
+ saveTempText()
return
}
@@ -262,9 +486,10 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
return
}
}
- mid, err := strconv.Atoi(e.Get("data").Get("id").Str)
+ mid, err := strconv.Atoi(e.Get("data").Get("id").String())
+ customText := e.Get("data").Get("text").String()
if err == nil {
- org := bot.GetGroupMessage(int32(mid))
+ org := bot.GetMessage(int32(mid))
if org != nil {
r = append([]message.IMessageElement{
&message.ReplyElement{
@@ -276,11 +501,34 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
}, r...)
return
}
+ } else if customText != "" {
+ sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64)
+ if err != nil {
+ log.Warnf("警告:自定义 Reply 元素中必须包含Uin")
+ return
+ }
+ msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64)
+ if err != nil {
+ msgTime = time.Now().Unix()
+ }
+ r = append([]message.IMessageElement{
+ &message.ReplyElement{
+ ReplySeq: int32(0),
+ Sender: sender,
+ Time: int32(msgTime),
+ Elements: bot.ConvertStringMessage(customText, group),
+ },
+ }, r...)
+ return
}
}
+ if t == "forward" {
+ r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())}
+ return
+ }
d := make(map[string]string)
e.Get("data").ForEach(func(key, value gjson.Result) bool {
- d[key.Str] = value.Str
+ d[key.Str] = value.String()
return true
})
elem, err := bot.ToElement(t, d, group)
@@ -288,7 +536,12 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
log.Warnf("转换CQ码到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", err)
return
}
- r = append(r, elem)
+ switch i := elem.(type) {
+ case message.IMessageElement:
+ r = append(r, i)
+ case []message.IMessageElement:
+ r = append(r, i...)
+ }
}
if m.Type == gjson.String {
return bot.ConvertStringMessage(m.Str, group)
@@ -304,55 +557,97 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
return
}
-func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.IMessageElement, error) {
+// ToElement 将解码后的CQCode转换为Element.
+// 返回 interface{} 存在三种类型
+// message.IMessageElement []message.IMessageElement nil
+func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interface{}, err error) {
switch t {
case "text":
+ if SplitUrl {
+ var ret []message.IMessageElement
+ for _, text := range global.SplitURL(d["text"]) {
+ ret = append(ret, message.NewText(text))
+ }
+ return ret, nil
+ }
return message.NewText(d["text"]), nil
case "image":
- return bot.makeImageElem(t, d, group)
- case "record":
+ img, err := bot.makeImageOrVideoElem(d, false, group)
+ if err != nil {
+ return nil, err
+ }
+ tp := d["type"]
+ if tp != "show" && tp != "flash" {
+ return img, nil
+ }
+ if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧
+ if group {
+ img, err = bot.UploadLocalImageAsGroup(1, i)
+ } else {
+ img, err = bot.UploadLocalImageAsPrivate(1, i)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ switch tp {
+ case "flash":
+ if i, ok := img.(*message.GroupImageElement); ok {
+ return &message.GroupFlashPicElement{GroupImageElement: *i}, nil
+ }
+ if i, ok := img.(*message.FriendImageElement); ok {
+ return &message.FriendFlashPicElement{FriendImageElement: *i}, nil
+ }
+ case "show":
+ id, _ := strconv.ParseInt(d["id"], 10, 64)
+ if id < 40000 || id >= 40006 {
+ id = 40000
+ }
+ if i, ok := img.(*message.GroupImageElement); ok {
+ return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil
+ }
+ return img, nil // 私聊还没做
+ }
+
+ case "poke":
+ t, _ := strconv.ParseInt(d["qq"], 10, 64)
+ return &PokeElement{Target: t}, nil
+ case "gift":
if !group {
- return nil, errors.New("private voice unsupported now")
+ return nil, errors.New("private gift unsupported") // no free private gift
}
+ t, _ := strconv.ParseInt(d["qq"], 10, 64)
+ id, _ := strconv.Atoi(d["id"])
+ if id < 0 || id >= 14 {
+ return nil, errors.New("invalid gift id")
+ }
+ return &GiftElement{Target: t, GiftId: GiftId[id]}, nil
+ case "tts":
+ defer func() {
+ if r := recover(); r != nil {
+ m = nil
+ err = errors.New("tts 转换失败")
+ }
+ }()
+ data, err := bot.Client.GetTts(d["text"])
+ if err != nil {
+ return nil, err
+ }
+ return &message.VoiceElement{Data: data}, nil
+ case "record":
f := d["file"]
- var data []byte
- if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") {
- b, err := global.GetBytes(f)
- if err != nil {
- return nil, err
- }
- data = b
+ data, err := global.FindFile(f, d["cache"], global.VoicePath)
+ if err == global.ErrSyntax {
+ data, err = global.FindFile(f, d["cache"], global.VoicePathOld)
}
- if strings.HasPrefix(f, "base64") {
- b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", ""))
- if err != nil {
- return nil, err
- }
- data = b
- }
- if strings.HasPrefix(f, "file") {
- fu, err := url.Parse(f)
- if err != nil {
- return nil, err
- }
- if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` {
- fu.Path = fu.Path[1:]
- }
- b, err := ioutil.ReadFile(fu.Path)
- if err != nil {
- return nil, err
- }
- data = b
- }
- if global.PathExists(path.Join(global.VOICE_PATH, f)) {
- b, err := ioutil.ReadFile(path.Join(global.VOICE_PATH, f))
- if err != nil {
- return nil, err
- }
- data = b
+ if err != nil {
+ return nil, err
}
if !global.IsAMRorSILK(data) {
- return nil, errors.New("unsupported voice file format (please use AMR file for now)")
+ data, err = global.EncoderSilk(data)
+ if err != nil {
+ return nil, err
+ }
}
return &message.VoiceElement{Data: data}, nil
case "face":
@@ -380,7 +675,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
return nil, errors.New("song not found")
}
aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10)
- name := info.Get("track_info.name").Str + " - " + info.Get("track_info.singer.0.name").Str
+ name := info.Get("track_info.name").Str
mid := info.Get("track_info.mid").Str
albumMid := info.Get("track_info.album.mid").Str
pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576")
@@ -390,12 +685,18 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
if len(aid) < 2 {
return nil, errors.New("song error")
}
- content := "来自go-cqhttp"
+ content := info.Get("track_info.singer.0.name").Str
if d["content"] != "" {
content = d["content"]
}
- json := fmt.Sprintf("{\"app\": \"com.tencent.structmsg\",\"desc\": \"音乐\",\"meta\": {\"music\": {\"desc\": \"%s\",\"jumpUrl\": \"%s\",\"musicUrl\": \"%s\",\"preview\": \"%s\",\"tag\": \"QQ音乐\",\"title\": \"%s\"}},\"prompt\": \"[分享]%s\",\"ver\": \"0.0.0.1\",\"view\": \"music\"}", content, jumpUrl, purl, preview, name, name)
- return message.NewLightApp(json), nil
+ return &message.MusicShareElement{
+ MusicType: message.QQMusic,
+ Title: name,
+ Summary: content,
+ Url: jumpUrl,
+ PictureUrl: preview,
+ MusicUrl: purl,
+ }, nil
}
if d["type"] == "163" {
info, err := global.NeteaseMusicSongInfo(d["id"])
@@ -413,12 +714,39 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
if info.Get("artists.0").Exists() {
artistName = info.Get("artists.0.name").Str
}
- json := fmt.Sprintf("{\"app\": \"com.tencent.structmsg\",\"desc\":\"音乐\",\"view\":\"music\",\"prompt\":\"[分享]%s\",\"ver\":\"0.0.0.1\",\"meta\":{ \"music\": { \"desc\": \"%s\", \"jumpUrl\": \"%s\", \"musicUrl\": \"%s\", \"preview\": \"%s\", \"tag\": \"网易云音乐\", \"title\":\"%s\"}}}", name, artistName, jumpUrl, musicUrl, picUrl, name)
- return message.NewLightApp(json), nil
+ return &message.MusicShareElement{
+ MusicType: message.CloudMusic,
+ Title: name,
+ Summary: artistName,
+ Url: jumpUrl,
+ PictureUrl: picUrl,
+ MusicUrl: musicUrl,
+ }, nil
}
if d["type"] == "custom" {
+ if d["subtype"] != "" {
+ var subtype = map[string]int{
+ "qq": message.QQMusic,
+ "163": message.CloudMusic,
+ "migu": message.MiguMusic,
+ "kugou": message.KugouMusic,
+ "kuwo": message.KuwoMusic,
+ }
+ var musicType = 0
+ if tp, ok := subtype[d["subtype"]]; ok {
+ musicType = tp
+ }
+ return &message.MusicShareElement{
+ MusicType: musicType,
+ Title: d["title"],
+ Summary: d["content"],
+ Url: d["url"],
+ PictureUrl: d["image"],
+ MusicUrl: d["purl"],
+ }, nil
+ }
xml := fmt.Sprintf(`- %s%s
`,
- d["title"], d["url"], d["image"], d["audio"], d["title"], d["content"])
+ XmlEscape(d["title"]), d["url"], d["image"], d["audio"], XmlEscape(d["title"]), XmlEscape(d["content"]))
return &message.ServiceElement{
Id: 60,
Content: xml,
@@ -429,14 +757,12 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
case "xml":
resId := d["resid"]
template := CQCodeEscapeValue(d["data"])
- //println(template)
i, _ := strconv.ParseInt(resId, 10, 64)
msg := message.NewRichXml(template, i)
return msg, nil
case "json":
resId := d["resid"]
i, _ := strconv.ParseInt(resId, 10, 64)
- log.Warnf("json msg=%s", d["data"])
if i == 0 {
//默认情况下走小程序通道
msg := message.NewLightApp(CQCodeUnescapeValue(d["data"]))
@@ -448,30 +774,84 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
case "cardimage":
source := d["source"]
icon := d["icon"]
- minwidth, _ := strconv.ParseInt(d["minwidth"], 10, 64)
- if minwidth == 0 {
- minwidth = 200
+ minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64)
+ if minWidth == 0 {
+ minWidth = 200
}
- minheight, _ := strconv.ParseInt(d["minheight"], 10, 64)
- if minheight == 0 {
- minheight = 200
+ minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64)
+ if minHeight == 0 {
+ minHeight = 200
}
- maxwidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64)
- if maxwidth == 0 {
- maxwidth = 500
+ maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64)
+ if maxWidth == 0 {
+ maxWidth = 500
}
- maxheight, _ := strconv.ParseInt(d["maxheight"], 10, 64)
- if maxheight == 0 {
- maxheight = 1000
+ maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64)
+ if maxHeight == 0 {
+ maxHeight = 1000
}
- img, err := bot.makeImageElem(t, d, group)
+ img, err := bot.makeImageOrVideoElem(d, false, group)
if err != nil {
return nil, errors.New("send cardimage faild")
}
- return bot.SendNewPic(img, source, icon, minwidth, minheight, maxwidth, maxheight, group)
+ return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, group)
+ case "video":
+ cache := d["cache"]
+ if cache == "" {
+ cache = "1"
+ }
+ file, err := bot.makeImageOrVideoElem(d, true, group)
+ if err != nil {
+ return nil, err
+ }
+ v := file.(*LocalVideoElement)
+ if v.File == "" {
+ return v, nil
+ }
+ var data []byte
+ if cover, ok := d["cover"]; ok {
+ data, _ = global.FindFile(cover, cache, global.ImagePath)
+ } else {
+ _ = global.ExtractCover(v.File, v.File+".jpg")
+ data, _ = ioutil.ReadFile(v.File + ".jpg")
+ }
+ v.thumb = bytes.NewReader(data)
+ video, _ := os.Open(v.File)
+ defer video.Close()
+ _, err = video.Seek(4, io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+ var header = make([]byte, 4)
+ _, err = video.Read(header)
+ if err != nil {
+ return nil, err
+ }
+ if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp
+ _, _ = video.Seek(0, io.SeekStart)
+ hash, _ := utils.ComputeMd5AndLength(video)
+ cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4")
+ if global.PathExists(cacheFile) && cache == "1" {
+ goto ok
+ }
+ err = global.EncodeMP4(v.File, cacheFile)
+ if err != nil {
+ return nil, err
+ }
+ ok:
+ v.File = cacheFile
+ }
+ return v, nil
default:
return nil, errors.New("unsupported cq code: " + t)
}
+ return nil, nil
+}
+
+func XmlEscape(c string) string {
+ buf := new(bytes.Buffer)
+ _ = xml2.EscapeText(buf, []byte(c))
+ return buf.String()
}
func CQCodeEscapeText(raw string) string {
@@ -503,34 +883,37 @@ func CQCodeUnescapeValue(content string) string {
}
// 图片 elem 生成器,单独拎出来,用于公用
-func (bot *CQBot) makeImageElem(t string, d map[string]string, group bool) (message.IMessageElement, error) {
+func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) {
f := d["file"]
if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") {
cache := d["cache"]
+ c := d["c"]
if cache == "" {
cache = "1"
}
hash := md5.Sum([]byte(f))
- cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".cache")
- if global.PathExists(cacheFile) && cache == "1" {
- b, err := ioutil.ReadFile(cacheFile)
- if err == nil {
- return message.NewImage(b), nil
+ cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
+ var maxSize = func() int64 {
+ if video {
+ return maxVideoSize
}
+ return maxImageSize
+ }()
+ thread, _ := strconv.Atoi(c)
+ if global.PathExists(cacheFile) && cache == "1" {
+ goto hasCacheFile
}
- b, err := global.GetBytes(f)
- if err != nil {
+ if global.PathExists(cacheFile) {
+ _ = os.Remove(cacheFile)
+ }
+ if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil {
return nil, err
}
- _ = ioutil.WriteFile(cacheFile, b, 0644)
- return message.NewImage(b), nil
- }
- if strings.HasPrefix(f, "base64") {
- b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", ""))
- if err != nil {
- return nil, err
+ hasCacheFile:
+ if video {
+ return &LocalVideoElement{File: cacheFile}, nil
}
- return message.NewImage(b), nil
+ return &LocalImageElement{File: cacheFile}, nil
}
if strings.HasPrefix(f, "file") {
fu, err := url.Parse(f)
@@ -540,33 +923,81 @@ func (bot *CQBot) makeImageElem(t string, d map[string]string, group bool) (mess
if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` {
fu.Path = fu.Path[1:]
}
- b, err := ioutil.ReadFile(fu.Path)
+ info, err := os.Stat(fu.Path)
+ if err != nil {
+ if !os.IsExist(err) {
+ return nil, errors.New("file not found")
+ }
+ return nil, err
+ }
+ if video {
+ if info.Size() == 0 || info.Size() >= maxVideoSize {
+ return nil, errors.New("invalid video size")
+ }
+ return &LocalVideoElement{File: fu.Path}, nil
+ }
+ if info.Size() == 0 || info.Size() >= maxImageSize {
+ return nil, errors.New("invalid image size")
+ }
+ return &LocalImageElement{File: fu.Path}, nil
+ }
+ rawPath := path.Join(global.ImagePath, f)
+ if video {
+ rawPath = path.Join(global.VideoPath, f)
+ if !global.PathExists(rawPath) {
+ return nil, errors.New("invalid video")
+ }
+ if path.Ext(rawPath) == ".video" {
+ b, _ := ioutil.ReadFile(rawPath)
+ r := binary.NewReader(b)
+ return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效
+ Md5: r.ReadBytes(16),
+ ThumbMd5: r.ReadBytes(16),
+ Size: r.ReadInt32(),
+ ThumbSize: r.ReadInt32(),
+ Name: r.ReadString(),
+ Uuid: r.ReadAvailable(),
+ }}, nil
+ } else {
+ return &LocalVideoElement{File: rawPath}, nil
+ }
+ }
+ if strings.HasPrefix(f, "base64") {
+ b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", ""))
if err != nil {
return nil, err
}
- return message.NewImage(b), nil
+ return &LocalImageElement{Stream: bytes.NewReader(b)}, nil
+ }
+ if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) {
+ rawPath = path.Join(global.ImagePathOld, f)
}
- rawPath := path.Join(global.IMAGE_PATH, f)
if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") {
rawPath += ".cqimg"
}
if !global.PathExists(rawPath) && d["url"] != "" {
- return bot.ToElement(t, map[string]string{"file": d["url"]}, group)
+ return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group)
}
if global.PathExists(rawPath) {
- b, err := ioutil.ReadFile(rawPath)
+ file, err := os.Open(rawPath)
if err != nil {
return nil, err
}
if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" {
- return message.NewImage(b), nil
+ return &LocalImageElement{Stream: file}, nil
+ }
+ b, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
}
if len(b) < 20 {
return nil, errors.New("invalid local file")
}
- var size int32
- var hash []byte
- var url string
+ var (
+ size int32
+ hash []byte
+ url string
+ )
if path.Ext(rawPath) == ".cqimg" {
for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") {
kv := strings.SplitN(line, "=", 2)
@@ -587,27 +1018,23 @@ func (bot *CQBot) makeImageElem(t string, d map[string]string, group bool) (mess
}
if size == 0 {
if url != "" {
- return bot.ToElement(t, map[string]string{"file": url}, group)
+ return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group)
}
return nil, errors.New("img size is 0")
}
if len(hash) != 16 {
return nil, errors.New("invalid hash")
}
+ var rsp message.IMessageElement
if group {
- rsp, err := bot.Client.QueryGroupImage(1, hash, size)
- if err != nil {
- if url != "" {
- return bot.ToElement(t, map[string]string{"file": url}, group)
- }
- return nil, err
- }
- return rsp, nil
+ rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size)
+ goto ok
}
- rsp, err := bot.Client.QueryFriendImage(1, hash, size)
+ rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size)
+ ok:
if err != nil {
if url != "" {
- return bot.ToElement(t, map[string]string{"file": url}, group)
+ return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group)
}
return nil, err
}
@@ -616,38 +1043,43 @@ func (bot *CQBot) makeImageElem(t string, d map[string]string, group bool) (mess
return nil, errors.New("invalid image")
}
-//SendNewPic 一种xml 方式发送的群消息图片
-func (bot *CQBot) SendNewPic(elem message.IMessageElement, source string, icon string, minwidth int64, minheigt int64, maxwidth int64, maxheight int64, group bool) (*message.ServiceElement, error) {
- var xml string
- xml = ""
- if i, ok := elem.(*message.ImageElement); ok {
- if group == false {
- gm, err := bot.Client.UploadPrivateImage(1, i.Data)
+//makeShowPic 一种xml 方式发送的群消息图片
+func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) {
+ xml := ""
+ var suf message.IMessageElement
+ if i, ok := elem.(*LocalImageElement); ok {
+ if !group {
+ gm, err := bot.UploadLocalImageAsPrivate(1, i)
if err != nil {
log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err)
return nil, err
}
- xml = fmt.Sprintf(` `, "", gm.Md5, gm.Md5, len(i.Data), "", minwidth, minheigt, maxwidth, maxheight, source, icon)
-
+ suf = gm
+ xml = fmt.Sprintf(` `, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
} else {
- gm, err := bot.Client.UploadGroupImage(1, i.Data)
+ gm, err := bot.UploadLocalImageAsGroup(1, i)
if err != nil {
log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err)
return nil, err
}
- xml = fmt.Sprintf(` `, "", gm.Md5, gm.Md5, len(i.Data), "", minwidth, minheigt, maxwidth, maxheight, source, icon)
+ suf = gm
+ xml = fmt.Sprintf(` `, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
}
}
+
if i, ok := elem.(*message.GroupImageElement); ok {
- xml = fmt.Sprintf(` `, "", i.Md5, i.Md5, 0, "", minwidth, minheigt, maxwidth, maxheight, source, icon)
+ xml = fmt.Sprintf(` `, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
+ suf = i
}
if i, ok := elem.(*message.FriendImageElement); ok {
- xml = fmt.Sprintf(` `, "", i.Md5, i.Md5, 0, "", minwidth, minheigt, maxwidth, maxheight, source, icon)
+ xml = fmt.Sprintf(` `, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
+ suf = i
}
if xml != "" {
- log.Warn(xml)
- XmlMsg := message.NewRichXml(xml, 5)
- return XmlMsg, nil
+ //log.Warn(xml)
+ ret := []message.IMessageElement{suf}
+ ret = append(ret, message.NewRichXml(xml, 5))
+ return ret, nil
}
- return nil, errors.New("发送xml图片消息失败")
+ return nil, errors.New("生成xml图片消息失败")
}
diff --git a/coolq/event.go b/coolq/event.go
index e91a0ed..9299751 100644
--- a/coolq/event.go
+++ b/coolq/event.go
@@ -2,47 +2,54 @@ package coolq
import (
"encoding/hex"
- "github.com/Mrs4s/MiraiGo/binary"
- "github.com/Mrs4s/MiraiGo/client"
- "github.com/Mrs4s/MiraiGo/message"
- "github.com/Mrs4s/go-cqhttp/global"
- log "github.com/sirupsen/logrus"
"io/ioutil"
"path"
"strconv"
"strings"
"time"
+
+ "github.com/Mrs4s/MiraiGo/binary"
+ "github.com/Mrs4s/MiraiGo/client"
+ "github.com/Mrs4s/MiraiGo/message"
+ "github.com/Mrs4s/go-cqhttp/global"
+ log "github.com/sirupsen/logrus"
)
var format = "string"
+//SetMessageFormat 设置消息上报格式,默认为string
func SetMessageFormat(f string) {
format = f
}
-func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r interface{}) {
+//ToFormattedMessage 将给定[]message.IMessageElement转换为通过coolq.SetMessageFormat所定义的消息上报格式
+func ToFormattedMessage(e []message.IMessageElement, id int64, raw ...bool) (r interface{}) {
if format == "string" {
- r = ToStringMessage(e, code, raw...)
+ r = ToStringMessage(e, id, raw...)
} else if format == "array" {
- r = ToArrayMessage(e, code, raw...)
+ r = ToArrayMessage(e, id, raw...)
}
return
}
func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) {
bot.checkMedia(m.Elements)
- cqm := ToStringMessage(m.Elements, 0, true)
+ cqm := ToStringMessage(m.Elements, m.Sender.Uin, true)
if !m.Sender.IsFriend {
bot.oneWayMsgCache.Store(m.Sender.Uin, "")
}
- log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm)
+ id := m.Id
+ if bot.db != nil {
+ id = bot.InsertPrivateMessage(m)
+ }
+ log.Infof("收到好友 %v(%v) 的消息: %v (%v)", m.Sender.DisplayName(), m.Sender.Uin, cqm, id)
fm := MSG{
"post_type": "message",
"message_type": "private",
"sub_type": "friend",
- "message_id": ToGlobalId(m.Sender.Uin, m.Id),
+ "message_id": id,
"user_id": m.Sender.Uin,
- "message": ToFormattedMessage(m.Elements, 0, false),
+ "message": ToFormattedMessage(m.Elements, m.Sender.Uin, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
@@ -86,52 +93,8 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
id = bot.InsertGroupMessage(m)
}
log.Infof("收到群 %v(%v) 内 %v(%v) 的消息: %v (%v)", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm, id)
- gm := MSG{
- "anonymous": nil,
- "font": 0,
- "group_id": m.GroupCode,
- "message": ToFormattedMessage(m.Elements, m.GroupCode, false),
- "message_id": id,
- "message_type": "group",
- "post_type": "message",
- "raw_message": cqm,
- "self_id": c.Uin,
- "sender": MSG{
- "age": 0,
- "area": "",
- "level": "",
- "sex": "unknown",
- "user_id": m.Sender.Uin,
- },
- "sub_type": "normal",
- "time": time.Now().Unix(),
- "user_id": m.Sender.Uin,
- }
- if m.Sender.IsAnonymous() {
- gm["anonymous"] = MSG{
- "flag": "",
- "id": 0,
- "name": m.Sender.Nickname,
- }
- gm["sender"].(MSG)["nickname"] = "匿名消息"
- gm["sub_type"] = "anonymous"
- } else {
- mem := c.FindGroup(m.GroupCode).FindMember(m.Sender.Uin)
- ms := gm["sender"].(MSG)
- ms["role"] = func() string {
- switch mem.Permission {
- case client.Owner:
- return "owner"
- case client.Administrator:
- return "admin"
- default:
- return "member"
- }
- }()
- ms["nickname"] = mem.Nickname
- ms["card"] = mem.CardName
- ms["title"] = mem.SpecialTitle
- }
+ gm := bot.formatGroupMessage(m)
+ gm["message_id"] = id
bot.dispatchEventMessage(gm)
}
@@ -163,13 +126,24 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent) {
g := c.FindGroup(e.GroupCode)
- if e.Time > 0 {
- log.Infof("群 %v 内 %v 被 %v 禁言了 %v秒.",
- formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin)), e.Time)
+ if e.TargetUin == 0 {
+ if e.Time != 0 {
+ log.Infof("群 %v 被 %v 开启全员禁言.",
+ formatGroupName(g), formatMemberName(g.FindMember(e.OperatorUin)))
+ } else {
+ log.Infof("群 %v 被 %v 解除全员禁言.",
+ formatGroupName(g), formatMemberName(g.FindMember(e.OperatorUin)))
+ }
} else {
- log.Infof("群 %v 内 %v 被 %v 解除禁言.",
- formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin)))
+ if e.Time > 0 {
+ log.Infof("群 %v 内 %v 被 %v 禁言了 %v 秒.",
+ formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin)), e.Time)
+ } else {
+ log.Infof("群 %v 内 %v 被 %v 解除禁言.",
+ formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin)))
+ }
}
+
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"duration": e.Time,
@@ -180,10 +154,10 @@ func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent)
"user_id": e.TargetUin,
"time": time.Now().Unix(),
"sub_type": func() string {
- if e.Time > 0 {
- return "ban"
+ if e.Time == 0 {
+ return "lift_ban"
}
- return "lift_ban"
+ return "ban"
}(),
})
}
@@ -205,20 +179,121 @@ func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMessageRec
})
}
+func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
+ group := c.FindGroup(e.From())
+ switch notify := e.(type) {
+ case *client.GroupPokeNotifyEvent:
+ sender := group.FindMember(notify.Sender)
+ receiver := group.FindMember(notify.Receiver)
+ log.Infof("群 %v 内 %v 戳了戳 %v", formatGroupName(group), formatMemberName(sender), formatMemberName(receiver))
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "group_id": group.Code,
+ "notice_type": "notify",
+ "sub_type": "poke",
+ "self_id": c.Uin,
+ "user_id": notify.Sender,
+ "sender_id": notify.Sender,
+ "target_id": notify.Receiver,
+ "time": time.Now().Unix(),
+ })
+ case *client.GroupRedBagLuckyKingNotifyEvent:
+ sender := group.FindMember(notify.Sender)
+ luckyKing := group.FindMember(notify.LuckyKing)
+ log.Infof("群 %v 内 %v 的红包被抢完, %v 是运气王", formatGroupName(group), formatMemberName(sender), formatMemberName(luckyKing))
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "group_id": group.Code,
+ "notice_type": "notify",
+ "sub_type": "lucky_king",
+ "self_id": c.Uin,
+ "user_id": notify.Sender,
+ "sender_id": notify.Sender,
+ "target_id": notify.LuckyKing,
+ "time": time.Now().Unix(),
+ })
+ case *client.MemberHonorChangedNotifyEvent:
+ log.Info(notify.Content())
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "group_id": group.Code,
+ "notice_type": "notify",
+ "sub_type": "honor",
+ "self_id": c.Uin,
+ "user_id": notify.Uin,
+ "time": time.Now().Unix(),
+ "honor_type": func() string {
+ switch notify.Honor {
+ case client.Talkative:
+ return "talkative"
+ case client.Performer:
+ return "performer"
+ case client.Emotion:
+ return "emotion"
+ default:
+ return "ERROR"
+ }
+ }(),
+ })
+ }
+}
+
+func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
+ friend := c.FindFriend(e.From())
+ switch notify := e.(type) {
+ case *client.FriendPokeNotifyEvent:
+ log.Infof("好友 %v 戳了戳你.", friend.Nickname)
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "notice_type": "notify",
+ "sub_type": "poke",
+ "self_id": c.Uin,
+ "user_id": notify.Sender,
+ "sender_id": notify.Sender,
+ "target_id": notify.Receiver,
+ "time": time.Now().Unix(),
+ })
+ }
+}
+
func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageRecalledEvent) {
f := c.FindFriend(e.FriendUin)
gid := ToGlobalId(e.FriendUin, e.MessageId)
- log.Infof("好友 %v(%v) 撤回了消息: %v", f.Nickname, f.Uin, gid)
+ if f != nil {
+ log.Infof("好友 %v(%v) 撤回了消息: %v", f.Nickname, f.Uin, gid)
+ } else {
+ log.Infof("好友 %v 撤回了消息: %v", e.FriendUin, gid)
+ }
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "friend_recall",
"self_id": c.Uin,
- "user_id": f.Uin,
+ "user_id": e.FriendUin,
"time": e.Time,
"message_id": gid,
})
}
+func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.OfflineFileEvent) {
+ f := c.FindFriend(e.Sender)
+ if f == nil {
+ return
+ }
+ log.Infof("好友 %v(%v) 发送了离线文件 %v", f.Nickname, f.Uin, e.FileName)
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "notice_type": "offline_file",
+ "user_id": e.Sender,
+ "file": MSG{
+ "name": e.FileName,
+ "size": e.FileSize,
+ "url": e.DownloadUrl,
+ },
+ "self_id": c.Uin,
+ "time": time.Now().Unix(),
+ })
+}
+
func (bot *CQBot) joinGroupEvent(c *client.QQClient, group *client.GroupInfo) {
log.Infof("Bot进入了群 %v.", formatGroupName(group))
bot.dispatchEventMessage(bot.groupIncrease(group.Code, 0, c.Uin))
@@ -251,6 +326,20 @@ func (bot *CQBot) memberPermissionChangedEvent(c *client.QQClient, e *client.Mem
})
}
+func (bot *CQBot) memberCardUpdatedEvent(c *client.QQClient, e *client.MemberCardUpdatedEvent) {
+ log.Infof("群 %v 的 %v 更新了名片 %v -> %v", formatGroupName(e.Group), formatMemberName(e.Member), e.OldCard, e.Member.CardName)
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "notice_type": "group_card",
+ "group_id": e.Group.Code,
+ "user_id": e.Member.Uin,
+ "card_new": e.Member.CardName,
+ "card_old": e.OldCard,
+ "time": time.Now().Unix(),
+ "self_id": c.Uin,
+ })
+}
+
func (bot *CQBot) memberJoinEvent(c *client.QQClient, e *client.MemberJoinGroupEvent) {
log.Infof("新成员 %v 进入了群 %v.", formatMemberName(e.Member), formatGroupName(e.Group))
bot.dispatchEventMessage(bot.groupIncrease(e.Group.Code, 0, e.Member.Uin))
@@ -295,7 +384,6 @@ func (bot *CQBot) friendAddedEvent(c *client.QQClient, e *client.NewFriendEvent)
func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) {
log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin)
flag := strconv.FormatInt(e.RequestId, 10)
- bot.invitedReqCache.Store(flag, e)
bot.dispatchEventMessage(MSG{
"post_type": "request",
"request_type": "group",
@@ -312,7 +400,6 @@ func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRe
func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) {
log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupCode, e.RequesterNick, e.RequesterUin)
flag := strconv.FormatInt(e.RequestId, 10)
- bot.joinReqCache.Store(flag, e)
bot.dispatchEventMessage(MSG{
"post_type": "request",
"request_type": "group",
@@ -326,6 +413,67 @@ func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupR
})
}
+func (bot *CQBot) otherClientStatusChangedEvent(c *client.QQClient, e *client.OtherClientStatusChangedEvent) {
+ if e.Online {
+ log.Infof("Bot 账号在客户端 %v (%v) 登录.", e.Client.DeviceName, e.Client.DeviceKind)
+ } else {
+ log.Infof("Bot 账号在客户端 %v (%v) 登出.", e.Client.DeviceName, e.Client.DeviceKind)
+ }
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "notice_type": "client_status",
+ "online": e.Online,
+ "client": MSG{
+ "app_id": e.Client.AppId,
+ "device_name": e.Client.DeviceName,
+ "device_kind": e.Client.DeviceKind,
+ },
+ "self_id": c.Uin,
+ "time": time.Now().Unix(),
+ })
+}
+
+func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent) {
+ g := c.FindGroup(e.GroupCode)
+ gid := ToGlobalId(e.GroupCode, e.MessageID)
+ if e.OperationType == 1 {
+ log.Infof(
+ "群 %v 内 %v 将 %v 的消息(%v)设为了精华消息.",
+ formatGroupName(g),
+ formatMemberName(g.FindMember(e.OperatorUin)),
+ formatMemberName(g.FindMember(e.SenderUin)),
+ gid,
+ )
+ } else {
+ log.Infof(
+ "群 %v 内 %v 将 %v 的消息(%v)移出了精华消息.",
+ formatGroupName(g),
+ formatMemberName(g.FindMember(e.OperatorUin)),
+ formatMemberName(g.FindMember(e.SenderUin)),
+ gid,
+ )
+ }
+ if e.OperatorUin == bot.Client.Uin {
+ return
+ }
+ bot.dispatchEventMessage(MSG{
+ "post_type": "notice",
+ "group_id": e.GroupCode,
+ "notice_type": "essence",
+ "sub_type": func() string {
+ if e.OperationType == 1 {
+ return "add"
+ }
+ return "delete"
+ }(),
+ "self_id": c.Uin,
+ "sender_id": e.SenderUin,
+ "operator_id": e.OperatorUin,
+ "time": time.Now().Unix(),
+ "message_id": gid,
+ })
+}
+
func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) MSG {
return MSG{
"post_type": "notice",
@@ -370,8 +518,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
switch i := elem.(type) {
case *message.ImageElement:
filename := hex.EncodeToString(i.Md5) + ".image"
- if !global.PathExists(path.Join(global.IMAGE_PATH, filename)) {
- _ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
+ if !global.PathExists(path.Join(global.ImagePath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
@@ -379,23 +527,67 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
}), 0644)
}
i.Filename = filename
+ case *message.GroupImageElement:
+ filename := hex.EncodeToString(i.Md5) + ".image"
+ if !global.PathExists(path.Join(global.ImagePath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
+ w.Write(i.Md5)
+ w.WriteUInt32(uint32(i.Size))
+ w.WriteString(filename)
+ w.WriteString(i.Url)
+ }), 0644)
+ }
+ case *message.FriendImageElement:
+ filename := hex.EncodeToString(i.Md5) + ".image"
+ if !global.PathExists(path.Join(global.ImagePath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
+ w.Write(i.Md5)
+ w.WriteUInt32(uint32(0)) // 发送时会调用url, 大概没事
+ w.WriteString(filename)
+ w.WriteString(i.Url)
+ }), 0644)
+ }
+ case *message.GroupFlashImgElement:
+ filename := hex.EncodeToString(i.Md5) + ".image"
+ if !global.PathExists(path.Join(global.ImagePath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
+ w.Write(i.Md5)
+ w.WriteUInt32(uint32(i.Size))
+ w.WriteString(i.Filename)
+ w.WriteString("")
+ }), 0644)
+ }
+ i.Filename = filename
+ case *message.FriendFlashImgElement:
+ filename := hex.EncodeToString(i.Md5) + ".image"
+ if !global.PathExists(path.Join(global.ImagePath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
+ w.Write(i.Md5)
+ w.WriteUInt32(uint32(i.Size))
+ w.WriteString(i.Filename)
+ w.WriteString("")
+ }), 0644)
+ }
+ i.Filename = filename
case *message.VoiceElement:
i.Name = strings.ReplaceAll(i.Name, "{", "")
i.Name = strings.ReplaceAll(i.Name, "}", "")
- if !global.PathExists(path.Join(global.VOICE_PATH, i.Name)) {
+ if !global.PathExists(path.Join(global.VoicePath, i.Name)) {
b, err := global.GetBytes(i.Url)
if err != nil {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue
}
- _ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0644)
+ _ = ioutil.WriteFile(path.Join(global.VoicePath, i.Name), b, 0644)
}
case *message.ShortVideoElement:
filename := hex.EncodeToString(i.Md5) + ".video"
- if !global.PathExists(path.Join(global.VIDEO_PATH, filename)) {
- _ = ioutil.WriteFile(path.Join(global.VIDEO_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
+ if !global.PathExists(path.Join(global.VideoPath, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.VideoPath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
+ w.Write(i.ThumbMd5)
w.WriteUInt32(uint32(i.Size))
+ w.WriteUInt32(uint32(i.ThumbSize))
w.WriteString(i.Name)
w.Write(i.Uuid)
}), 0644)
diff --git a/docs/EventFilter.md b/docs/EventFilter.md
index c9ccf05..7f23406 100644
--- a/docs/EventFilter.md
+++ b/docs/EventFilter.md
@@ -1,12 +1,22 @@
# 事件过滤器
-在go-cqhttp同级目录下新建`filter.json`文件即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则会暂停所有上报。
+在go-cqhttp同级目录下新建`filter.json`文件即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则不会启用事件过滤器。
事件过滤器会处理所有事件(包括心跳事件在内的元事件),请谨慎使用!!
+注意: 与客户端建立连接的握手事件**不会**经过事件过滤器
+
## 示例
这节首先给出一些示例,演示过滤器的基本用法,下一节将给出具体语法说明。
+### 过滤所有事件
+
+```json
+{
+ ".not": {}
+}
+```
+
### 只上报以「!!」开头的消息
```json
@@ -110,16 +120,16 @@
下面列出所有运算符(「要求的参数类型」是指运算符的键所对应的值的类型,「可作用于的类型」是指在过滤时事件对象相应值的类型):
-| 运算符 | 要求的参数类型 | 可作用于的类型 |
-| ----- | ------------ | ----------- |
-| `.not` | object | 任何 |
-| `.and` | object | 若参数中全为运算符,则任何;若不全为运算符,则 object |
-| `.or` | array(数组元素为 object) | 任何 |
-| `.eq` | 任何 | 任何 |
-| `.neq` | 任何 | 任何 |
-| `.in` | string/array | 若参数为 string,则 string;若参数为 array,则任何 |
-| `.contains` | string | string |
-| `.regex` | string | string |
+| 运算符 | 要求的参数类型 | 可作用于的类型 |
+| ----------- | -------------------------- | ----------------------------------------------------- |
+| `.not` | object | 任何 |
+| `.and` | object | 若参数中全为运算符,则任何;若不全为运算符,则 object |
+| `.or` | array(数组元素为 object) | 任何 |
+| `.eq` | 任何 | 任何 |
+| `.neq` | 任何 | 任何 |
+| `.in` | string/array | 若参数为 string,则 string;若参数为 array,则任何 |
+| `.contains` | string | string |
+| `.regex` | string | string |
## 过滤时的事件数据对象
@@ -128,5 +138,5 @@
这里有几点需要注意:
-- `message` 字段在运行过滤器时是消息段数组的形式(见 [消息格式]( https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md ))
+- `message` 字段在运行过滤器时和上报信息类型相同(见 [消息格式]( https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md ))
- `raw_message` 字段为未经**CQ码**处理的原始消息字符串,这意味着其中可能会出现形如 `[CQ:face,id=123]` 的 CQ 码
diff --git a/docs/QA.md b/docs/QA.md
new file mode 100644
index 0000000..28f455c
--- /dev/null
+++ b/docs/QA.md
@@ -0,0 +1,5 @@
+# 常见问题
+
+### Q: 为什么挂一段时间后就会出现 `消息发送失败,账号可能被风控`?
+
+### A: 如果你刚开始使用 go-cqhttp 建议挂机3-7天,即可解除风控
diff --git a/docs/adminApi.md b/docs/adminApi.md
new file mode 100644
index 0000000..a455dcd
--- /dev/null
+++ b/docs/adminApi.md
@@ -0,0 +1,251 @@
+# 管理 API
+
+> 支持跨域
+
+## 公共参数
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------------ | ------ | --------------------------- |
+| access_token | string | 校验口令,config.hjson中配置 |
+
+
+
+## admin/do_restart
+
+### 热重启
+
+> 热重启
+
+> ps: 目前不支持ws部分的修改生效
+
+method:`POST/GET`
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ---- | ---- |
+| 无 | | |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+
+### admin/get_web_write
+
+> 拉取验证码/设备锁
+
+method: `GET`
+
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ---- | ---- |
+| 无 | | |
+
+返回:
+
+```json
+{"data": {"ispic": true,"picbase64":"xxxxx"}, "retcode": 0, "status": "ok"}
+```
+| 参数名 | 类型 | 说明 |
+| -------- | ------ | --------------------------------------------------- |
+| ispic | bool | 是否是验证码类型 true是,false为不是(比如设备锁 |
+| picbas64 | string | 验证码的base64编码内容,加上头,放入img标签即可显示 |
+
+### admin/do_web_write
+
+> web输入验证码/设备锁确认
+
+method: `POST` formdata
+
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | ---------- |
+| input | string | 输入的内容 |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+
+### admin/do_restart_docker
+
+> 冷重启
+
+> 注意:此api 会直接结束掉进程,需要依赖docker/supervisor等进程管理工具来自动拉起
+
+method: `POST`
+
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ---- | ---- |
+| 无 | | |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+### admin/do_process_restart
+
+> 冷重启
+
+method: `POST`
+
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ---- | ---- |
+| 无 | | |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+### admin/do_config_base
+
+> 基础配置
+
+method: `POST` formdata
+
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------------ | ------ | ------------------------------------- |
+| uin | string | qq号 |
+| password | string | qq密码 |
+| enable_db | string | 是否启动数据库,填 'true' 或者 'false' |
+| access_token | string | 授权 token |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+
+### admin/do_config_http
+
+> http服务配置
+
+method: `POST` formdata
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ----------- | ------ | --------------------------------------------- |
+| port | string | 服务端口 |
+| host | string | 服务监听地址 |
+| enable | string | 是否启用 ,填 'true' 或者 'false' |
+| timeout | string | http请求超时时间 |
+| post_url | string | post上报地址 不需要就填空字符串,或者不填 |
+| post_secret | string | post上报的secret 不需要就填空字符串,或者不填 |
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+
+### admin/do_config_ws
+
+> 正向ws设置
+
+method: `POST` formdata
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | -------------------------------- |
+| port | string | 服务端口 |
+| host | string | 服务监听地址 |
+| enable | string | 是否启用 ,填 'true' 或者 'false' |
+
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+### admin/do_config_reverse
+
+> 反向ws配置
+
+method: `POST` formdata
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | -------------------------------- |
+| port | string | 服务端口 |
+| host | string | 服务监听地址 |
+| enable | string | 是否启用 ,填 'true' 或者 'false' |
+
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+### admin/do_config_json
+
+> 直接修改 config.hjson配置
+
+method: `POST` formdata
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | ----------------------------------- |
+| json | string | 完整的config.hjson的配合,json字符串 |
+
+
+返回:
+
+```json
+{"data": {}, "retcode": 0, "status": "ok"}
+```
+
+### admin/get_config_json
+
+> 获取当前 config.hjson配置
+
+method: `GET`
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ---- | ---- |
+| 无 | | |
+
+
+返回:
+
+```json
+{"data": {"config":"xxxx"}, "retcode": 0, "status": "ok"}
+```
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | ----------------------------------- |
+| config | string | 完整的config.hjson的配合,json字符串 |
+
diff --git a/docs/config.md b/docs/config.md
index ccdff7c..495f245 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,6 +1,6 @@
# 配置
-go-cqhttp 包含 `config.json` 和 `device.json` 两个配置文件, 其中 `config.json` 为运行配置 `device.json` 为虚拟设备信息.
+go-cqhttp 包含 `config.hjson` 和 `device.json` 两个配置文件, 其中 `config.json` 为运行配置 `device.json` 为虚拟设备信息.
## 从原CQHTTP导入配置
@@ -36,6 +36,7 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 5,
+ "use_sso_address": false,
"http_config": {
"enabled": true,
"host": "0.0.0.0",
@@ -60,28 +61,30 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
}
````
-| 字段 | 类型 | 说明 |
-| ------------------ | -------- | ------------------------------------------------------------------- |
-| uin | int64 | 登录用QQ号 |
-| password | string | 登录用密码 |
-| encrypt_password | bool | 是否对密码进行加密. |
-| password_encrypted | string | 加密后的密码(请勿修改) |
-| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
-| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
-| relogin | bool | 是否自动重新登录 |
-| relogin_delay | int | 重登录延时(秒) |
-| max_relogin_times | uint | 最大重登录次数,若0则不设置上限 |
-| _rate_limit | bool | 是否启用API调用限速 |
-| frequency | float64 | 1s内能调用API的次数 |
-| bucket_size | int | 令牌桶的大小,默认为1,修改此值可允许一定程度内连续调用api |
-| post_message_format | string | 上报信息类型 |
-| ignore_invalid_cqcode| bool | 是否忽略错误的CQ码 |
-| force_fragmented | bool | 是否强制分片发送群长消息 |
-| heartbeat_interval | int64 | 心跳间隔时间,单位秒,若0则关闭心跳 |
-| http_config | object | HTTP API配置 |
-| ws_config | object | Websocket API 配置 |
-| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
-| log_level | string | 指定日志收集级别,将收集的日志单独存放到固定文件中,便于查看日志线索 当前支持 warn,error|
+| 字段 | 类型 | 说明 |
+| --------------------- | -------- | ---------------------------------------------------------------------------------------- |
+| uin | int64 | 登录用QQ号 |
+| password | string | 登录用密码 |
+| encrypt_password | bool | 是否对密码进行加密. |
+| password_encrypted | string | 加密后的密码(请勿修改) |
+| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
+| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
+| relogin | bool | 是否自动重新登录 |
+| relogin_delay | int | 重登录延时(秒) |
+| max_relogin_times | uint | 最大重登录次数,若0则不设置上限 |
+| _rate_limit | bool | 是否启用API调用限速 |
+| frequency | float64 | 1s内能调用API的次数 |
+| bucket_size | int | 令牌桶的大小,默认为1,修改此值可允许一定程度内连续调用api |
+| post_message_format | string | 上报信息类型 |
+| ignore_invalid_cqcode | bool | 是否忽略错误的CQ码 |
+| force_fragmented | bool | 是否强制分片发送群长消息 |
+| fix_url | bool | 是否对链接的发送进行预处理, 可缓解链接信息被风控导致无法发送的情况, 但可能影响客户端着色(不影响内容)|
+| use_sso_address | bool | 是否使用服务器下发的地址 |
+| heartbeat_interval | int64 | 心跳间隔时间,单位秒。小于0则关闭心跳,等于0使用默认值(5秒) |
+| http_config | object | HTTP API配置 |
+| ws_config | object | Websocket API 配置 |
+| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
+| log_level | string | 指定日志收集级别,将收集的日志单独存放到固定文件中,便于查看日志线索 当前支持 warn,error |
> 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
@@ -90,3 +93,41 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
> 注3:关闭心跳服务可能引起断线,请谨慎关闭
+
+## 设备信息
+
+默认生成的设备信息如下所示:
+
+``` json
+{
+ "protocol": 0,
+ "display": "xxx",
+ "finger_print": "xxx",
+ "boot_id": "xxx",
+ "proc_version": "xxx",
+ "imei": "xxx"
+}
+```
+
+在大部分情况下 我们只需要关心 `protocol` 字段:
+
+| 值 | 类型 | 限制 |
+| --- | ------------- | ---------------------------------------------------------------- |
+| 0 | iPad | 无 |
+| 1 | Android Phone | 无 |
+| 2 | Android Watch | 无法接收 `notify` 事件、无法接收口令红包、无法接收撤回消息 |
+| 3 | MacOS | 无 |
+
+> 注意, 根据协议的不同, 各类消息有所限制
+
+## 自定义服务器IP
+
+> 某些海外服务器使用默认地址可能会存在链路问题,此功能可以指定 go-cqhttp 连接哪些地址以达到最优化.
+
+将文件 `address.txt` 创建到 `go-cqhttp` 工作目录, 并键入 `IP:PORT` 以换行符为分割即可.
+
+示例:
+````
+1.1.1.1:53
+1.1.2.2:8899
+````
\ No newline at end of file
diff --git a/docs/cqhttp.md b/docs/cqhttp.md
index 6c13a80..53cc24e 100644
--- a/docs/cqhttp.md
+++ b/docs/cqhttp.md
@@ -2,21 +2,235 @@
由于部分 api 原版 CQHTTP 并未实现,go-cqhttp 修改并增加了一些拓展 api .
+
+目录
+
+
+##### CQCode
+- [图片](#图片)
+- [回复](#回复)
+- [红包](#红包)
+- [戳一戳](#戳一戳)
+- [礼物](#礼物)
+- [合并转发](#合并转发)
+- [合并转发消息节点](#合并转发消息节点)
+- [XML 消息](#xml-消息)
+- [JSON 消息](#json-消息)
+- [cardimage](#cardimage)
+- [文本转语音](#文本转语音)
+- [图片](#图片)
+
+##### API
+- [设置群名](#设置群名)
+- [设置群头像](#设置群头像)
+- [获取图片信息](#获取图片信息)
+- [获取消息](#获取消息)
+- [获取合并转发内容](#获取合并转发内容)
+- [发送合并转发(群)](#发送合并转发(群))
+- [获取中文分词](#获取中文分词)
+- [图片OCR](#图片OCR)
+- [获取中文分词](#获取中文分词)
+- [获取群系统消息](#获取群文件系统信息)
+- [获取群文件系统信息](#获取群文件系统信息)
+- [获取群根目录文件列表](#获取群根目录文件列表)
+- [获取群子目录文件列表](#获取群子目录文件列表)
+- [获取群文件资源链接](#获取群文件资源链接)
+- [获取状态](#获取状态)
+- [获取群子目录文件列表](#设置群名)
+- [获取用户VIP信息](#获取用户VIP信息)
+- [发送群公告](#发送群公告)
+- [设置精华消息](#设置精华消息)
+- [移出精华消息](#移出精华消息)
+- [获取精华消息列表](#获取精华消息列表)
+- [重载事件过滤器](#重载事件过滤器)
+
+##### 事件
+- [群消息撤回](#群消息撤回)
+- [好友消息撤回](#好友消息撤回)
+- [好友戳一戳](#好友戳一戳)
+- [群内戳一戳](#群内戳一戳)
+- [群红包运气王提示](#群红包运气王提示)
+- [群成员荣誉变更提示](#群成员荣誉变更提示)
+- [群成员名片更新](#群成员名片更新)
+- [接收到离线文件](#接收到离线文件)
+- [群精华消息](#精华消息)
+
+
+
+
## CQCode
+### 图片
+
+Type : `image`
+
+范围: **发送/接收**
+
+参数:
+
+| 参数名 | 可能的值 | 说明 |
+| ------- | --------------- | --------------------------------------------------------------- |
+| `file` | - | 图片文件名 |
+| `type` | `flash`,`show` | 图片类型,`flash` 表示闪照,`show` 表示秀图,默认普通图片 |
+| `url` | - | 图片 URL |
+| `cache` | `0` `1` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `1` |
+| `id` | - | 发送秀图时的特效id,默认为40000 |
+| `c` | `2` `3` | 通过网络下载图片时的线程数, 默认单线程. (在资源不支持并发时会自动处理)|
+
+可用的特效ID:
+
+| id | 类型 |
+| ----- | ---- |
+| 40000 | 普通 |
+| 40001 | 幻影 |
+| 40002 | 抖动 |
+| 40003 | 生日 |
+| 40004 | 爱你 |
+| 40005 | 征友 |
+
+示例: `[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]`
+
+> 注意:图片总大小不能超过30MB,gif总帧数不能超过300帧
+
### 回复
Type : `reply`
范围: **发送/接收**
+> 注意: 如果id存在则优先处理id
+
参数:
| 参数名 | 类型 | 说明 |
| ------ | ---- | ------------------------------------- |
-| id | int | 回复时所引用的消息id, 必须为本群消息. |
+| `id` | int | 回复时所引用的消息id, 必须为本群消息. |
+| `text` | string | 自定义回复的信息 |
+| `qq` | int64 | 自定义回复时的自定义QQ, 如果使用自定义信息必须指定. |
+| `time` | int64 | 自定义回复时的时间, 格式为Unix时间 |
-示例: `[CQ:reply,id=123456]`
+
+
+示例: `[CQ:reply,id=123456]`
+\
+自定义回复示例: `[CQ:reply,text=Hello World,qq=10086,time=3376656000]`
+
+### 音乐分享
+
+```json
+{
+ "type": "music",
+ "data": {
+ "type": "163",
+ "id": "28949129"
+ }
+}
+```
+
+```
+[CQ:music,type=163,id=28949129]
+```
+
+| 参数名 | 收 | 发 | 可能的值 | 说明 |
+| --- | --- | --- | --- | --- |
+| `type` | | ✓ | `qq` `163` | 分别表示使用 QQ 音乐、网易云音乐 |
+| `id` | | ✓ | - | 歌曲 ID |
+
+### 音乐自定义分享
+
+```json
+{
+ "type": "music",
+ "data": {
+ "type": "custom",
+ "url": "http://baidu.com",
+ "audio": "http://baidu.com/1.mp3",
+ "title": "音乐标题"
+ }
+}
+```
+
+```
+[CQ:music,type=custom,url=http://baidu.com,audio=http://baidu.com/1.mp3,title=音乐标题]
+```
+
+| 参数名 | 收 | 发 | 可能的值 | 说明 |
+| --- | --- | --- | --- | --- |
+| `type` | | ✓ | `custom` | 表示音乐自定义分享 |
+| `subtype` | | ✓ | `qq,163,migu,kugou,kuwo` | 表示分享类型,不填写发送为xml卡片,推荐填写提高稳定性 |
+| `url` | | ✓ | - | 点击后跳转目标 URL |
+| `audio` | | ✓ | - | 音乐 URL |
+| `title` | | ✓ | - | 标题 |
+| `content` | | ✓ | - | 内容描述 |
+| `image` | | ✓ | - | 图片 URL |
+
+### 红包
+
+Type: `redbag`
+
+范围: **接收**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ----------- |
+| `title` | string | 祝福语/口令 |
+
+示例: `[CQ:redbag,title=恭喜发财]`
+
+### 戳一戳
+
+> 注意:发送戳一戳消息无法撤回,返回的 `message id` 恒定为 `0`
+
+Type: `poke`
+
+范围: **发送(仅群聊)**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ----- | ------------ |
+| `qq` | int64 | 需要戳的成员 |
+
+示例: `[CQ:poke,qq=123456]`
+
+### 礼物
+
+> 注意:仅支持免费礼物,发送群礼物消息无法撤回,返回的 `message id` 恒定为 `0`
+
+Type: `gift`
+
+范围: **发送(仅群聊,接收的时候不是CQ码)**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ----- | -------------- |
+| `qq` | int64 | 接收礼物的成员 |
+| `id` | int | 礼物的类型 |
+
+目前支持的礼物ID:
+
+| id | 类型 |
+| --- | ---------- |
+| 0 | 甜Wink |
+| 1 | 快乐肥宅水 |
+| 2 | 幸运手链 |
+| 3 | 卡布奇诺 |
+| 4 | 猫咪手表 |
+| 5 | 绒绒手套 |
+| 6 | 彩虹糖果 |
+| 7 | 坚强 |
+| 8 | 告白话筒 |
+| 9 | 牵你的手 |
+| 10 | 可爱猫咪 |
+| 11 | 神秘面具 |
+| 12 | 我超忙的 |
+| 13 | 爱心口罩 |
+
+
+
+示例: `[CQ:gift,qq=123456,id=8]`
### 合并转发
@@ -28,7 +242,7 @@ Type: `forward`
| 参数名 | 类型 | 说明 |
| ------ | ------ | ------------------------------------------------------------ |
-| id | string | 合并转发ID, 需要通过 `/get_forward_msg` API获取转发的具体内容 |
+| `id` | string | 合并转发ID, 需要通过 `/get_forward_msg` API获取转发的具体内容 |
示例: `[CQ:forward,id=xxxx]`
@@ -40,12 +254,12 @@ Type: `node`
参数:
-| 参数名 | 类型 | 说明 | 特殊说明 |
-| ------- | ------- | -------------- | ------------------------------------------------------------ |
-| id | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** |
-| name | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) |
-| uin | int64 | 发送者QQ号 | 用于自定义消息 |
-| content | message | 具体消息 | 用于自定义消息 **不支持转发套娃,不支持引用回复** |
+| 参数名 | 类型 | 说明 | 特殊说明 |
+| --------- | ------- | -------------- | ------------------------------------------------------------ |
+| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** |
+| `name` | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) |
+| `uin` | int64 | 发送者QQ号 | 用于自定义消息 |
+| `content` | message | 具体消息 | 用于自定义消息 |
特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送,并且由于消息段较为复杂,仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. 另外按 [CQHTTP](https://cqhttp.cc/docs/4.15/#/Message?id=格式) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃**
@@ -107,7 +321,8 @@ Type: `node`
"data": {
"name": "自定义发送者",
"uin": "10086",
- "content": "我是自定义消息"
+ "content": "我是自定义消息",
+ "time": "3376656000"
}
},
{
@@ -118,8 +333,22 @@ Type: `node`
}
]
````
+### 短视频消息
-### xml支持
+Type: `video`
+
+范围: **发送/接收**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ------------------------------------------------|
+| `file` | string | 支持http和file发送 |
+| `cover` | string | 视频封面,支持http,file和base64发送,格式必须为jpg |
+| `c` | `2` `3`| 通过网络下载视频时的线程数, 默认单线程. (在资源不支持并发时会自动处理)|
+示例: `[CQ:image,file=file:///C:\\Users\Richard\Pictures\1.mp4]`
+
+### XML 消息
Type: `xml`
@@ -127,16 +356,16 @@ Type: `xml`
参数:
-| 参数名 | 类型 | 说明 |
-| ------ | ------ | ------------------------------------------------------------ |
-| data | string | xml内容,xml中的value部分,记得实体化处理|
-| resid | int32 | 可以不填|
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ----------------------------------------- |
+| `data` | string | xml内容,xml中的value部分,记得实体化处理 |
+| `resid` | int32 | 可以不填 |
示例: `[CQ:xml,data=xxxx]`
-####一些xml样例
+#### 一些xml样例
-####ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中
+#### ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中
#### qq音乐
@@ -168,7 +397,7 @@ Type: `xml`
```
-### json消息支持
+### JSON 消息
Type: `json`
@@ -176,20 +405,20 @@ Type: `json`
参数:
-| 参数名 | 类型 | 说明 |
-| ------ | ------ | ------------------------------------------------------------ |
-| data | string | json内容,json的所有字符串记得实体化处理|
-| resid | int32 | 默认不填为0,走小程序通道,填了走富文本通道发送|
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ----------------------------------------------- |
+| `data` | string | json内容,json的所有字符串记得实体化处理 |
+| `resid` | int32 | 默认不填为0,走小程序通道,填了走富文本通道发送 |
json中的字符串需要进行转义:
->","=>`,`、
+>","=> `,`
->"&"=> `&`、
+>"&"=> `&`
->"["=>`[`、
+>"["=> `[`
->"]"=>`]`、
+>"]"=> `]`
否则无法正确得到解析
@@ -199,7 +428,8 @@ json中的字符串需要进行转义:
```
-### cardimage 一种xml的图片消息(装逼大图)
+### cardimage
+一种xml的图片消息(装逼大图)
ps: xml 接口的消息都存在风控风险,请自行兼容发送失败后的处理(可以失败后走普通图片模式)
@@ -209,15 +439,15 @@ Type: `cardimage`
参数:
-| 参数名 | 类型 | 说明 |
-| ------ | ------ | ------------------------------------------------------------ |
-| file | string | 和image的file字段对齐,支持也是一样的|
-| minwidth | int64 | 默认不填为400,最小width|
-| minheight | int64 | 默认不填为400,最小height|
-| maxwidth | int64 | 默认不填为500,最大width|
-| maxheight | int64 | 默认不填为1000,最大height|
-| source | string | 分享来源的名称,可以留空|
-| icon | string | 分享来源的icon图标url,可以留空|
+| 参数名 | 类型 | 说明 |
+| ----------- | ------ | ------------------------------------- |
+| `file` | string | 和image的file字段对齐,支持也是一样的 |
+| `minwidth` | int64 | 默认不填为400,最小width |
+| `minheight` | int64 | 默认不填为400,最小height |
+| `maxwidth` | int64 | 默认不填为500,最大width |
+| `maxheight` | int64 | 默认不填为1000,最大height |
+| `source` | string | 分享来源的名称,可以留空 |
+| `icon` | string | 分享来源的icon图标url,可以留空 |
示例cardimage 的cq码:
@@ -225,6 +455,22 @@ Type: `cardimage`
[CQ:cardimage,file=https://i.pixiv.cat/img-master/img/2020/03/25/00/00/08/80334602_p0_master1200.jpg]
```
+### 文本转语音
+
+> 注意:通过TX的TTS接口,采用的音源与登录账号的性别有关
+
+Type: `tts`
+
+范围: **发送(仅群聊)**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | ---- |
+| `text` | string | 内容 |
+
+示例: `[CQ:tts,text=这是一条测试消息]`
+
## API
### 设置群名
@@ -233,10 +479,32 @@ Type: `cardimage`
**参数**
-| 字段 | 类型 | 说明 |
-| -------- | ------ | ---- |
-| group_id | int64 | 群号 |
-| group_name | string | 新名 |
+| 字段 | 类型 | 说明 |
+| ------------ | ------ | ---- |
+| `group_id` | int64 | 群号 |
+| `group_name` | string | 新名 |
+
+### 设置群头像
+
+终结点: `/set_group_portrait`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------ |
+| `group_id` | int64 | 群号 |
+| `file` | string | 图片文件名 |
+| `cache` | int | 表示是否使用已缓存的文件 |
+
+[1]`file` 参数支持以下几种格式:
+
+- 绝对路径,例如 `file:///C:\\Users\Richard\Pictures\1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089)
+- 网络 URL,例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg`
+- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
+
+[2]`cache`参数: 通过网络 URL 发送时有效,`1`表示使用缓存,`0`关闭关闭缓存,默认 为`1`
+
+[3] 目前这个API在登录一段时间后因cookie失效而失效,请考虑后使用
### 获取图片信息
@@ -258,9 +526,9 @@ Type: `cardimage`
| `filename` | string | 图片文件原名 |
| `url` | string | 图片下载地址 |
-### 获取群消息
+### 获取消息
-终结点: `/get_group_msg`
+终结点: `/get_msg`
参数
@@ -276,7 +544,7 @@ Type: `cardimage`
| `real_id` | int32 | 消息真实id |
| `sender` | object | 发送者 |
| `time` | int32 | 发送时间 |
-| `content` | message | 消息内容 |
+| `message` | message | 消息内容 |
### 获取合并转发内容
@@ -334,11 +602,459 @@ Type: `cardimage`
| `group_id` | int64 | 群号 |
| `messages` | forward node[] | 自定义转发消息, 具体看CQCode |
-###
+响应数据
+
+| 字段 | 类型 | 说明 |
+| ------------ | ------ | ------ |
+| `message_id` | string | 消息id |
+
+### 获取中文分词
+
+终结点: `/.get_word_slices`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| --------- | ------ | ---- |
+| `content` | string | 内容 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| -------- | -------- | ---- |
+| `slices` | string[] | 词组 |
+
+### 设置精华消息
+
+终结点: `/set_essence_msg`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| --------- | ------ | ---- |
+| `message_id` | int32 | 消息ID |
+
+**响应数据**
+
+无
+
+### 移出精华消息
+
+终结点: `/delete_essence_msg`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| --------- | ------ | ---- |
+| `message_id` | int32 | 消息ID |
+
+**响应数据**
+
+无
+
+### 获取精华消息列表
+
+终结点: `/get_essence_msg_list`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| --------- | ------ | ---- |
+| `group_id` | int64 | 群号 |
+
+**响应数据**
+
+响应内容为 JSON 数组,每个元素如下:
+
+| 字段名 | 数据类型 | 说明 |
+| ----- | ------- | --- |
+| `sender_id` |int64 | 发送者QQ 号 |
+| `sender_nick` | string | 发送者昵称 |
+| `sender_time` | int64 | 消息发送时间 |
+| `operator_id` |int64 | 发送者QQ 号 |
+| `operator_nick` | string | 发送者昵称 |
+| `operator_time` | int64 | 消息发送时间|
+| `message_id` | int32 | 消息ID |
+
+### 图片OCR
+
+> 注意: 目前图片OCR接口仅支持接受的图片
+
+终结点: `/ocr_image`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ------- | ------ | ------ |
+| `image` | string | 图片ID |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ---------- | --------------- | ------- |
+| `texts` | TextDetection[] | OCR结果 |
+| `language` | string | 语言 |
+
+**TextDetection**
+
+| 字段 | 类型 | 说明 |
+| ------------- | ------- | ------ |
+| `text` | string | 文本 |
+| `confidence` | int32 | 置信度 |
+| `coordinates` | vector2 | 坐标 |
+
+
+### 获取群系统消息
+
+终结点: `/get_group_system_msg`
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ------------------ | ---------------- | ------------ |
+| `invited_requests` | InvitedRequest[] | 邀请消息列表 |
+| `join_requests` | JoinRequest[] | 进群消息列表 |
+
+ > 注意: 如果列表不存在任何消息, 将返回 `null`
+
+ **InvitedRequest**
+
+| 字段 | 类型 | 说明 |
+| -------------- | ------ | ----------------- |
+| `request_id` | int64 | 请求ID |
+| `invitor_uin` | int64 | 邀请者 |
+| `invitor_nick` | string | 邀请者昵称 |
+| `group_id` | int64 | 群号 |
+| `group_name` | string | 群名 |
+| `checked` | bool | 是否已被处理 |
+| `actor` | int64 | 处理者, 未处理为0 |
+
+ **JoinRequest**
+
+| 字段 | 类型 | 说明 |
+| ---------------- | ------ | ----------------- |
+| `request_id` | int64 | 请求ID |
+| `requester_uin` | int64 | 请求者ID |
+| `requester_nick` | string | 请求者昵称 |
+| `message` | string | 验证消息 |
+| `group_id` | int64 | 群号 |
+| `group_name` | string | 群名 |
+| `checked` | bool | 是否已被处理 |
+| `actor` | int64 | 处理者, 未处理为0 |
+
+### 获取群文件系统信息
+
+终结点: `/get_group_file_system_info`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ----- | ---- |
+| `group_id` | int64 | 群号 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ------------- | ----- | ---------- |
+| `file_count` | int32 | 文件总数 |
+| `limit_count` | int32 | 文件上限 |
+| `used_space` | int64 | 已使用空间 |
+| `total_space` | int64 | 空间上限 |
+
+### 获取群根目录文件列表
+
+> `File` 和 `Folder` 对象信息请参考最下方
+
+终结点: `/get_group_root_files`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ----- | ---- |
+| `group_id` | int64 | 群号 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| --------- | -------- | ---------- |
+| `files` | File[] | 文件列表 |
+| `folders` | Folder[] | 文件夹列表 |
+
+### 获取群子目录文件列表
+
+> `File` 和 `Folder` 对象信息请参考最下方
+
+终结点: `/get_group_files_by_folder`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ----------- | ------ | --------------------------- |
+| `group_id` | int64 | 群号 |
+| `folder_id` | string | 文件夹ID 参考 `Folder` 对象 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| --------- | -------- | ---------- |
+| `files` | File[] | 文件列表 |
+| `folders` | Folder[] | 文件夹列表 |
+
+### 获取群文件资源链接
+
+> `File` 和 `Folder` 对象信息请参考最下方
+
+终结点: `/get_group_file_url`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `group_id` | int64 | 群号 |
+| `file_id` | string | 文件ID 参考 `File` 对象 |
+| `busid` | int32 | 文件类型 参考 `File` 对象 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ----- | ------ | ------------ |
+| `url` | string | 文件下载链接 |
+
+ **File**
+
+| 字段 | 类型 | 说明 |
+| ---------------- | ------ | ---------------------- |
+| `file_id` | string | 文件ID |
+| `file_name` | string | 文件名 |
+| `busid` | int32 | 文件类型 |
+| `file_size` | int64 | 文件大小 |
+| `upload_time` | int64 | 上传时间 |
+| `dead_time` | int64 | 过期时间,永久文件恒为0 |
+| `modify_time` | int64 | 最后修改时间 |
+| `download_times` | int32 | 下载次数 |
+| `uploader` | int64 | 上传者ID |
+| `uploader_name` | string | 上传者名字 |
+
+ **Folder**
+
+| 字段 | 类型 | 说明 |
+| ------------------ | ------ | ---------- |
+| `folder_id` | string | 文件夹ID |
+| `folder_name` | string | 文件名 |
+| `create_time` | int64 | 创建时间 |
+| `creator` | int64 | 创建者 |
+| `creator_name` | string | 创建者名字 |
+| `total_file_count` | int32 | 子文件数量 |
+
+### 上传群文件
+
+终结点: `/upload_group_file`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `group_id` | int64 | 群号 |
+| `file` | string | 本地文件路径 |
+| `name` | string | 储存名称 |
+| `folder` | string | 父目录ID |
+
+> 在不提供 `folder` 参数的情况下默认上传到根目录
+> 只能上传本地文件, 需要上传 `http` 文件的话请先调用 `download_file` API下载
+
+### 获取状态
+
+终结点: `/get_status`
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ----------------- | ---------- | ------------------------------- |
+| `app_initialized` | bool | 原 `CQHTTP` 字段, 恒定为 `true` |
+| `app_enabled` | bool | 原 `CQHTTP` 字段, 恒定为 `true` |
+| `plugins_good` | bool | 原 `CQHTTP` 字段, 恒定为 `true` |
+| `app_good` | bool | 原 `CQHTTP` 字段, 恒定为 `true` |
+| `online` | bool | 表示BOT是否在线 |
+| `good` | bool | 同 `online` |
+| `stat` | Statistics | 运行统计 |
+
+**Statistics**
+
+
+| 字段 | 类型 | 说明 |
+| ------------------ | ------ | ---------------- |
+| `packet_received` | uint64 | 收到的数据包总数 |
+| `packet_sent` | uint64 | 发送的数据包总数 |
+| `packet_lost` | uint32 | 数据包丢失总数 |
+| `message_received` | uint64 | 接受信息总数 |
+| `message_sent` | uint64 | 发送信息总数 |
+| `disconnect_times` | uint32 | TCP链接断开次数 |
+| `lost_times` | uint32 | 账号掉线次数 |
+
+> 注意: 所有统计信息都将在重启后重制
+
+### 获取群@全体成员剩余次数
+
+终结点: `/get_group_at_all_remain`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `group_id` | int64 | 群号 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ------------------------------- | ---------- | ------------------------------- |
+| `can_at_all` | bool | 是否可以@全体成员 |
+| `remain_at_all_count_for_group` | int16 | 群内所有管理当天剩余@全体成员次数 |
+| `remain_at_all_count_for_uin` | int16 | BOT当天剩余@全体成员次数 |
+
+### 下载文件到缓存目录
+
+终结点: `/download_file`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `url` | string | 链接地址 |
+| `thread_count` | int32 | 下载线程数 |
+| `headers` | string or array | 自定义请求头 |
+
+**`headers`格式:**
+
+字符串:
+
+```
+User-Agent=YOUR_UA[\r\n]Referer=https://www.baidu.com
+```
+
+> `[\r\n]` 为换行符, 使用http请求时请注意编码
+
+JSON数组:
+
+```
+[
+ "User-Agent=YOUR_UA",
+ "Referer=https://www.baidu.com",
+]
+```
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ---------- | ------------ |
+| `file` | string | 下载文件的*绝对路径* |
+
+> 通过这个API下载的文件能直接放入CQ码作为图片或语音发送
+> 调用后会阻塞直到下载完成后才会返回数据,请注意下载大文件时的超时
+
+### 获取群消息历史记录
+
+终结点:`/get_group_msg_history`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `message_seq` | int64 | 起始消息序号, 可通过 `get_msg` 获得 |
+| `group_id` | int64 | 群号 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ---------- | ------------ |
+| `messages` | []Message | 从起始序号开始的前19条消息 |
+
+> 不提供起始序号将默认获取最新的消息
+
+### 获取当前账号在线客户端列表
+
+终结点:`/get_online_clients`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `no_cache` | bool | 是否无视缓存 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ---------- | ------------ |
+| `clients` | []Device | 在线客户端列表 |
+
+**Device**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ---------- | ------------ |
+| `app_id` | int64 | 客户端ID |
+| `device_name` | string | 设备名称 |
+| `device_kind` | string | 设备类型 |
+
+### 检查链接安全性
+
+终结点:`/check_url_safely`
+
+**参数**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------- |
+| `url` | string | 需要检查的链接 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ---------- | ---------- | ------------ |
+| `level` | int | 安全等级, 1: 安全 2: 未知 3: 危险 |
+
+### 获取用户VIP信息
+
+终结点:`/_get_vip_info`
+
+**参数**
+
+| 字段名 | 数据类型 | 默认值 | 说明 |
+| ----- | ------- | ----- | --- |
+| `user_id` | int64 | | QQ 号 |
+
+**响应数据**
+
+| 字段 | 类型 | 说明 |
+| ------------------ | ------- | ---------- |
+| `user_id` | int64 | QQ 号 |
+| `nickname` | string | 用户昵称 |
+| `level` | int64 | QQ 等级 |
+| `level_speed` | float64 | 等级加速度 |
+| `vip_level` | string | 会员等级 |
+| `vip_growth_speed` | int64 | 会员成长速度 |
+| `vip_growth_total` | int64 | 会员成长总值 |
+
+### 发送群公告
+
+终结点: `/_send_group_notice`
+
+**参数**
+
+| 字段名 | 数据类型 | 默认值 | 说明 |
+| ---------- | ------- | ----- | ------ |
+| `group_id` | int64 | | 群号 |
+| `content` | string | | 公告内容 |
+
+`该 API 没有响应数据`
+
+### 重载事件过滤器
+
+终结点:`/reload_event_filter`
+
+`该 API 无需参数也没有响应数据`
+
## 事件
-#### 群消息撤回
+### 群消息撤回
**上报数据**
@@ -351,14 +1067,133 @@ Type: `cardimage`
| `operator_id` | int64 | | 操作者id |
| `message_id` | int64 | | 被撤回的消息id |
-#### 好友消息撤回
+### 好友消息撤回
**上报数据**
-| 字段 | 类型 | 可能的值 | 说明 |
-| ------------- | ------ | -------------- | -------------- |
-| `post_type` | string | `notice` | 上报类型 |
-| `notice_type` | string | `friend_recall`| 消息类型 |
-| `user_id` | int64 | | 好友id |
-| `message_id` | int64 | | 被撤回的消息id |
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | --------------- | -------------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `friend_recall` | 消息类型 |
+| `user_id` | int64 | | 好友id |
+| `message_id` | int64 | | 被撤回的消息id |
+## 好友戳一戳
+
+**事件数据**
+
+| 字段名 | 数据类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------- | --- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `notify` | 消息类型 |
+| `sub_type` | string | `poke` | 提示类型 |
+| `self_id` | int64 | | BOT QQ 号 |
+| `sender_id` | int64 | | 发送者 QQ 号 |
+| `user_id` | int64 | | 发送者 QQ 号 |
+| `target_id` | int64 | | 被戳者 QQ 号 |
+| `time` | int64 | | 时间 |
+
+### 群内戳一戳
+
+> 注意:此事件无法在平板和手表协议上触发
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------- | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `notify` | 消息类型 |
+| `group_id` | int64 | | 群号 |
+| `sub_type` | string | `poke` | 提示类型 |
+| `user_id` | int64 | | 发送者id |
+| `target_id` | int64 | | 被戳者id |
+
+### 群红包运气王提示
+
+> 注意:此事件无法在平板和手表协议上触发
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | ------------ | ------------ |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `notify` | 消息类型 |
+| `group_id` | int64 | | 群号 |
+| `sub_type` | string | `lucky_king` | 提示类型 |
+| `user_id` | int64 | | 红包发送者id |
+| `target_id` | int64 | | 运气王id |
+
+### 群成员荣誉变更提示
+
+> 注意:此事件无法在平板和手表协议上触发
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------------------------------------------------------- | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `notify` | 消息类型 |
+| `group_id` | int64 | | 群号 |
+| `sub_type` | string | `honor` | 提示类型 |
+| `user_id` | int64 | | 成员id |
+| `honor_type` | string | `talkative:龙王` `performer:群聊之火` `emotion:快乐源泉` | 荣誉类型 |
+
+### 群成员名片更新
+
+> 注意: 此事件不保证时效性,仅在收到消息时校验卡片
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | ------------ | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `group_card` | 消息类型 |
+| `group_id` | int64 | | 群号 |
+| `user_id` | int64 | | 成员id |
+| `card_new` | int64 | | 新名片 |
+| `card_old` | int64 | | 旧名片 |
+
+> PS: 当名片为空时 `card_xx` 字段为空字符串, 并不是昵称
+
+### 接收到离线文件
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------------- | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `offline_file` | 消息类型 |
+| `user_id` | int64 | | 发送者id |
+| `file` | object | | 文件数据 |
+
+**file object**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------ | ------ | -------- | -------- |
+| `name` | string | | 文件名 |
+| `size` | int64 | | 文件大小 |
+| `url` | string | | 下载链接 |
+
+### 其他客户端在线状态变更
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------------- | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `client_status` | 消息类型 |
+| `client` | Device | | 客户端信息 |
+| `online` | bool | | 当前是否在线 |
+
+### 精华消息
+
+**上报数据**
+
+| 字段 | 类型 | 可能的值 | 说明 |
+| ------------- | ------ | -------------- | -------- |
+| `post_type` | string | `notice` | 上报类型 |
+| `notice_type` | string | `essence` | 消息类型 |
+| `sub_type` | string | `add`,`delete` | 添加为`add`,移出为`delete` |
+| `sender_id` | int64 | | 消息发送者ID |
+| `operator_id` | int64 | | 操作者ID |
+| `message_id` | int32 | | 消息ID |
diff --git a/docs/file.md b/docs/file.md
index 4fe926e..987a49d 100644
--- a/docs/file.md
+++ b/docs/file.md
@@ -5,7 +5,7 @@ go-cqhttp 默认生成的文件树如下所示:
````
.
├── go-cqhttp
-├── config.json
+├── config.hjson
├── device.json
├── logs
│ └── xx-xx-xx.log
@@ -18,7 +18,7 @@ go-cqhttp 默认生成的文件树如下所示:
| 文件 | 用途 |
| ----------- | ------------------- |
| go-cqhttp | go-cqhttp可执行文件 |
-| config.json | 运行配置文件 |
+| config.hjson | 运行配置文件 |
| device.json | 虚拟设备配置文件 |
| logs | 日志存放目录 |
| data | 数据目录 |
diff --git a/docs/quick_start.md b/docs/quick_start.md
index ebd81f6..af91dcb 100644
--- a/docs/quick_start.md
+++ b/docs/quick_start.md
@@ -1,3 +1,155 @@
# 开始
-欢迎来到 go-cqhttp 文档
\ No newline at end of file
+欢迎来到 go-cqhttp 文档 目前还在咕
+
+# 基础教程
+## 下载
+从[release](https://github.com/Mrs4s/go-cqhttp/releases)界面下载最新版本的go-cqhttp
+
+- Windows下32位文件为 `go-cqhttp-v*-windows-386.zip`
+- Windows下64位文件为 `go-cqhttp-v*-windows-amd64.zip`
+- Windows下arm用(如使用高通CPU的笔记本)文件为 `go-cqhttp-v*-windows-arm.zip`
+- Linux下32位文件为 `go-cqhttp-v*-linux-386.tar.gz`
+- Linux下64位文件为 `go-cqhttp-v*-linux-amd64.tar.gz`
+- Linux下arm用(如树莓派)文件为 `go-cqhttp-v*-linux-arm.tar.gz`
+- MD5文件为 `*.md5` ,用于校验文件完整性
+- 如果没有你所使用的系统版本或者希望自己构建,请移步[进阶指南-如何自己构建](#如何自己构建)
+
+## 解压
+
+- Windows下请使用自己熟悉的解压软件自行解压
+- Linux下在命令行中输入 `tar -xzvf [文件名]`
+
+## 使用
+
+### Windows
+
+#### 标准方法
+
+1. 双击`go-cqhttp.exe`此时将提示
+```
+[WARNING]: 尝试加载配置文件 config.hjson 失败: 文件不存在
+[INFO]: 默认配置文件已生成,请编辑 config.hjson 后重启程序.
+```
+2. 参照[config.md](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数
+3. 再次双击`go-cqhttp.exe`
+```
+[INFO]: 登录成功 欢迎使用: balabala
+```
+
+如出现需要认证的信息,请自行认证设备。
+
+此时,基础配置完成
+
+#### 懒人法
+
+1. [下载包含Windows.bat的zip](https://github.com/fkx4-p/go-cqhttp-lazy/archive/master.zip)
+2. 解压
+3. 将`Windows.bat`复制/剪切到**go-cqhttp**文件夹
+4. 双击运行
+
+效果如下
+
+```
+QQ account:
+[QQ账号]
+QQ password:
+[QQ密码]
+enable http?(Y/n)
+[是否开启http(y/n),默认开启]
+enable ws?(Y/n)
+[是否开启websocket(y/n),默认开启]
+请按任意键继续. . .
+```
+
+5. 双击`go-cqhttp.exe`
+```
+[INFO]: 登录成功 欢迎使用: balabala
+```
+
+如出现需要认证的信息,请自行认证设备。
+
+此时,基础配置完成
+
+### Linux
+
+#### 标准方法
+
+1. 打开一个命令行/ssh
+2. `cd`到解压目录
+3. 输入 `./go-cqhttp`,`Enter`运行 ,此时将提示
+```
+[WARNING]: 尝试加载配置文件 config.hjson 失败: 文件不存在
+[INFO]: 默认配置文件已生成,请编辑 config.hjson 后重启程序.
+```
+
+4. 参照[config.md](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数
+5. 再次输入 `./go-cqhttp`,`Enter`运行
+```
+[INFO]: 登录成功 欢迎使用: balabala
+```
+
+如出现需要认证的信息,请自行认证设备。
+
+此时,基础配置完成
+
+#### 懒人法
+
+暂时咕咕咕了
+
+## 验证http是否成功配置
+
+此时,如果在本地开启的服务器,可以在浏览器输入`http://127.0.0.1:5700/send_private_msg?user_id=[接收者qq号]&message=[发送的信息]`来发送一条测试信息
+
+如果出现`{"data":{"message_id":balabala},"retcode":0,"status":"ok"}`则证明已经成功配置HTTP
+
+*注:请 连 中括号 也替换掉,就像这样:*`http://127.0.0.1:5700/send_private_msg?user_id=10001&message=ffeecoishp`
+
+# 进阶指南
+
+## 跳过启动的五秒延时
+
+使用命令行参数 `faststart`即可跳过启动的五秒钟延时,例如
+
+```
+.\go-cqhttp.exe faststart
+```
+
+## 如何自己构建
+
+1. [下载源码](https://github.com/Mrs4s/go-cqhttp/archive/master.zip)并解压 || 使用`git clone https://github.com/Mrs4s/go-cqhttp.git`来拉取
+
+2. [下载golang binary release](https://golang.google.cn/dl/)并安装或者[自己构建golang](https://golang.google.cn/doc/install/source)
+
+3. 在`cmd`或Linux命令行中,`cd`到目录中
+
+4. 输入`go build -ldflags "-s -w -extldflags '-static'"`,`Enter`运行
+
+*注:可以使用*`go env -w GOPROXY=https://goproxy.cn,direct`*来加速国内依赖安装速度*
+
+## 更新
+
+### 方法一
+
+从[release](https://github.com/Mrs4s/go-cqhttp/releases)界面下载最新版本的go-cqhttp
+并替换之前的版本
+
+### 方法二
+
+使用更新参数,在命令行中打开go-cqhttp所在目录
+#### windows
+输入指令
+`go-cqhttp.exe update`
+
+如果在国内连接github下载速度可能很慢,可以使用镜像源下载
+
+`go-cqhttp.exe update https://github.rc1844.workers.dev`
+
+几个可用的镜像源
+- `https://hub.fastgit.org`
+- `https://github.com.cnpmjs.org`
+- `https://github.bajins.com`
+- `https://github.rc1844.workers.dev`
+
+#### linux
+方法与windows基本一致,将 `go-cqhttp.exe` 替换为 `./go-cqhttp`即可
diff --git a/docs/slider.md b/docs/slider.md
new file mode 100644
index 0000000..c29593c
--- /dev/null
+++ b/docs/slider.md
@@ -0,0 +1,74 @@
+# 滑块验证码
+
+由于TX最新的限制, 所有协议在陌生设备/IP登录时都有可能被要求通过滑块验证码, 否则将会出现 `当前上网环境异常` 的错误. 目前我们准备了两个临时方案应对该验证码.
+
+> 如果您有一台运行Windows的PC/Server 并且不会抓包操作, 我们建议直接使用方案B
+
+## 方案A: 自行抓包
+
+由于滑块验证码和QQ本体的协议独立, 我们无法直接处理并提交. 需要在浏览器通过后抓包并获取 `Ticket` 提交.
+
+该方案为具体的抓包教程, 如果您已经知道如何在浏览器中抓包. 可以略过接下来的文档并直接抓取 `cap_union_new_verify` 的返回值, 提取 `Ticket` 并在命令行提交.
+
+首先获取滑块验证码的地址, 并在浏览器中打开. 这里以 *Microsoft Edge* 浏览器为例, *Chrome* 同理.
+
+
+
+首先选择 `1` 并提取链接在浏览器中打开
+
+
+
+
+
+此时不要滑动验证码, 首先按下 `F12` (键盘右上角退格键上方) 打开 *开发者工具*
+
+
+
+点击 `Network` 选项卡 (在某些浏览器它可能叫做 `网络`)
+
+
+
+点开 `Filter` (箭头) 按钮以确定您能看到下面的工具栏, 勾选 `Preserve log`(红框)
+
+此时可以滑动并通过验证码
+
+
+
+回到 *开发者工具*, 我们可以看到已经有了一个请求.
+
+
+
+此时如果有多个请求, 请不要慌张. 看到上面的 `Filter` 没? 此时在 `Filter` 输入框中输入 `cap_union_new`, 就应该只剩一个请求了.
+
+然后点击该请求. 点开 `Preview` 选项卡 (箭头):
+
+
+
+此时就能看到一个标准的 `JSON`, 复制 `ticket` 字段并回到 `go-cqhttp` 粘贴. 即可通过滑块验证.
+
+如果您看到这里还是不会如何操作, 没关系! 我们还准备了方案B.
+
+## 方案B: 使用专用工具
+
+此方案需要您有一台可以操作的 `Windows` 电脑.
+
+首先下载工具: [蓝奏云](https://wws.lanzous.com/i2vn0jrofte) [Google Drive](https://drive.google.com/file/d/1peMDHqgP8AgWBVp5vP-cfhcGrb2ksSrE/view?usp=sharing)
+
+解压并打开工具:
+
+
+
+打开 `go-cqhttp` 并选择 `2`:
+
+
+
+复制 `ID` 并前往工具粘贴:
+
+
+
+
+
+点击 `OK` 并处理滑块, 完成即可登录成功. (OK可能反应稍微慢点, 请不要多次点击)
+
+
+
diff --git a/global/codec.go b/global/codec.go
new file mode 100644
index 0000000..1a6b35f
--- /dev/null
+++ b/global/codec.go
@@ -0,0 +1,63 @@
+package global
+
+import (
+ "crypto/md5"
+ "fmt"
+ "io/ioutil"
+ "os/exec"
+ "path"
+
+ "github.com/Mrs4s/go-cqhttp/global/codec"
+ "github.com/pkg/errors"
+ log "github.com/sirupsen/logrus"
+)
+
+var useSilkCodec = true
+
+//InitCodec 初始化Silk编码器
+func InitCodec() {
+ log.Info("正在加载silk编码器...")
+ err := codec.Init()
+ if err != nil {
+ log.Error(err)
+ useSilkCodec = false
+ }
+}
+
+//EncoderSilk 将音频编码为Silk
+func EncoderSilk(data []byte) ([]byte, error) {
+ if !useSilkCodec {
+ return nil, errors.New("no silk encoder")
+ }
+ h := md5.New()
+ _, err := h.Write(data)
+ if err != nil {
+ return nil, errors.Wrap(err, "calc md5 failed")
+ }
+ tempName := fmt.Sprintf("%x", h.Sum(nil))
+ if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) {
+ return ioutil.ReadFile(silkPath)
+ }
+ slk, err := codec.EncodeToSilk(data, tempName, true)
+ if err != nil {
+ return nil, errors.Wrap(err, "encode silk failed")
+ }
+ return slk, nil
+}
+
+//EncodeMP4 将给定视频文件编码为MP4
+func EncodeMP4(src string, dst string) error { // -y 覆盖文件
+ cmd1 := exec.Command("ffmpeg", "-i", src, "-y", "-c", "copy", "-map", "0", dst)
+ err := cmd1.Run()
+ if err != nil {
+ cmd2 := exec.Command("ffmpeg", "-i", src, "-y", "-c:v", "h264", "-c:a", "mp3", dst)
+ return errors.Wrap(cmd2.Run(), "convert mp4 failed")
+ }
+ return err
+}
+
+//ExtractCover 获取给定视频文件的Cover
+func ExtractCover(src string, target string) error {
+ cmd := exec.Command("ffmpeg", "-i", src, "-y", "-r", "1", "-f", "image2", target)
+ return errors.Wrap(cmd.Run(), "extract video cover failed")
+}
diff --git a/global/codec/codec.go b/global/codec/codec.go
new file mode 100644
index 0000000..9b6115c
--- /dev/null
+++ b/global/codec/codec.go
@@ -0,0 +1,97 @@
+// +build linux windows darwin
+// +build 386 amd64 arm arm64
+
+package codec
+
+import (
+ "github.com/pkg/errors"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/exec"
+ "path"
+ "runtime"
+)
+
+const (
+ silkCachePath = "data/cache"
+ encoderPath = "codec"
+)
+
+func downloadCodec(url string) (err error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return
+ }
+ err = ioutil.WriteFile(getEncoderFilePath(), body, os.ModePerm)
+ return
+}
+
+func getEncoderFilePath() string {
+ encoderFile := path.Join(encoderPath, runtime.GOOS+"-"+runtime.GOARCH+"-encoder")
+ if runtime.GOOS == "windows" {
+ encoderFile = encoderFile + ".exe"
+ }
+ return encoderFile
+}
+
+//Init 下载Silk编码器
+func Init() error {
+ if !fileExist(silkCachePath) {
+ _ = os.MkdirAll(silkCachePath, os.ModePerm)
+ }
+ if !fileExist(encoderPath) {
+ _ = os.MkdirAll(encoderPath, os.ModePerm)
+ }
+ p := getEncoderFilePath()
+ if !fileExist(p) {
+ if err := downloadCodec("https://cdn.jsdelivr.net/gh/wdvxdr1123/tosilk/codec/" + runtime.GOOS + "-" + runtime.GOARCH + "-encoder"); err != nil {
+ return errors.New("下载依赖失败")
+ }
+ }
+ return nil
+}
+
+//EncodeToSilk 将音频编码为Silk
+func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
+ // 1. 写入缓存文件
+ rawPath := path.Join(silkCachePath, tempName+".wav")
+ err := ioutil.WriteFile(rawPath, record, os.ModePerm)
+ if err != nil {
+ return nil, errors.Wrap(err, "write temp file error")
+ }
+ defer os.Remove(rawPath)
+
+ // 2.转换pcm
+ pcmPath := path.Join(silkCachePath, tempName+".pcm")
+ cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath)
+ if err = cmd.Run(); err != nil {
+ return nil, errors.Wrap(err, "convert pcm file error")
+ }
+ defer os.Remove(pcmPath)
+
+ // 3. 转silk
+ silkPath := path.Join(silkCachePath, tempName+".silk")
+ cmd = exec.Command(getEncoderFilePath(), pcmPath, silkPath, "-rate", "24000", "-quiet", "-tencent")
+ if err = cmd.Run(); err != nil {
+ return nil, errors.Wrap(err, "convert silk file error")
+ }
+ if !useCache {
+ defer os.Remove(silkPath)
+ }
+ return ioutil.ReadFile(silkPath)
+}
+
+// FileExist 检查文件是否存在
+func fileExist(path string) bool {
+ if runtime.GOOS == "windows" {
+ path = path + ".exe"
+ }
+ _, err := os.Lstat(path)
+ return !os.IsNotExist(err)
+}
diff --git a/global/codec/codec_unsupportedarch.go b/global/codec/codec_unsupportedarch.go
new file mode 100644
index 0000000..b0e9890
--- /dev/null
+++ b/global/codec/codec_unsupportedarch.go
@@ -0,0 +1,15 @@
+// +build !386,!arm64,!amd64,!arm
+
+package codec
+
+import "errors"
+
+//Init 下载silk编码器
+func Init() error {
+ return errors.New("Unsupport arch now")
+}
+
+//EncodeToSilk 将音频编码为Silk
+func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
+ return nil, errors.New("Unsupport arch now")
+}
diff --git a/global/codec/codec_unsupportedos.go b/global/codec/codec_unsupportedos.go
new file mode 100644
index 0000000..9428fe0
--- /dev/null
+++ b/global/codec/codec_unsupportedos.go
@@ -0,0 +1,15 @@
+// +build !windows,!linux,!darwin
+
+package codec
+
+import "errors"
+
+//Init 下载silk编码器
+func Init() error {
+ return errors.New("not support now")
+}
+
+//EncodeToSilk 将音频编码为Silk
+func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
+ return nil, errors.New("not support now")
+}
diff --git a/global/config.go b/global/config.go
index 31ed776..0abf933 100644
--- a/global/config.go
+++ b/global/config.go
@@ -1,15 +1,145 @@
package global
import (
- "encoding/json"
"os"
"strconv"
"time"
+ "github.com/hjson/hjson-go"
+ jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)
-type JsonConfig struct {
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+//DefaultConfigWithComments 为go-cqhttp的默认配置文件
+var DefaultConfigWithComments = `
+/*
+ go-cqhttp 默认配置文件
+*/
+
+{
+ // QQ号
+ uin: 0
+ // QQ密码
+ password: ""
+ // 是否启用密码加密
+ encrypt_password: false
+ // 加密后的密码, 如未启用密码加密将为空, 请勿随意修改.
+ password_encrypted: ""
+ // 是否启用内置数据库
+ // 启用将会增加10-20MB的内存占用和一定的磁盘空间
+ // 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
+ enable_db: true
+ // 访问密钥, 强烈推荐在公网的服务器设置
+ access_token: ""
+ // 重连设置
+ relogin: {
+ // 是否启用自动重连
+ // 如不启用掉线后将不会自动重连
+ enabled: true
+ // 重连延迟, 单位秒
+ relogin_delay: 3
+ // 最大重连次数, 0为无限制
+ max_relogin_times: 0
+ }
+ // API限速设置
+ // 该设置为全局生效
+ // 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
+ // 目前该限速设置为令牌桶算法, 请参考:
+ // https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
+ _rate_limit: {
+ // 是否启用限速
+ enabled: false
+ // 令牌回复频率, 单位秒
+ frequency: 1
+ // 令牌桶大小
+ bucket_size: 1
+ }
+ // 是否忽略无效的CQ码
+ // 如果为假将原样发送
+ ignore_invalid_cqcode: false
+ // 是否强制分片发送消息
+ // 分片发送将会带来更快的速度
+ // 但是兼容性会有些问题
+ force_fragmented: false
+ // 心跳频率, 单位秒
+ // -1 为关闭心跳
+ heartbeat_interval: 0
+ // HTTP设置
+ http_config: {
+ // 是否启用正向HTTP服务器
+ enabled: true
+ // 服务端监听地址
+ host: 0.0.0.0
+ // 服务端监听端口
+ port: 5700
+ // 反向HTTP超时时间, 单位秒
+ // 最小值为5,小于5将会忽略本项设置
+ timeout: 0
+ // 反向HTTP POST地址列表
+ // 格式:
+ // {
+ // 地址: secret
+ // }
+ post_urls: {}
+ }
+ // 正向WS设置
+ ws_config: {
+ // 是否启用正向WS服务器
+ enabled: true
+ // 正向WS服务器监听地址
+ host: 0.0.0.0
+ // 正向WS服务器监听端口
+ port: 6700
+ }
+ // 反向WS设置
+ ws_reverse_servers: [
+ // 可以添加多个反向WS推送
+ {
+ // 是否启用该推送
+ enabled: false
+ // 反向WS Universal 地址
+ // 注意 设置了此项地址后下面两项将会被忽略
+ // 留空请使用 ""
+ reverse_url: ws://you_websocket_universal.server
+ // 反向WS API 地址
+ reverse_api_url: ws://you_websocket_api.server
+ // 反向WS Event 地址
+ reverse_event_url: ws://you_websocket_event.server
+ // 重连间隔 单位毫秒
+ reverse_reconnect_interval: 3000
+ }
+ ]
+ // 上报数据类型
+ // 可选: string array
+ post_message_format: string
+ // 是否使用服务器下发的新地址进行重连
+ // 注意, 此设置可能导致在海外服务器上连接情况更差
+ use_sso_address: false
+ // 是否启用 DEBUG
+ debug: false
+ // 日志等级 trace,debug,info,warn,error
+ log_level: "info"
+ // WebUi 设置
+ web_ui: {
+ // 是否启用 WebUi
+ enabled: true
+ // 监听地址
+ host: 127.0.0.1
+ // 监听端口
+ web_ui_port: 9999
+ // 是否接收来自web的输入
+ web_input: false
+ }
+}
+`
+
+//PasswordHash 存储QQ密码哈希供登录使用
+var PasswordHash [16]byte
+
+//JSONConfig Config对应的结构体
+type JSONConfig struct {
Uin int64 `json:"uin"`
Password string `json:"password"`
EncryptPassword bool `json:"encrypt_password"`
@@ -28,35 +158,41 @@ type JsonConfig struct {
} `json:"_rate_limit"`
IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"`
ForceFragmented bool `json:"force_fragmented"`
+ FixURL bool `json:"fix_url"`
+ ProxyRewrite string `json:"proxy_rewrite"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
- HttpConfig *GoCQHttpConfig `json:"http_config"`
- WSConfig *GoCQWebsocketConfig `json:"ws_config"`
- ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
+ HTTPConfig *GoCQHTTPConfig `json:"http_config"`
+ WSConfig *GoCQWebSocketConfig `json:"ws_config"`
+ ReverseServers []*GoCQReverseWebSocketConfig `json:"ws_reverse_servers"`
PostMessageFormat string `json:"post_message_format"`
+ UseSSOAddress bool `json:"use_sso_address"`
Debug bool `json:"debug"`
LogLevel string `json:"log_level"`
+ WebUI *GoCQWebUI `json:"web_ui"`
}
-type CQHttpApiConfig struct {
+//CQHTTPAPIConfig HTTPAPI对应的Config结构体
+type CQHTTPAPIConfig struct {
Host string `json:"host"`
Port uint16 `json:"port"`
- UseHttp bool `json:"use_http"`
+ UseHTTP bool `json:"use_http"`
WSHost string `json:"ws_host"`
WSPort uint16 `json:"ws_port"`
UseWS bool `json:"use_ws"`
- WSReverseUrl string `json:"ws_reverse_url"`
- WSReverseApiUrl string `json:"ws_reverse_api_url"`
- WSReverseEventUrl string `json:"ws_reverse_event_url"`
+ WSReverseURL string `json:"ws_reverse_url"`
+ WSReverseAPIURL string `json:"ws_reverse_api_url"`
+ WSReverseEventURL string `json:"ws_reverse_event_url"`
WSReverseReconnectInterval uint16 `json:"ws_reverse_reconnect_interval"`
WSReverseReconnectOnCode1000 bool `json:"ws_reverse_reconnect_on_code_1000"`
UseWsReverse bool `json:"use_ws_reverse"`
- PostUrl string `json:"post_url"`
+ PostURL string `json:"post_url"`
AccessToken string `json:"access_token"`
Secret string `json:"secret"`
PostMessageFormat string `json:"post_message_format"`
}
-type GoCQHttpConfig struct {
+//GoCQHTTPConfig 正向HTTP对应config结构体
+type GoCQHTTPConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
@@ -64,22 +200,33 @@ type GoCQHttpConfig struct {
PostUrls map[string]string `json:"post_urls"`
}
-type GoCQWebsocketConfig struct {
+//GoCQWebSocketConfig 正向WebSocket对应Config结构体
+type GoCQWebSocketConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
}
-type GoCQReverseWebsocketConfig struct {
+//GoCQReverseWebSocketConfig 反向WebSocket对应Config结构体
+type GoCQReverseWebSocketConfig struct {
Enabled bool `json:"enabled"`
- ReverseUrl string `json:"reverse_url"`
- ReverseApiUrl string `json:"reverse_api_url"`
- ReverseEventUrl string `json:"reverse_event_url"`
+ ReverseURL string `json:"reverse_url"`
+ ReverseAPIURL string `json:"reverse_api_url"`
+ ReverseEventURL string `json:"reverse_event_url"`
ReverseReconnectInterval uint16 `json:"reverse_reconnect_interval"`
}
-func DefaultConfig() *JsonConfig {
- return &JsonConfig{
+//GoCQWebUI WebUI对应Config结构体
+type GoCQWebUI struct {
+ Enabled bool `json:"enabled"`
+ Host string `json:"host"`
+ WebUIPort uint64 `json:"web_ui_port"`
+ WebInput bool `json:"web_input"`
+}
+
+//DefaultConfig 返回一份默认配置对应结构体
+func DefaultConfig() *JSONConfig {
+ return &JSONConfig{
EnableDB: true,
ReLogin: struct {
Enabled bool `json:"enabled"`
@@ -100,51 +247,67 @@ func DefaultConfig() *JsonConfig {
BucketSize: 1,
},
PostMessageFormat: "string",
- ForceFragmented: true,
- HttpConfig: &GoCQHttpConfig{
+ ForceFragmented: false,
+ HTTPConfig: &GoCQHTTPConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 5700,
PostUrls: map[string]string{},
},
- WSConfig: &GoCQWebsocketConfig{
+ WSConfig: &GoCQWebSocketConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 6700,
},
- ReverseServers: []*GoCQReverseWebsocketConfig{
+ ReverseServers: []*GoCQReverseWebSocketConfig{
{
Enabled: false,
- ReverseUrl: "ws://you_websocket_universal.server",
- ReverseApiUrl: "ws://you_websocket_api.server",
- ReverseEventUrl: "ws://you_websocket_event.server",
+ ReverseURL: "ws://you_websocket_universal.server",
+ ReverseAPIURL: "ws://you_websocket_api.server",
+ ReverseEventURL: "ws://you_websocket_event.server",
ReverseReconnectInterval: 3000,
},
},
+ WebUI: &GoCQWebUI{
+ Enabled: true,
+ Host: "127.0.0.1",
+ WebInput: false,
+ WebUIPort: 9999,
+ },
}
}
-func Load(p string) *JsonConfig {
+//LoadConfig 加载配置文件
+func LoadConfig(p string) *JSONConfig {
if !PathExists(p) {
log.Warnf("尝试加载配置文件 %v 失败: 文件不存在", p)
return nil
}
- c := JsonConfig{}
- err := json.Unmarshal([]byte(ReadAllText(p)), &c)
+ var dat map[string]interface{}
+ var c = JSONConfig{}
+ err := hjson.Unmarshal([]byte(ReadAllText(p)), &dat)
+ if err == nil {
+ b, _ := json.Marshal(dat)
+ err = json.Unmarshal(b, &c)
+ }
if err != nil {
log.Warnf("尝试加载配置文件 %v 时出现错误: %v", p, err)
log.Infoln("原文件已备份")
- os.Rename(p, p+".backup"+strconv.FormatInt(time.Now().Unix(), 10))
+ _ = os.Rename(p, p+".backup"+strconv.FormatInt(time.Now().Unix(), 10))
return nil
}
return &c
}
-func (c *JsonConfig) Save(p string) error {
- data, err := json.MarshalIndent(c, "", "\t")
+//Save 写入配置文件至path
+func (c *JSONConfig) Save(path string) error {
+ data, err := hjson.MarshalWithOptions(c, hjson.EncoderOptions{
+ Eol: "\n",
+ BracesSameLine: true,
+ IndentBy: " ",
+ })
if err != nil {
return err
}
- WriteAllText(p, string(data))
- return nil
+ return WriteAllText(path, string(data))
}
diff --git a/global/filter.go b/global/filter.go
index b2ac4cb..fdcf5fb 100644
--- a/global/filter.go
+++ b/global/filter.go
@@ -1,24 +1,61 @@
package global
import (
- log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
+ "fmt"
"io/ioutil"
"regexp"
"strings"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
)
-type Filter interface {
- Eval(payload gjson.Result) bool
+//MSG 消息Map
+type MSG map[string]interface{}
+
+//Get 尝试从消息Map中取出key为s的值,若不存在则返回MSG{}
+//
+//若所给key对应的值的类型是global.MSG,则返回此值
+//
+//若所给key对应值的类型不是global.MSG,则返回MSG{"__str__": Val}
+func (m MSG) Get(s string) MSG {
+ if v, ok := m[s]; ok {
+ if msg, ok := v.(MSG); ok {
+ return msg
+ }
+ return MSG{"__str__": v} // 用这个名字应该没问题吧
+ }
+ return nil // 不存在为空
}
-type OperationNode struct {
+//String 将消息Map转化为String。若Map存在key "__str__",则返回此key对应的值,否则将输出整张消息Map对应的JSON字符串
+func (m MSG) String() string {
+ if m == nil {
+ return "" // 空 JSON
+ }
+ if str, ok := m["__str__"]; ok {
+ if str == nil {
+ return "" // 空 JSON
+ }
+ return fmt.Sprint(str)
+ }
+ str, _ := json.MarshalToString(m)
+ return str
+}
+
+//Filter 定义了一个消息上报过滤接口
+type Filter interface {
+ Eval(payload MSG) bool
+}
+
+type operationNode struct {
key string
filter Filter
}
+//NotOperator 定义了过滤器中Not操作符
type NotOperator struct {
- operand_ Filter
+ operand Filter
}
func notOperatorConstruct(argument gjson.Result) *NotOperator {
@@ -26,16 +63,18 @@ func notOperatorConstruct(argument gjson.Result) *NotOperator {
panic("the argument of 'not' operator must be an object")
}
op := new(NotOperator)
- op.operand_ = Generate("and", argument)
+ op.operand = Generate("and", argument)
return op
}
-func (notOperator NotOperator) Eval(payload gjson.Result) bool {
- return !(notOperator.operand_).Eval(payload)
+//Eval 对payload执行Not过滤
+func (op *NotOperator) Eval(payload MSG) bool {
+ return !op.operand.Eval(payload)
}
+//AndOperator 定义了过滤器中And操作符
type AndOperator struct {
- operands []OperationNode
+ operands []operationNode
}
func andOperatorConstruct(argument gjson.Result) *AndOperator {
@@ -50,26 +89,27 @@ func andOperatorConstruct(argument gjson.Result) *AndOperator {
// "bar": "baz"
// }
opKey := key.Str[1:]
- op.operands = append(op.operands, OperationNode{"", Generate(opKey, value)})
+ op.operands = append(op.operands, operationNode{"", Generate(opKey, value)})
} else if value.IsObject() {
// is an normal key with an object as the value
// "foo": {
// ".bar": "baz"
// }
opKey := key.String()
- op.operands = append(op.operands, OperationNode{opKey, Generate("and", value)})
+ op.operands = append(op.operands, operationNode{opKey, Generate("and", value)})
} else {
// is an normal key with a non-object as the value
// "foo": "bar"
opKey := key.String()
- op.operands = append(op.operands, OperationNode{opKey, Generate("eq", value)})
+ op.operands = append(op.operands, operationNode{opKey, Generate("eq", value)})
}
return true
})
return op
}
-func (andOperator *AndOperator) Eval(payload gjson.Result) bool {
+//Eval 对payload执行And过滤
+func (andOperator *AndOperator) Eval(payload MSG) bool {
res := true
for _, operand := range andOperator.operands {
@@ -82,13 +122,14 @@ func (andOperator *AndOperator) Eval(payload gjson.Result) bool {
res = res && operand.filter.Eval(val)
}
- if res == false {
+ if !res {
break
}
}
return res
}
+//OrOperator 定义了过滤器中Or操作符
type OrOperator struct {
operands []Filter
}
@@ -105,48 +146,54 @@ func orOperatorConstruct(argument gjson.Result) *OrOperator {
return op
}
-func (orOperator OrOperator) Eval(payload gjson.Result) bool {
+//Eval 对payload执行Or过滤
+func (op *OrOperator) Eval(payload MSG) bool {
res := false
- for _, operand := range orOperator.operands {
+ for _, operand := range op.operands {
res = res || operand.Eval(payload)
-
- if res == true {
+ if res {
break
}
}
return res
}
+//EqualOperator 定义了过滤器中Equal操作符
type EqualOperator struct {
- value gjson.Result
+ operand string
}
func equalOperatorConstruct(argument gjson.Result) *EqualOperator {
op := new(EqualOperator)
- op.value = argument
+ op.operand = argument.String()
return op
}
-func (equalOperator EqualOperator) Eval(payload gjson.Result) bool {
- return payload.String() == equalOperator.value.String()
+//Eval 对payload执行Equal过滤
+func (op *EqualOperator) Eval(payload MSG) bool {
+ return payload.String() == op.operand
}
+//NotEqualOperator 定义了过滤器中NotEqual操作符
type NotEqualOperator struct {
- value gjson.Result
+ operand string
}
func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator {
op := new(NotEqualOperator)
- op.value = argument
+ op.operand = argument.String()
return op
}
-func (notEqualOperator NotEqualOperator) Eval(payload gjson.Result) bool {
- return !(payload.String() == notEqualOperator.value.String())
+//Eval 对payload执行NotEqual过滤
+func (op *NotEqualOperator) Eval(payload MSG) bool {
+ return !(payload.String() == op.operand)
}
+//InOperator 定义了过滤器中In操作符
type InOperator struct {
- operand gjson.Result
+ operandString string
+ operandArray []string
}
func inOperatorConstruct(argument gjson.Result) *InOperator {
@@ -154,22 +201,33 @@ func inOperatorConstruct(argument gjson.Result) *InOperator {
panic("the argument of 'in' operator must be an array or a string")
}
op := new(InOperator)
- op.operand = argument
+ if argument.IsArray() {
+ op.operandArray = []string{}
+ argument.ForEach(func(_, value gjson.Result) bool {
+ op.operandArray = append(op.operandArray, value.String())
+ return true
+ })
+ } else {
+ op.operandString = argument.String()
+ }
return op
}
-func (inOperator InOperator) Eval(payload gjson.Result) bool {
- if inOperator.operand.IsArray() {
- res := false
- inOperator.operand.ForEach(func(key, value gjson.Result) bool {
- res = res || value.String() == payload.String()
- return true
- })
- return res
+//Eval 对payload执行In过滤
+func (op *InOperator) Eval(payload MSG) bool {
+ payloadStr := payload.String()
+ if op.operandArray != nil {
+ for _, value := range op.operandArray {
+ if value == payloadStr {
+ return true
+ }
+ }
+ return false
}
- return strings.Contains(inOperator.operand.String(), payload.String())
+ return strings.Contains(op.operandString, payloadStr)
}
+//ContainsOperator 定义了过滤器中Contains操作符
type ContainsOperator struct {
operand string
}
@@ -183,15 +241,14 @@ func containsOperatorConstruct(argument gjson.Result) *ContainsOperator {
return op
}
-func (containsOperator ContainsOperator) Eval(payload gjson.Result) bool {
- if payload.IsObject() || payload.IsArray() {
- return false
- }
- return strings.Contains(payload.String(), containsOperator.operand)
+//Eval 对payload执行Contains过滤
+func (op *ContainsOperator) Eval(payload MSG) bool {
+ return strings.Contains(payload.String(), op.operand)
}
+//RegexOperator 定义了过滤器中Regex操作符
type RegexOperator struct {
- regex string
+ regex *regexp.Regexp
}
func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
@@ -199,15 +256,17 @@ func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
panic("the argument of 'regex' operator must be a string")
}
op := new(RegexOperator)
- op.regex = argument.String()
+ op.regex = regexp.MustCompile(argument.String())
return op
}
-func (containsOperator RegexOperator) Eval(payload gjson.Result) bool {
- matched, _ := regexp.MatchString(containsOperator.regex, payload.String())
+//Eval 对payload执行RegexO过滤
+func (op *RegexOperator) Eval(payload MSG) bool {
+ matched := op.regex.MatchString(payload.String())
return matched
}
+//Generate 根据给定操作符名opName及操作符参数argument创建一个过滤器实例
func Generate(opName string, argument gjson.Result) Filter {
switch opName {
case "not":
@@ -231,8 +290,10 @@ func Generate(opName string, argument gjson.Result) Filter {
}
}
-var EventFilter = new(Filter)
+//EventFilter 初始化一个nil过滤器
+var EventFilter Filter = nil
+//BootFilter 启动事件过滤器
func BootFilter() {
defer func() {
if e := recover(); e != nil {
@@ -246,6 +307,6 @@ func BootFilter() {
if err != nil {
panic(err)
} else {
- *EventFilter = Generate("and", gjson.ParseBytes(f))
+ EventFilter = Generate("and", gjson.ParseBytes(f))
}
}
diff --git a/global/fs.go b/global/fs.go
index f3dc6b0..a6760a6 100644
--- a/global/fs.go
+++ b/global/fs.go
@@ -1,47 +1,253 @@
package global
import (
+ "bufio"
"bytes"
+ "compress/bzip2"
+ "crypto/md5"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
"io/ioutil"
+ "net"
+ "net/url"
"os"
"path"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "github.com/kardianos/osext"
+
+ "github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus"
)
-var (
- IMAGE_PATH = path.Join("data", "images")
- VOICE_PATH = path.Join("data", "voices")
- VIDEO_PATH = path.Join("data", "videos")
- CACHE_PATH = path.Join("data", "cache")
-
- HEADER_AMR = []byte("#!AMR")
- HEADER_SILK = []byte("\x02#!SILK_V3")
+const (
+ //ImagePath go-cqhttp使用的图片缓存目录
+ ImagePath = "data/images"
+ //ImagePathOld 兼容旧版go-cqhtto使用的图片缓存目录
+ ImagePathOld = "data/image"
+ //VoicePath go-cqhttp使用的语音缓存目录
+ VoicePath = "data/voices"
+ //VoicePathOld 兼容旧版go-cqhtto使用的语音缓存目录
+ VoicePathOld = "data/record"
+ //VideoPath go-cqhttp使用的视频缓存目录
+ VideoPath = "data/videos"
+ //CachePath go-cqhttp使用的缓存目录
+ CachePath = "data/cache"
)
+var (
+ //ErrSyntax Path语法错误时返回的错误
+ ErrSyntax = errors.New("syntax error")
+ //HeaderAmr AMR文件头
+ HeaderAmr = []byte("#!AMR")
+ //HeaderSilk Silkv3文件头
+ HeaderSilk = []byte("\x02#!SILK_V3")
+)
+
+//PathExists 判断给定path是否存在
func PathExists(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
+//ReadAllText 读取给定path对应文件,无法读取时返回空值
func ReadAllText(path string) string {
b, err := ioutil.ReadFile(path)
if err != nil {
+ log.Error(err)
return ""
}
return string(b)
}
-func WriteAllText(path, text string) {
- _ = ioutil.WriteFile(path, []byte(text), 0644)
+//WriteAllText 将给定text写入给定path
+func WriteAllText(path, text string) error {
+ return ioutil.WriteFile(path, []byte(text), 0644)
}
+//Check 检测err是否为nil
func Check(err error) {
if err != nil {
log.Fatalf("遇到错误: %v", err)
}
}
+//IsAMRorSILK 判断给定文件是否为Amr或Silk格式
func IsAMRorSILK(b []byte) bool {
- return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK)
+ return bytes.HasPrefix(b, HeaderAmr) || bytes.HasPrefix(b, HeaderSilk)
+}
+
+//FindFile 从给定的File寻找文件,并返回文件byte数组。File是一个合法的URL。Path为文件寻找位置。
+//对于HTTP/HTTPS形式的URL,Cache为"1"或空时表示启用缓存
+func FindFile(file, cache, PATH string) (data []byte, err error) {
+ data, err = nil, ErrSyntax
+ if strings.HasPrefix(file, "http") || strings.HasPrefix(file, "https") {
+ if cache == "" {
+ cache = "1"
+ }
+ hash := md5.Sum([]byte(file))
+ cacheFile := path.Join(CachePath, hex.EncodeToString(hash[:])+".cache")
+ if PathExists(cacheFile) && cache == "1" {
+ return ioutil.ReadFile(cacheFile)
+ }
+ data, err = GetBytes(file)
+ _ = ioutil.WriteFile(cacheFile, data, 0644)
+ if err != nil {
+ return nil, err
+ }
+ } else if strings.HasPrefix(file, "base64") {
+ data, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(file, "base64://", ""))
+ if err != nil {
+ return nil, err
+ }
+ } else if strings.HasPrefix(file, "file") {
+ var fu *url.URL
+ fu, err = url.Parse(file)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` {
+ fu.Path = fu.Path[1:]
+ }
+ data, err = ioutil.ReadFile(fu.Path)
+ if err != nil {
+ return nil, err
+ }
+ } else if PathExists(path.Join(PATH, file)) {
+ data, err = ioutil.ReadFile(path.Join(PATH, file))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return
+}
+
+//DelFile 删除一个给定path,并返回删除结果
+func DelFile(path string) bool {
+ err := os.Remove(path)
+ if err != nil {
+ // 删除失败
+ log.Error(err)
+ return false
+ }
+ // 删除成功
+ log.Info(path + "删除成功")
+ return true
+}
+
+//ReadAddrFile 从给定path中读取合法的IP地址与端口,每个IP地址以换行符"\n"作为分隔
+func ReadAddrFile(path string) []*net.TCPAddr {
+ d, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil
+ }
+ str := string(d)
+ lines := strings.Split(str, "\n")
+ var ret []*net.TCPAddr
+ for _, l := range lines {
+ ip := strings.Split(strings.TrimSpace(l), ":")
+ if len(ip) == 2 {
+ port, _ := strconv.Atoi(ip[1])
+ ret = append(ret, &net.TCPAddr{IP: net.ParseIP(ip[0]), Port: port})
+ }
+ }
+ return ret
+}
+
+//WriteCounter 写入量计算实例
+type WriteCounter struct {
+ Total uint64
+}
+
+//Write 方法将写入的byte长度追加至写入的总长度Total中
+func (wc *WriteCounter) Write(p []byte) (int, error) {
+ n := len(p)
+ wc.Total += uint64(n)
+ wc.PrintProgress()
+ return n, nil
+}
+
+//PrintProgress 方法将打印当前的总写入量
+func (wc *WriteCounter) PrintProgress() {
+ fmt.Printf("\r%s", strings.Repeat(" ", 35))
+ fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total))
+}
+
+//UpdateFromStream copy form getlantern/go-update
+func UpdateFromStream(updateWith io.Reader) (err error, errRecover error) {
+ updatePath, err := osext.Executable()
+ if err != nil {
+ return
+ }
+ var newBytes []byte
+ // no patch to apply, go on through
+ var fileHeader []byte
+ bufBytes := bufio.NewReader(updateWith)
+ fileHeader, err = bufBytes.Peek(2)
+ if err != nil {
+ return
+ }
+ // The content is always bzip2 compressed except when running test, in
+ // which case is not prefixed with the magic byte sequence for sure.
+ if bytes.Equal([]byte{0x42, 0x5a}, fileHeader) {
+ // Identifying bzip2 files.
+ updateWith = bzip2.NewReader(bufBytes)
+ } else {
+ updateWith = io.Reader(bufBytes)
+ }
+ newBytes, err = ioutil.ReadAll(updateWith)
+ if err != nil {
+ return
+ }
+ // get the directory the executable exists in
+ updateDir := filepath.Dir(updatePath)
+ filename := filepath.Base(updatePath)
+ // Copy the contents of of newbinary to a the new executable file
+ newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
+ fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
+ if err != nil {
+ return
+ }
+ // We won't log this error, because it's always going to happen.
+ defer func() { _ = fp.Close() }()
+ if _, err = io.Copy(fp, bytes.NewReader(newBytes)); err != nil {
+ log.Errorf("Unable to copy data: %v\n", err)
+ }
+
+ // if we don't call fp.Close(), windows won't let us move the new executable
+ // because the file will still be "in use"
+ if err := fp.Close(); err != nil {
+ log.Errorf("Unable to close file: %v\n", err)
+ }
+ // this is where we'll move the executable to so that we can swap in the updated replacement
+ oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
+
+ // delete any existing old exec file - this is necessary on Windows for two reasons:
+ // 1. after a successful update, Windows can't remove the .old file because the process is still running
+ // 2. windows rename operations fail if the destination file already exists
+ _ = os.Remove(oldPath)
+
+ // move the existing executable to a new file in the same directory
+ err = os.Rename(updatePath, oldPath)
+ if err != nil {
+ return
+ }
+
+ // move the new executable in to become the new program
+ err = os.Rename(newPath, updatePath)
+
+ if err != nil {
+ // copy unsuccessful
+ errRecover = os.Rename(oldPath, updatePath)
+ } else {
+ // copy successful, remove the old binary
+ _ = os.Remove(oldPath)
+ }
+ return
}
diff --git a/global/log_hook.go b/global/log_hook.go
new file mode 100644
index 0000000..63865f8
--- /dev/null
+++ b/global/log_hook.go
@@ -0,0 +1,153 @@
+package global
+
+import (
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sync"
+)
+
+type LocalHook struct {
+ lock *sync.Mutex
+ levels []logrus.Level // hook级别
+ formatter logrus.Formatter // 格式
+ path string // 写入path
+ writer io.Writer // io
+}
+
+// ref: logrus/hooks.go. impl Hook interface
+func (hook *LocalHook) Levels() []logrus.Level {
+ if len(hook.levels) == 0 {
+ return logrus.AllLevels
+ }
+ return hook.levels
+}
+
+func (hook *LocalHook) ioWrite(entry *logrus.Entry) error {
+ log, err := hook.formatter.Format(entry)
+ if err != nil {
+ return err
+ }
+
+ _, err = hook.writer.Write(log)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (hook *LocalHook) pathWrite(entry *logrus.Entry) error {
+ dir := filepath.Dir(hook.path)
+ if err := os.MkdirAll(dir, os.ModePerm); err != nil {
+ return err
+ }
+
+ fd, err := os.OpenFile(hook.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+
+ log, err := hook.formatter.Format(entry)
+
+ if err != nil {
+ return err
+ }
+
+ _, err = fd.Write(log)
+ return err
+}
+
+func (hook *LocalHook) Fire(entry *logrus.Entry) error {
+ hook.lock.Lock()
+ defer hook.lock.Unlock()
+
+ if hook.writer != nil {
+ return hook.ioWrite(entry)
+ }
+
+ if hook.path != "" {
+ return hook.pathWrite(entry)
+ }
+
+ return nil
+}
+
+func (hook *LocalHook) SetFormatter(formatter logrus.Formatter) {
+ hook.lock.Lock()
+ defer hook.lock.Unlock()
+
+ if formatter == nil {
+ // 用默认的
+ formatter = &logrus.TextFormatter{DisableColors: true}
+ } else {
+ switch f := formatter.(type) {
+ case *logrus.TextFormatter:
+ textFormatter := f
+ textFormatter.DisableColors = true
+ default:
+ // todo
+ }
+ }
+ logrus.SetFormatter(formatter)
+ hook.formatter = formatter
+}
+
+func (hook *LocalHook) SetWriter(writer io.Writer) {
+ hook.lock.Lock()
+ defer hook.lock.Unlock()
+ hook.writer = writer
+}
+
+func (hook *LocalHook) SetPath(path string) {
+ hook.lock.Lock()
+ defer hook.lock.Unlock()
+ hook.path = path
+}
+
+func NewLocalHook(args interface{}, formatter logrus.Formatter, levels ...logrus.Level) *LocalHook {
+ hook := &LocalHook{
+ lock: new(sync.Mutex),
+ }
+ hook.SetFormatter(formatter)
+ hook.levels = append(hook.levels, levels...)
+
+ switch arg := args.(type) {
+ case string:
+ hook.SetPath(arg)
+ case io.Writer:
+ hook.SetWriter(arg)
+ default:
+ panic(fmt.Sprintf("unsupported type: %v", reflect.TypeOf(args)))
+ }
+
+ return hook
+}
+
+func GetLogLevel(level string) []logrus.Level {
+ switch level {
+ case "trace":
+ return []logrus.Level{logrus.TraceLevel, logrus.DebugLevel,
+ logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel,
+ logrus.FatalLevel, logrus.PanicLevel}
+ case "debug":
+ return []logrus.Level{logrus.DebugLevel, logrus.InfoLevel,
+ logrus.WarnLevel, logrus.ErrorLevel,
+ logrus.FatalLevel, logrus.PanicLevel}
+ case "info":
+ return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
+ logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
+ case "warn":
+ return []logrus.Level{logrus.WarnLevel, logrus.ErrorLevel,
+ logrus.FatalLevel, logrus.PanicLevel}
+ case "error":
+ return []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel,
+ logrus.PanicLevel}
+ default:
+ return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
+ logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
+ }
+}
diff --git a/global/net.go b/global/net.go
index 55f27de..33ebb66 100644
--- a/global/net.go
+++ b/global/net.go
@@ -1,22 +1,60 @@
package global
import (
+ "bufio"
"bytes"
"compress/gzip"
"fmt"
- "github.com/tidwall/gjson"
+ "io"
"io/ioutil"
"net/http"
+ "net/url"
+ "os"
+ "strconv"
"strings"
+ "sync"
+ "time"
+
+ "github.com/guonaihong/gout"
+ "github.com/pkg/errors"
+
+ "github.com/tidwall/gjson"
)
+var (
+ client = &http.Client{
+ Transport: &http.Transport{
+ Proxy: func(request *http.Request) (u *url.URL, e error) {
+ if Proxy == "" {
+ return http.ProxyFromEnvironment(request)
+ }
+ return url.Parse(Proxy)
+ },
+ ForceAttemptHTTP2: true,
+ MaxConnsPerHost: 0,
+ MaxIdleConns: 0,
+ MaxIdleConnsPerHost: 999,
+ },
+ }
+
+ //Proxy 存储Config.proxy_rewrite,用于设置代理
+ Proxy string
+
+ //ErrOverSize 响应主体过大时返回此错误
+ ErrOverSize = errors.New("oversize")
+
+ //UserAgent HTTP请求时使用的UA
+ UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66"
+)
+
+//GetBytes 对给定URL发送Get请求,返回响应主体
func GetBytes(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
- req.Header["User-Agent"] = []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Edg/83.0.478.61"}
- resp, err := http.DefaultClient.Do(req)
+ req.Header["User-Agent"] = []string{UserAgent}
+ resp, err := client.Do(req)
if err != nil {
return nil, err
}
@@ -35,6 +73,215 @@ func GetBytes(url string) ([]byte, error) {
return body, nil
}
+//DownloadFile 将给定URL对应的文件下载至给定Path
+func DownloadFile(url, path string, limit int64, headers map[string]string) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+
+ if _, ok := headers["User-Agent"]; !ok {
+ req.Header["User-Agent"] = []string{UserAgent}
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if limit > 0 && resp.ContentLength > limit {
+ return ErrOverSize
+ }
+ _, err = io.Copy(file, resp.Body)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+//DownloadFileMultiThreading 使用threadCount个线程将给定URL对应的文件下载至给定Path
+func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, headers map[string]string) error {
+ if threadCount < 2 {
+ return DownloadFile(url, path, limit, headers)
+ }
+ type BlockMetaData struct {
+ BeginOffset int64
+ EndOffset int64
+ DownloadedSize int64
+ }
+ var blocks []*BlockMetaData
+ var contentLength int64
+ errUnsupportedMultiThreading := errors.New("unsupported multi-threading")
+ // 初始化分块或直接下载
+ initOrDownload := func() error {
+ copyStream := func(s io.ReadCloser) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ if _, err = io.Copy(file, s); err != nil {
+ return err
+ }
+ return errUnsupportedMultiThreading
+ }
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+
+ for k, v := range headers {
+ req.Header.Set(k, v)
+
+ }
+ if _, ok := headers["User-Agent"]; !ok {
+ req.Header["User-Agent"] = []string{UserAgent}
+ }
+ req.Header.Set("range", "bytes=0-")
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
+ }
+ if resp.StatusCode == 200 {
+ if limit > 0 && resp.ContentLength > limit {
+ return ErrOverSize
+ }
+ return copyStream(resp.Body)
+ }
+ if resp.StatusCode == 206 {
+ contentLength = resp.ContentLength
+ if limit > 0 && resp.ContentLength > limit {
+ return ErrOverSize
+ }
+ blockSize := func() int64 {
+ if contentLength > 1024*1024 {
+ return (contentLength / int64(threadCount)) - 10
+ }
+ return contentLength
+
+ }()
+ if blockSize == contentLength {
+ return copyStream(resp.Body)
+ }
+ var tmp int64
+ for tmp+blockSize < contentLength {
+ blocks = append(blocks, &BlockMetaData{
+ BeginOffset: tmp,
+ EndOffset: tmp + blockSize - 1,
+ })
+ tmp += blockSize
+ }
+ blocks = append(blocks, &BlockMetaData{
+ BeginOffset: tmp,
+ EndOffset: contentLength - 1,
+ })
+ return nil
+ }
+ return errors.New("unknown status code")
+ }
+ // 下载分块
+ downloadBlock := func(block *BlockMetaData) error {
+ req, _ := http.NewRequest("GET", url, nil)
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ _, _ = file.Seek(block.BeginOffset, io.SeekStart)
+ writer := bufio.NewWriter(file)
+ defer writer.Flush()
+
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+
+ if _, ok := headers["User-Agent"]; ok {
+ req.Header["User-Agent"] = []string{UserAgent}
+ }
+ req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10))
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
+ }
+ var buffer = make([]byte, 1024)
+ i, err := resp.Body.Read(buffer)
+ for {
+ if err != nil && err != io.EOF {
+ return err
+ }
+ i64 := int64(len(buffer[:i]))
+ needSize := block.EndOffset + 1 - block.BeginOffset
+ if i64 > needSize {
+ i64 = needSize
+ err = io.EOF
+ }
+ _, e := writer.Write(buffer[:i64])
+ if e != nil {
+ return e
+ }
+ block.BeginOffset += i64
+ block.DownloadedSize += i64
+ if err == io.EOF || block.BeginOffset > block.EndOffset {
+ break
+ }
+ i, err = resp.Body.Read(buffer)
+ }
+ return nil
+ }
+
+ if err := initOrDownload(); err != nil {
+ if err == errUnsupportedMultiThreading {
+ return nil
+ }
+ return err
+ }
+ wg := sync.WaitGroup{}
+ wg.Add(len(blocks))
+ var lastErr error
+ for i := range blocks {
+ go func(b *BlockMetaData) {
+ defer wg.Done()
+ if err := downloadBlock(b); err != nil {
+ lastErr = err
+ }
+ }(blocks[i])
+ }
+ wg.Wait()
+ return lastErr
+}
+
+//GetSliderTicket 通过给定的验证链接raw和id,获取验证结果Ticket
+func GetSliderTicket(raw, id string) (string, error) {
+ var rsp string
+ if err := gout.POST("https://api.shkong.com/gocqhttpapi/task").SetJSON(gout.H{
+ "id": id,
+ "url": raw,
+ }).SetTimeout(time.Second * 35).BindBody(&rsp).Do(); err != nil {
+ return "", err
+ }
+ g := gjson.Parse(rsp)
+ if g.Get("error").Str != "" {
+ return "", errors.New(g.Get("error").Str)
+ }
+ return g.Get("ticket").Str, nil
+}
+
+//QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
func QQMusicSongInfo(id string) (gjson.Result, error) {
d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`)
if err != nil {
@@ -43,6 +290,7 @@ func QQMusicSongInfo(id string) (gjson.Result, error) {
return gjson.ParseBytes(d).Get("songinfo.data"), nil
}
+//NeteaseMusicSongInfo 通过给定id在wdd音乐上查找曲目信息
func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id))
if err != nil {
@@ -50,4 +298,3 @@ func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
}
return gjson.ParseBytes(d).Get("songs.0"), nil
}
-
diff --git a/global/param.go b/global/param.go
index 25e138f..d89d7d2 100644
--- a/global/param.go
+++ b/global/param.go
@@ -1,8 +1,12 @@
package global
import (
- "github.com/tidwall/gjson"
+ "math"
+ "regexp"
+ "strconv"
"strings"
+
+ "github.com/tidwall/gjson"
)
var trueSet = map[string]struct{}{
@@ -17,6 +21,15 @@ var falseSet = map[string]struct{}{
"0": {},
}
+//EnsureBool 判断给定的p是否可表示为合法Bool类型,否则返回defaultVal
+//
+//支持的合法类型有
+//
+//type bool
+//
+//type gjson.True or gjson.False
+//
+//type string "true","yes","1" or "false","no","0" (case insensitive)
func EnsureBool(p interface{}, defaultVal bool) bool {
var str string
if b, ok := p.(bool); ok {
@@ -48,3 +61,47 @@ func EnsureBool(p interface{}, defaultVal bool) bool {
}
return defaultVal
}
+
+// VersionNameCompare 检查版本名是否需要更新, 仅适用于 go-cqhttp 的版本命名规则
+//
+// 例: v0.9.29-fix2 == v0.9.29-fix2 -> false
+//
+// v0.9.29-fix1 < v0.9.29-fix2 -> true
+//
+// v0.9.29-fix2 > v0.9.29-fix1 -> false
+//
+// v0.9.29-fix2 < v0.9.30 -> true
+func VersionNameCompare(current, remote string) bool {
+ sp := regexp.MustCompile(`[0-9]\d*`)
+ cur := sp.FindAllStringSubmatch(current, -1)
+ re := sp.FindAllStringSubmatch(remote, -1)
+ for i := 0; i < int(math.Min(float64(len(cur)), float64(len(re)))); i++ {
+ curSub, _ := strconv.Atoi(cur[i][0])
+ reSub, _ := strconv.Atoi(re[i][0])
+ if curSub < reSub {
+ return true
+ }
+ }
+ return len(cur) < len(re)
+}
+
+//SplitURL 将给定URL字符串分割为两部分,用于URL预处理防止风控
+func SplitURL(s string) []string {
+ reg := regexp.MustCompile(`(?i)[a-z\d][-a-z\d]{0,62}(\.[a-z\d][-a-z\d]{0,62})+\.?`)
+ idx := reg.FindAllStringIndex(s, -1)
+ if len(idx) == 0 {
+ return []string{s}
+ }
+ var result []string
+ last := 0
+ for i := 0; i < len(idx); i++ {
+ if len(idx[i]) != 2 {
+ continue
+ }
+ m := int(math.Abs(float64(idx[i][0]-idx[i][1]))/1.5) + idx[i][0]
+ result = append(result, s[last:m])
+ last = m
+ }
+ result = append(result, s[last:])
+ return result
+}
diff --git a/global/ratelimit.go b/global/ratelimit.go
index 79594be..5983922 100644
--- a/global/ratelimit.go
+++ b/global/ratelimit.go
@@ -2,19 +2,22 @@ package global
import (
"context"
+
"golang.org/x/time/rate"
)
var limiter *rate.Limiter
var limitEnable = false
+//RateLimit 执行API调用速率限制
func RateLimit(ctx context.Context) {
if limitEnable {
_ = limiter.Wait(ctx)
}
}
-func InitLimiter(r float64, b int) {
+//InitLimiter 初始化速率限制器
+func InitLimiter(frequency float64, bucketSize int) {
limitEnable = true
- limiter = rate.NewLimiter(rate.Limit(r), b)
+ limiter = rate.NewLimiter(rate.Limit(frequency), bucketSize)
}
diff --git a/go.mod b/go.mod
index 5a5723f..c324dd5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,31 +1,27 @@
module github.com/Mrs4s/go-cqhttp
-go 1.14
+go 1.15
require (
- github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099
- github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
+ github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e
+ github.com/dustin/go-humanize v1.0.0
+ github.com/gin-contrib/pprof v1.3.0
github.com/gin-gonic/gin v1.6.3
- github.com/go-playground/validator/v10 v10.3.0 // indirect
github.com/gorilla/websocket v1.4.2
- github.com/guonaihong/gout v0.1.2
- github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
- github.com/jonboulle/clockwork v0.2.0 // indirect
- github.com/json-iterator/go v1.1.10 // indirect
+ github.com/guonaihong/gout v0.1.4
+ github.com/hjson/hjson-go v3.1.0+incompatible
+ github.com/jonboulle/clockwork v0.2.2 // indirect
+ github.com/json-iterator/go v1.1.10
+ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
- github.com/lestrrat-go/strftime v1.0.3 // indirect
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
- github.com/modern-go/reflect2 v1.0.1 // indirect
- github.com/pkg/errors v0.9.1 // indirect
- github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
- github.com/sirupsen/logrus v1.6.0
+ github.com/lestrrat-go/strftime v1.0.4 // indirect
+ github.com/pkg/errors v0.9.1
+ github.com/sirupsen/logrus v1.7.0
+ github.com/syndtr/goleveldb v1.0.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
- github.com/tebeka/strftime v0.1.5 // indirect
- github.com/tidwall/gjson v1.6.1
- github.com/xujiajun/nutsdb v0.5.0
+ github.com/tidwall/gjson v1.6.7
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189
- golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
- golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
- golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
- gopkg.in/yaml.v2 v2.3.0 // indirect
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
+ golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
+ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
)
diff --git a/go.sum b/go.sum
index 7d5ff26..21574d6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,37 +1,31 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/Mrs4s/MiraiGo v0.0.0-20200909095006-dde8bded28d1 h1:3cmUqA5RaikLx+59SODlBA7tjORQoh4t1w5CzH5bIH8=
-github.com/Mrs4s/MiraiGo v0.0.0-20200909095006-dde8bded28d1/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo=
-github.com/Mrs4s/MiraiGo v0.0.0-20200909103204-808a63a78efe h1:O2BW87BwpwZDsn7YFHLfRGFGvTS4OUZsG2UiA13OxcQ=
-github.com/Mrs4s/MiraiGo v0.0.0-20200909103204-808a63a78efe/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo=
-github.com/Mrs4s/MiraiGo v0.0.0-20200910010455-37409b1f6b9c h1:bhVr3W0+WTVN+vgZGlxD4iFSV9L3CmUg/lt91h+Ll18=
-github.com/Mrs4s/MiraiGo v0.0.0-20200910010455-37409b1f6b9c/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo=
-github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099 h1:b+Tmo9h5leZmQokdUu8c2xSIRkkSYoP1z8G+zcwwyRY=
-github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo=
-github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
-github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
+github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e h1:SnN+nyRdqN7sULnHUWCofP+Jxs3VJN/y8AlMpcz0nbk=
+github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e/go.mod h1:yhqA0NyKxUf7I/0HR/1OMchveFggX8wde04gqdGrNfU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
+github.com/gin-gonic/gin v1.6.0/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
-github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -43,8 +37,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -54,106 +50,96 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/guonaihong/gout v0.1.2 h1:TR2XCRopGgJdj231IayEoeavgbznFXzzzcZVdT/hG10=
-github.com/guonaihong/gout v0.1.2/go.mod h1:vXvv5Kxr70eM5wrp4F0+t9lnLWmq+YPW2GByll2f/EA=
-github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
-github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/guonaihong/gout v0.1.4 h1:uBBoyztMX9okC27OQxqhn6bZ0ROkGyvnEIHwtp3TM4g=
+github.com/guonaihong/gout v0.1.4/go.mod h1:0rFYAYyzbcxEg11eY2qUbffJs7hHRPeugAnlVYSp8Ic=
+github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
+github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
+github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
-github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible h1:4mNlp+/SvALIPFpbXV3kxNJJno9iKFWGxSDE13Kl66Q=
-github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
-github.com/lestrrat-go/strftime v1.0.3 h1:qqOPU7y+TM8Y803I8fG9c/DyKG3xH/xkng6keC1015Q=
-github.com/lestrrat-go/strftime v1.0.3/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
+github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
-github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA=
-github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig=
-github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws=
-github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
-github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
-github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/gjson v1.6.7 h1:Mb1M9HZCRWEcXQ8ieJo7auYyyiSux6w9XN3AdTpxJrE=
+github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
+github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
+github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
-github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
-github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
-github.com/xujiajun/nutsdb v0.5.0 h1:j/jM3Zw7Chg8WK7bAcKR0Xr7Mal47U1oJAMgySfDn9E=
-github.com/xujiajun/nutsdb v0.5.0/go.mod h1:owdwN0tW084RxEodABLbO7h4Z2s9WiAjZGZFhRh0/1Q=
-github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
-github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if55Fu3HOwbfcaQlJ27p3oeJU2JZqoeT3ITJQk=
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189/go.mod h1:rIrm5geMiBhPQkdfUm8gDFi/WiHneOp1i9KjmJqc+9I=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
-golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
+golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -179,11 +165,11 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
-gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/main.go b/main.go
index b8a8271..327663b 100644
--- a/main.go
+++ b/main.go
@@ -2,177 +2,151 @@ package main
import (
"bufio"
- "bytes"
+ "crypto/aes"
"crypto/md5"
+ "crypto/sha1"
"encoding/base64"
- "encoding/json"
+ "encoding/hex"
"fmt"
- "image"
"io"
"io/ioutil"
+ "net/http"
"os"
+ "os/exec"
"os/signal"
"path"
+ "path/filepath"
+ "runtime"
"strconv"
"strings"
+ "syscall"
"time"
+ "github.com/Mrs4s/go-cqhttp/server"
+ "github.com/guonaihong/gout"
+ "github.com/tidwall/gjson"
+ "golang.org/x/crypto/pbkdf2"
+ "golang.org/x/term"
+
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
- "github.com/Mrs4s/go-cqhttp/server"
-
+ jsoniter "github.com/json-iterator/go"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
- "github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus"
easy "github.com/t-tomalak/logrus-easy-formatter"
- asciiart "github.com/yinghau76/go-ascii-art"
)
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+var conf *global.JSONConfig
+var isFastStart = false
+
func init() {
- log.SetFormatter(&easy.Formatter{
- TimestampFormat: "2006-01-02 15:04:05",
- LogFormat: "[%time%] [%lvl%]: %msg% \n",
- })
- w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotatelogs.WithRotationTime(time.Hour*24))
- if err == nil {
- log.SetOutput(io.MultiWriter(os.Stderr, w))
- }
- if !global.PathExists(global.IMAGE_PATH) {
- if err := os.MkdirAll(global.IMAGE_PATH, 0755); err != nil {
- log.Fatalf("创建图片缓存文件夹失败: %v", err)
- }
- }
- if !global.PathExists(global.VOICE_PATH) {
- if err := os.MkdirAll(global.VOICE_PATH, 0755); err != nil {
- log.Fatalf("创建语音缓存文件夹失败: %v", err)
- }
- }
- if !global.PathExists(global.VIDEO_PATH) {
- if err := os.MkdirAll(global.VIDEO_PATH, 0755); err != nil {
- log.Fatalf("创建视频缓存文件夹失败: %v", err)
- }
- }
- if !global.PathExists(global.CACHE_PATH) {
- if err := os.MkdirAll(global.CACHE_PATH, 0755); err != nil {
- log.Fatalf("创建发送图片缓存文件夹失败: %v", err)
- }
- }
if global.PathExists("cqhttp.json") {
log.Info("发现 cqhttp.json 将在五秒后尝试导入配置,按 Ctrl+C 取消.")
- log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.json 文件.")
+ log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.hjson 文件.")
time.Sleep(time.Second * 5)
- conf := global.CQHttpApiConfig{}
+ conf := global.CQHTTPAPIConfig{}
if err := json.Unmarshal([]byte(global.ReadAllText("cqhttp.json")), &conf); err != nil {
log.Fatalf("读取文件 cqhttp.json 失败: %v", err)
}
goConf := global.DefaultConfig()
goConf.AccessToken = conf.AccessToken
- goConf.HttpConfig.Host = conf.Host
- goConf.HttpConfig.Port = conf.Port
+ goConf.HTTPConfig.Host = conf.Host
+ goConf.HTTPConfig.Port = conf.Port
goConf.WSConfig.Host = conf.WSHost
goConf.WSConfig.Port = conf.WSPort
- if conf.PostUrl != "" {
- goConf.HttpConfig.PostUrls[conf.PostUrl] = conf.Secret
+ if conf.PostURL != "" {
+ goConf.HTTPConfig.PostUrls[conf.PostURL] = conf.Secret
}
if conf.UseWsReverse {
goConf.ReverseServers[0].Enabled = true
- goConf.ReverseServers[0].ReverseUrl = conf.WSReverseUrl
- goConf.ReverseServers[0].ReverseApiUrl = conf.WSReverseApiUrl
- goConf.ReverseServers[0].ReverseEventUrl = conf.WSReverseEventUrl
+ goConf.ReverseServers[0].ReverseURL = conf.WSReverseURL
+ goConf.ReverseServers[0].ReverseAPIURL = conf.WSReverseAPIURL
+ goConf.ReverseServers[0].ReverseEventURL = conf.WSReverseEventURL
goConf.ReverseServers[0].ReverseReconnectInterval = conf.WSReverseReconnectInterval
}
- if err := goConf.Save("config.json"); err != nil {
- log.Fatalf("保存 config.json 时出现错误: %v", err)
+ if err := goConf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
}
_ = os.Remove("cqhttp.json")
}
+
+ conf = getConfig()
+ if conf == nil {
+ os.Exit(1)
+ }
+
+ logFormatter := &easy.Formatter{
+ TimestampFormat: "2006-01-02 15:04:05",
+ LogFormat: "[%time%] [%lvl%]: %msg% \n",
+ }
+ w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotatelogs.WithRotationTime(time.Hour*24))
+ if err != nil {
+ log.Errorf("rotatelogs init err: %v", err)
+ panic(err)
+ }
+
+ // 在debug模式下,将在标准输出中打印当前执行行数
+ if conf.Debug {
+ log.SetReportCaller(true)
+ }
+
+ log.AddHook(global.NewLocalHook(w, logFormatter, global.GetLogLevel(conf.LogLevel)...))
+
+ if !global.PathExists(global.ImagePath) {
+ if err := os.MkdirAll(global.ImagePath, 0755); err != nil {
+ log.Fatalf("创建图片缓存文件夹失败: %v", err)
+ }
+ }
+ if !global.PathExists(global.VoicePath) {
+ if err := os.MkdirAll(global.VoicePath, 0755); err != nil {
+ log.Fatalf("创建语音缓存文件夹失败: %v", err)
+ }
+ }
+ if !global.PathExists(global.VideoPath) {
+ if err := os.MkdirAll(global.VideoPath, 0755); err != nil {
+ log.Fatalf("创建视频缓存文件夹失败: %v", err)
+ }
+ }
+ if !global.PathExists(global.CachePath) {
+ if err := os.MkdirAll(global.CachePath, 0755); err != nil {
+ log.Fatalf("创建发送图片缓存文件夹失败: %v", err)
+ }
+ }
}
func main() {
- console := bufio.NewReader(os.Stdin)
- var conf *global.JsonConfig
- if global.PathExists("config.json") || os.Getenv("UIN") == "" {
- conf = global.Load("config.json")
- } else if os.Getenv("UIN") != "" {
- log.Infof("将从环境变量加载配置.")
- uin, _ := strconv.ParseInt(os.Getenv("UIN"), 10, 64)
- pwd := os.Getenv("PASS")
- post := os.Getenv("HTTP_POST")
- conf = &global.JsonConfig{
- Uin: uin,
- Password: pwd,
- HttpConfig: &global.GoCQHttpConfig{
- Enabled: true,
- Host: "0.0.0.0",
- Port: 5700,
- PostUrls: map[string]string{},
- },
- WSConfig: &global.GoCQWebsocketConfig{
- Enabled: true,
- Host: "0.0.0.0",
- Port: 6700,
- },
- PostMessageFormat: "string",
- Debug: os.Getenv("DEBUG") == "true",
- }
- if post != "" {
- conf.HttpConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
+
+ var byteKey []byte
+ arg := os.Args
+ if len(arg) > 1 {
+ for i := range arg {
+ switch arg[i] {
+ case "update":
+ if len(arg) > i+1 {
+ selfUpdate(arg[i+1])
+ } else {
+ selfUpdate("")
+ }
+ case "key":
+ if len(arg) > i+1 {
+ b := []byte(arg[i+1])
+ byteKey = b
+ }
+ case "faststart":
+ isFastStart = true
+ }
}
}
- if conf == nil {
- err := global.DefaultConfig().Save("config.json")
- if err != nil {
- log.Fatalf("创建默认配置文件时出现错误: %v", err)
- return
- }
- log.Infof("默认配置文件已生成, 请编辑 config.json 后重启程序.")
- time.Sleep(time.Second * 5)
- return
- }
+
if conf.Uin == 0 || (conf.Password == "" && conf.PasswordEncrypted == "") {
- log.Warnf("请修改 config.json 以添加账号密码.")
- time.Sleep(time.Second * 5)
- return
- }
-
- // log classified by level
- // Collect all records up to the specified level (default level: warn)
- logLevel := conf.LogLevel
- if logLevel != "" {
- date := time.Now().Format("2006-01-02")
- var logPathMap lfshook.PathMap
- switch conf.LogLevel {
- case "warn":
- logPathMap = lfshook.PathMap{
- log.WarnLevel: path.Join("logs", date+"-warn.log"),
- log.ErrorLevel: path.Join("logs", date+"-warn.log"),
- log.FatalLevel: path.Join("logs", date+"-warn.log"),
- log.PanicLevel: path.Join("logs", date+"-warn.log"),
- }
- case "error":
- logPathMap = lfshook.PathMap{
- log.ErrorLevel: path.Join("logs", date+"-error.log"),
- log.FatalLevel: path.Join("logs", date+"-error.log"),
- log.PanicLevel: path.Join("logs", date+"-error.log"),
- }
- default:
- logPathMap = lfshook.PathMap{
- log.WarnLevel: path.Join("logs", date+"-warn.log"),
- log.ErrorLevel: path.Join("logs", date+"-warn.log"),
- log.FatalLevel: path.Join("logs", date+"-warn.log"),
- log.PanicLevel: path.Join("logs", date+"-warn.log"),
- }
+ log.Warnf("请修改 config.hjson 以添加账号密码.")
+ if !isFastStart {
+ time.Sleep(time.Second * 5)
}
-
- log.AddHook(lfshook.NewHook(
- logPathMap,
- &easy.Formatter{
- TimestampFormat: "2006-01-02 15:04:05",
- LogFormat: "[%time%] [%lvl%]: %msg% \n",
- },
- ))
+ return
}
log.Info("当前版本:", coolq.Version)
@@ -180,7 +154,12 @@ func main() {
log.SetLevel(log.DebugLevel)
log.Warnf("已开启Debug模式.")
log.Debugf("开发交流群: 192548878")
+ server.Debug = true
+ if conf.WebUI == nil || !conf.WebUI.Enabled {
+ log.Warnf("警告: 在Debug模式下未启用WebUi服务, 将无法进行性能分析.")
+ }
}
+ log.Info("用户交流群: 721829413")
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice()
@@ -194,26 +173,70 @@ func main() {
}
if conf.EncryptPassword && conf.PasswordEncrypted == "" {
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
- strKey, _ := console.ReadString('\n')
- key := md5.Sum([]byte(strKey))
- if encrypted := EncryptPwd(conf.Password, key[:]); encrypted != "" {
- conf.Password = ""
- conf.PasswordEncrypted = encrypted
- _ = conf.Save("config.json")
- } else {
- log.Warnf("加密时出现问题.")
- }
+ byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
+ global.PasswordHash = md5.Sum([]byte(conf.Password))
+ conf.Password = ""
+ conf.PasswordEncrypted = "AES:" + PasswordHashEncrypt(global.PasswordHash[:], byteKey)
+ _ = conf.Save("config.hjson")
}
if conf.PasswordEncrypted != "" {
- log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
- strKey, _ := console.ReadString('\n')
- key := md5.Sum([]byte(strKey))
- conf.Password = DecryptPwd(conf.PasswordEncrypted, key[:])
+ if len(byteKey) == 0 {
+ log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
+ cancel := make(chan struct{}, 1)
+ go func() {
+ select {
+ case <-cancel:
+ return
+ case <-time.After(time.Second * 45):
+ log.Infof("解密key输入超时")
+ time.Sleep(3 * time.Second)
+ os.Exit(0)
+ }
+ }()
+ byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
+ cancel <- struct{}{}
+ } else {
+ log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
+ }
+
+ //升级客户端密码加密方案,MD5+TEA 加密密码 -> PBKDF2+AES 加密 MD5
+ //升级后的 PasswordEncrypted 字符串以"AES:"开始,其后为 Hex 编码的16字节加密 MD5
+ if !strings.HasPrefix(conf.PasswordEncrypted, "AES:") {
+ password := OldPasswordDecrypt(conf.PasswordEncrypted, byteKey)
+ passwordHash := md5.Sum([]byte(password))
+ newPasswordHash := PasswordHashEncrypt(passwordHash[:], byteKey)
+ conf.PasswordEncrypted = "AES:" + newPasswordHash
+ _ = conf.Save("config.hjson")
+ log.Debug("密码加密方案升级完成")
+ }
+
+ ph, err := PasswordHashDecrypt(conf.PasswordEncrypted[4:], byteKey)
+ if err != nil {
+ log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
+ }
+ copy(global.PasswordHash[:], ph)
+ } else {
+ global.PasswordHash = md5.Sum([]byte(conf.Password))
+ }
+ if !isFastStart {
+ log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
+ time.Sleep(time.Second * 5)
}
- log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
- time.Sleep(time.Second * 5)
log.Info("开始尝试登录并同步消息...")
- cli := client.NewClient(conf.Uin, conf.Password)
+ log.Infof("使用协议: %v", func() string {
+ switch client.SystemDeviceInfo.Protocol {
+ case client.IPad:
+ return "iPad"
+ case client.AndroidPhone:
+ return "Android Phone"
+ case client.AndroidWatch:
+ return "Android Watch"
+ case client.MacOS:
+ return "MacOS"
+ }
+ return "未知"
+ }())
+ cli := client.NewClientMd5(conf.Uin, global.PasswordHash)
cli.OnLog(func(c *client.QQClient, e *client.LogEvent) {
switch e.Type {
case "INFO":
@@ -224,131 +247,272 @@ func main() {
log.Debug("Protocol -> " + e.Message)
}
})
- rsp, err := cli.Login()
- for {
- global.Check(err)
- if !rsp.Success {
- switch rsp.Error {
- case client.NeedCaptcha:
- _ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644)
- img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage))
- fmt.Println(asciiart.New("image", img).Art)
- log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)")
- text, _ := console.ReadString('\n')
- rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign)
- continue
- case client.UnsafeDeviceError:
- log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
- log.Infof(" 按 Enter 继续....")
- _, _ = console.ReadString('\n')
- return
- case client.OtherLoginError, client.UnknownLoginError:
- log.Fatalf("登录失败: %v", rsp.ErrorMessage)
- }
+ if global.PathExists("address.txt") {
+ log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
+ addr := global.ReadAddrFile("address.txt")
+ if len(addr) > 0 {
+ cli.SetCustomServer(addr)
}
- break
+ log.Infof("读取到 %v 个自定义地址.", len(addr))
}
- log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
- time.Sleep(time.Second)
- log.Info("开始加载好友列表...")
- global.Check(cli.ReloadFriendList())
- log.Infof("共加载 %v 个好友.", len(cli.FriendList))
- log.Infof("开始加载群列表...")
- global.Check(cli.ReloadGroupList())
- log.Infof("共加载 %v 个群.", len(cli.GroupList))
- b := coolq.NewQQBot(cli, conf)
- if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" {
- log.Warnf("post_message_format 配置错误, 将自动使用 string")
- coolq.SetMessageFormat("string")
- } else {
- coolq.SetMessageFormat(conf.PostMessageFormat)
- }
- if conf.RateLimit.Enabled {
- global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize)
- }
- log.Info("正在加载事件过滤器.")
- global.BootFilter()
- coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode
- coolq.ForceFragmented = conf.ForceFragmented
- if conf.HttpConfig != nil && conf.HttpConfig.Enabled {
- server.HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, b)
- for k, v := range conf.HttpConfig.PostUrls {
- server.NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, b)
+ cli.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
+ if !conf.UseSSOAddress {
+ log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
+ return false
}
- }
- if conf.WSConfig != nil && conf.WSConfig.Enabled {
- server.WebsocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, b)
- }
- for _, rc := range conf.ReverseServers {
- server.NewWebsocketClient(rc, conf.AccessToken, b).Run()
- }
- log.Info("资源初始化完成, 开始处理信息.")
- log.Info("アトリは、高性能ですから!")
- cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) {
- if conf.ReLogin.Enabled {
- var times uint = 1
- for {
-
- if conf.ReLogin.MaxReloginTimes == 0 {
- } else if times > conf.ReLogin.MaxReloginTimes {
- break
- }
- log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v",
- e.Message, conf.ReLogin.ReLoginDelay, times)
- times++
- time.Sleep(time.Second * time.Duration(conf.ReLogin.ReLoginDelay))
- rsp, err := cli.Login()
- if err != nil {
- log.Errorf("重连失败: %v", err)
- continue
- }
- if !rsp.Success {
- switch rsp.Error {
- case client.NeedCaptcha:
- log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)")
- case client.UnsafeDeviceError:
- log.Fatalf("重连失败: 设备锁")
- default:
- log.Errorf("重连失败: %v", rsp.ErrorMessage)
- continue
- }
- }
- log.Info("重连成功")
- return
-
- }
- log.Fatal("重连失败: 重连次数达到设置的上限值")
- }
- b.Release()
- log.Fatalf("Bot已离线:%v", e.Message)
+ log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
+ return true
})
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt, os.Kill)
- <-c
- b.Release()
-}
-
-func EncryptPwd(pwd string, key []byte) string {
- tea := binary.NewTeaCipher(key)
- if tea == nil {
- return ""
+ if conf.WebUI == nil {
+ conf.WebUI = &global.GoCQWebUI{
+ Enabled: true,
+ WebInput: false,
+ Host: "0.0.0.0",
+ WebUIPort: 9999,
+ }
+ }
+ if conf.WebUI.WebUIPort <= 0 {
+ conf.WebUI.WebUIPort = 9999
+ }
+ if conf.WebUI.Host == "" {
+ conf.WebUI.Host = "127.0.0.1"
+ }
+ global.Proxy = conf.ProxyRewrite
+ b := server.WebServer.Run(fmt.Sprintf("%s:%d", conf.WebUI.Host, conf.WebUI.WebUIPort), cli)
+ c := server.Console
+ r := server.Restart
+ go checkUpdate()
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+ select {
+ case <-c:
+ b.Release()
+ case <-r:
+ log.Info("正在重启中...")
+ defer b.Release()
+ restart(arg)
}
- return base64.StdEncoding.EncodeToString(tea.Encrypt([]byte(pwd)))
}
-func DecryptPwd(ePwd string, key []byte) string {
+// PasswordHashEncrypt 使用key加密给定passwordHash
+func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
+ if len(passwordHash) != 16 {
+ panic("密码加密参数错误")
+ }
+
+ key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
+
+ cipher, _ := aes.NewCipher(key)
+ result := make([]byte, 16)
+ cipher.Encrypt(result, passwordHash)
+
+ return hex.EncodeToString(result)
+}
+
+// PasswordHashDecrypt 使用key解密给定passwordHash
+func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, error) {
+ ciphertext, err := hex.DecodeString(encryptedPasswordHash)
+ if err != nil {
+ return nil, err
+ }
+
+ key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
+
+ cipher, _ := aes.NewCipher(key)
+ result := make([]byte, 16)
+ cipher.Decrypt(result, ciphertext)
+
+ return result, nil
+}
+
+// OldPasswordDecrypt 使用key解密老password,仅供兼容使用
+func OldPasswordDecrypt(encryptedPassword string, key []byte) string {
defer func() {
if pan := recover(); pan != nil {
log.Fatalf("密码解密失败: %v", pan)
}
}()
- encrypted, err := base64.StdEncoding.DecodeString(ePwd)
+ encKey := md5.Sum(key)
+ encrypted, err := base64.StdEncoding.DecodeString(encryptedPassword)
if err != nil {
panic(err)
}
- tea := binary.NewTeaCipher(key)
+ tea := binary.NewTeaCipher(encKey[:])
if tea == nil {
panic("密钥错误")
}
return string(tea.Decrypt(encrypted))
}
+
+func checkUpdate() {
+ log.Infof("正在检查更新.")
+ if coolq.Version == "unknown" {
+ log.Warnf("检查更新失败: 使用的 Actions 测试版或自编译版本.")
+ return
+ }
+ var res string
+ if err := gout.GET("https://api.github.com/repos/Mrs4s/go-cqhttp/releases").BindBody(&res).Do(); err != nil {
+ log.Warnf("检查更新失败: %v", err)
+ return
+ }
+ detail := gjson.Parse(res)
+ if len(detail.Array()) < 1 {
+ return
+ }
+ info := detail.Array()[0]
+ if global.VersionNameCompare(coolq.Version, info.Get("tag_name").Str) {
+ log.Infof("当前有更新的 go-cqhttp 可供更新, 请前往 https://github.com/Mrs4s/go-cqhttp/releases 下载.")
+ log.Infof("当前版本: %v 最新版本: %v", coolq.Version, info.Get("tag_name").Str)
+ return
+ }
+ log.Infof("检查更新完成. 当前已运行最新版本.")
+}
+
+func selfUpdate(imageURL string) {
+ console := bufio.NewReader(os.Stdin)
+ readLine := func() (str string) {
+ str, _ = console.ReadString('\n')
+ return
+ }
+ log.Infof("正在检查更新.")
+ var res string
+ if err := gout.GET("https://api.github.com/repos/Mrs4s/go-cqhttp/releases").BindBody(&res).Do(); err != nil {
+ log.Warnf("检查更新失败: %v", err)
+ return
+ }
+ detail := gjson.Parse(res)
+ if len(detail.Array()) < 1 {
+ return
+ }
+ info := detail.Array()[0]
+ version := info.Get("tag_name").Str
+ if coolq.Version != version {
+ log.Info("当前最新版本为 ", version)
+ log.Warn("是否更新(y/N): ")
+ r := strings.TrimSpace(readLine())
+
+ doUpdate := func() {
+ log.Info("正在更新,请稍等...")
+ url := fmt.Sprintf(
+ "%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp-%v-%v-%v",
+ func() string {
+ if imageURL != "" {
+ return imageURL
+ }
+ return "https://github.com"
+ }(),
+ version,
+ version,
+ runtime.GOOS,
+ runtime.GOARCH,
+ )
+ if runtime.GOOS == "windows" {
+ url = url + ".exe"
+ }
+ resp, err := http.Get(url)
+ if err != nil {
+ fmt.Println(err)
+ log.Error("更新失败!")
+ return
+ }
+ wc := global.WriteCounter{}
+ err, _ = global.UpdateFromStream(io.TeeReader(resp.Body, &wc))
+ fmt.Println()
+ if err != nil {
+ log.Error("更新失败!")
+ return
+ }
+ log.Info("更新完成!")
+ }
+
+ if r == "y" || r == "Y" {
+ doUpdate()
+ } else {
+ log.Warn("已取消更新!")
+ }
+ } else {
+ log.Info("当前版本已经是最新版本!")
+ }
+ log.Info("按 Enter 继续....")
+ readLine()
+ os.Exit(0)
+}
+
+func restart(Args []string) {
+ var cmd *exec.Cmd
+ if runtime.GOOS == "windows" {
+ file, err := exec.LookPath(Args[0])
+ if err != nil {
+ log.Errorf("重启失败:%s", err.Error())
+ return
+ }
+ path, err := filepath.Abs(file)
+ if err != nil {
+ log.Errorf("重启失败:%s", err.Error())
+ }
+ Args = append([]string{"/c", "start ", path, "faststart"}, Args[1:]...)
+ cmd = &exec.Cmd{
+ Path: "cmd.exe",
+ Args: Args,
+ Stderr: os.Stderr,
+ Stdout: os.Stdout,
+ }
+ } else {
+ Args = append(Args, "faststart")
+ cmd = &exec.Cmd{
+ Path: Args[0],
+ Args: Args,
+ Stderr: os.Stderr,
+ Stdout: os.Stdout,
+ }
+ }
+ _ = cmd.Start()
+}
+
+func getConfig() *global.JSONConfig {
+ var conf *global.JSONConfig
+ if global.PathExists("config.json") {
+ conf = global.LoadConfig("config.json")
+ _ = conf.Save("config.hjson")
+ _ = os.Remove("config.json")
+ } else if os.Getenv("UIN") != "" {
+ log.Infof("将从环境变量加载配置.")
+ uin, _ := strconv.ParseInt(os.Getenv("UIN"), 10, 64)
+ pwd := os.Getenv("PASS")
+ post := os.Getenv("HTTP_POST")
+ conf = &global.JSONConfig{
+ Uin: uin,
+ Password: pwd,
+ HTTPConfig: &global.GoCQHTTPConfig{
+ Enabled: true,
+ Host: "0.0.0.0",
+ Port: 5700,
+ PostUrls: map[string]string{},
+ },
+ WSConfig: &global.GoCQWebSocketConfig{
+ Enabled: true,
+ Host: "0.0.0.0",
+ Port: 6700,
+ },
+ PostMessageFormat: "string",
+ Debug: os.Getenv("DEBUG") == "true",
+ }
+ if post != "" {
+ conf.HTTPConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
+ }
+ } else {
+ conf = global.LoadConfig("config.hjson")
+ }
+ if conf == nil {
+ err := global.WriteAllText("config.hjson", global.DefaultConfigWithComments)
+ if err != nil {
+ log.Fatalf("创建默认配置文件时出现错误: %v", err)
+ return nil
+ }
+ log.Infof("默认配置文件已生成, 请编辑 config.hjson 后重启程序.")
+ if !isFastStart {
+ time.Sleep(time.Second * 5)
+ }
+ return nil
+ }
+ return conf
+}
diff --git a/server/apiAdmin.go b/server/apiAdmin.go
new file mode 100644
index 0000000..c871e3d
--- /dev/null
+++ b/server/apiAdmin.go
@@ -0,0 +1,630 @@
+package server
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "image"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/Mrs4s/MiraiGo/utils"
+ "github.com/gin-contrib/pprof"
+
+ "github.com/Mrs4s/MiraiGo/client"
+ "github.com/Mrs4s/go-cqhttp/coolq"
+ "github.com/Mrs4s/go-cqhttp/global"
+ "github.com/gin-gonic/gin"
+ jsoniter "github.com/json-iterator/go"
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
+ asciiart "github.com/yinghau76/go-ascii-art"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+var WebInput = make(chan string, 1) //长度1,用于阻塞
+
+var Console = make(chan os.Signal, 1)
+
+var Restart = make(chan struct{}, 1)
+
+var JSONConfig *global.JSONConfig
+
+type webServer struct {
+ engine *gin.Engine
+ bot *coolq.CQBot
+ Cli *client.QQClient
+ Conf *global.JSONConfig //old config
+ Console *bufio.Reader
+}
+
+var WebServer = &webServer{}
+
+// admin 子站的 路由映射
+var HttpuriAdmin = map[string]func(s *webServer, c *gin.Context){
+ "do_restart": AdminDoRestart, //热重启
+ "do_process_restart": AdminProcessRestart, //进程重启
+ "get_web_write": AdminWebWrite, //获取是否验证码输入
+ "do_web_write": AdminDoWebWrite, //web上进行输入操作
+ "do_restart_docker": AdminDoRestartDocker, //直接停止(依赖supervisord/docker)重新拉起
+ "do_config_base": AdminDoConfigBase, //修改config.json中的基础部分
+ "do_config_http": AdminDoConfigHttp, //修改config.json的http部分
+ "do_config_ws": AdminDoConfigWs, //修改config.json的正向ws部分
+ "do_config_reverse": AdminDoConfigReverse, //修改config.json 中的反向ws部分
+ "do_config_json": AdminDoConfigJson, //直接修改 config.json配置
+ "get_config_json": AdminGetConfigJson, //拉取 当前的config.json配置
+}
+
+func Failed(code int, msg string) coolq.MSG {
+ return coolq.MSG{"data": nil, "retcode": code, "status": "failed", "msg": msg}
+}
+
+func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot {
+ s.Cli = cli
+ s.Conf = GetConf()
+ JSONConfig = s.Conf
+ gin.SetMode(gin.ReleaseMode)
+ s.engine = gin.New()
+
+ s.engine.Use(AuthMiddleWare())
+
+ //通用路由
+ s.engine.Any("/admin/:action", s.admin)
+
+ go func() {
+ //开启端口监听
+ if s.Conf.WebUI != nil && s.Conf.WebUI.Enabled {
+ if Debug {
+ pprof.Register(s.engine)
+ log.Debugf("pprof 性能分析服务已启动在 http://%v/debug/pprof, 如果有任何性能问题请下载报告并提交给开发者", addr)
+ time.Sleep(time.Second * 3)
+ }
+ log.Infof("Admin API 服务器已启动: %v", addr)
+ err := s.engine.Run(addr)
+ if err != nil {
+ log.Error(err)
+ log.Infof("请检查端口是否被占用.")
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+ <-c
+ os.Exit(1)
+ }
+ } else {
+ //关闭端口监听
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+ <-c
+ os.Exit(1)
+ }
+ }()
+ s.Dologin()
+ s.UpServer()
+ b := s.bot //外部引入 bot对象,用于操作bot
+ return b
+}
+
+func (s *webServer) Dologin() {
+ s.Console = bufio.NewReader(os.Stdin)
+ readLine := func() (str string) {
+ str, _ = s.Console.ReadString('\n')
+ str = strings.TrimSpace(str)
+ return
+ }
+ conf := GetConf()
+ cli := s.Cli
+ cli.AllowSlider = true
+ rsp, err := cli.Login()
+ count := 0
+ for {
+ global.Check(err)
+ var text string
+ if !rsp.Success {
+ switch rsp.Error {
+ case client.SliderNeededError:
+ log.Warnf("登录需要滑条验证码, 请选择解决方案: ")
+ log.Warnf("1. 自行抓包. (推荐)")
+ log.Warnf("2. 使用Cef自动处理.")
+ log.Warnf("3. 不提交滑块并继续.(可能会导致上网环境异常错误)")
+ log.Warnf("详细信息请参考文档 -> https://github.com/Mrs4s/go-cqhttp/blob/master/docs/slider.md <-")
+ log.Warn("请输入(1 - 3): ")
+ text = readLine()
+ if strings.Contains(text, "1") {
+ log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", rsp.VerifyUrl)
+ log.Warn("请输入Ticket: (Enter 提交)")
+ text = readLine()
+ rsp, err = cli.SubmitTicket(strings.TrimSpace(text))
+ continue
+ }
+ if strings.Contains(text, "3") {
+ cli.AllowSlider = false
+ cli.Disconnect()
+ rsp, err = cli.Login()
+ continue
+ }
+ id := utils.RandomStringRange(6, "0123456789")
+ log.Warnf("滑块ID为 %v 请在30S内处理.", id)
+ ticket, err := global.GetSliderTicket(rsp.VerifyUrl, id)
+ if err != nil {
+ log.Warnf("错误: " + err.Error())
+ os.Exit(0)
+ }
+ rsp, err = cli.SubmitTicket(ticket)
+ if err != nil {
+ log.Warnf("错误: " + err.Error())
+ os.Exit(0)
+ }
+ continue
+ case client.NeedCaptcha:
+ _ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644)
+ img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage))
+ fmt.Println(asciiart.New("image", img).Art)
+ if conf.WebUI != nil && conf.WebUI.WebInput {
+ log.Warnf("请输入验证码 (captcha.jpg): (http://%s:%d/admin/do_web_write 输入)", conf.WebUI.Host, conf.WebUI.WebUIPort)
+ text = <-WebInput
+ } else {
+ log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)")
+ text = readLine()
+ }
+ rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign)
+ global.DelFile("captcha.jpg")
+ continue
+ case client.SMSNeededError:
+ log.Warnf("账号已开启设备锁, 按下 Enter 向手机 %v 发送短信验证码.", rsp.SMSPhone)
+ readLine()
+ if !cli.RequestSMS() {
+ log.Warnf("发送验证码失败,可能是请求过于频繁.")
+ time.Sleep(time.Second * 5)
+ os.Exit(0)
+ }
+ log.Warn("请输入短信验证码: (Enter 提交)")
+ text = readLine()
+ rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", ""))
+ continue
+ case client.SMSOrVerifyNeededError:
+ log.Warnf("账号已开启设备锁,请选择验证方式:")
+ log.Warnf("1. 向手机 %v 发送短信验证码", rsp.SMSPhone)
+ log.Warnf("2. 使用手机QQ扫码验证.")
+ log.Warn("请输入(1 - 2): ")
+ text = readLine()
+ if strings.Contains(text, "1") {
+ if !cli.RequestSMS() {
+ log.Warnf("发送验证码失败,可能是请求过于频繁.")
+ time.Sleep(time.Second * 5)
+ os.Exit(0)
+ }
+ log.Warn("请输入短信验证码: (Enter 提交)")
+ text = readLine()
+ rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", ""))
+ continue
+ }
+ log.Warnf("请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
+ log.Infof("按 Enter 继续....")
+ readLine()
+ os.Exit(0)
+ return
+ case client.UnsafeDeviceError:
+ log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
+ if conf.WebUI != nil && conf.WebUI.WebInput {
+ log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUI.Host, conf.WebUI.WebUIPort)
+ text = <-WebInput
+ } else {
+ log.Infof("按 Enter 继续....")
+ readLine()
+ }
+ log.Info(text)
+ os.Exit(0)
+ return
+ case client.OtherLoginError, client.UnknownLoginError:
+ msg := rsp.ErrorMessage
+ if strings.Contains(msg, "版本") {
+ msg = "密码错误或账号被冻结"
+ }
+ if strings.Contains(msg, "上网环境") && count < 5 {
+ cli.Disconnect()
+ rsp, err = cli.Login()
+ count++
+ log.Warnf("错误: 当前上网环境异常. 将更换服务器并重试.")
+ time.Sleep(time.Second)
+ continue
+ }
+ log.Warnf("登录失败: %v", msg)
+ log.Infof("按 Enter 继续....")
+ readLine()
+ os.Exit(0)
+ return
+ }
+ }
+ break
+ }
+ log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
+ time.Sleep(time.Second)
+ log.Info("开始加载好友列表...")
+ global.Check(cli.ReloadFriendList())
+ log.Infof("共加载 %v 个好友.", len(cli.FriendList))
+ log.Infof("开始加载群列表...")
+ global.Check(cli.ReloadGroupList())
+ log.Infof("共加载 %v 个群.", len(cli.GroupList))
+ s.bot = coolq.NewQQBot(cli, conf)
+ if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" {
+ log.Warnf("post_message_format 配置错误, 将自动使用 string")
+ coolq.SetMessageFormat("string")
+ } else {
+ coolq.SetMessageFormat(conf.PostMessageFormat)
+ }
+ if conf.RateLimit.Enabled {
+ global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize)
+ }
+ log.Info("正在加载事件过滤器.")
+ global.BootFilter()
+ global.InitCodec()
+ coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode
+ coolq.SplitUrl = conf.FixURL
+ coolq.ForceFragmented = conf.ForceFragmented
+ log.Info("资源初始化完成, 开始处理信息.")
+ log.Info("アトリは、高性能ですから!")
+ cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) {
+ if conf.ReLogin.Enabled {
+ conf.ReLogin.Enabled = false
+ defer func() { conf.ReLogin.Enabled = true }()
+ var times uint = 1
+ for {
+ if cli.Online {
+ log.Warn("Bot已登录")
+ return
+ }
+ if times > conf.ReLogin.MaxReloginTimes && conf.ReLogin.MaxReloginTimes != 0 {
+ break
+ }
+ log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v",
+ e.Message, conf.ReLogin.ReLoginDelay, times)
+ times++
+ time.Sleep(time.Second * time.Duration(conf.ReLogin.ReLoginDelay))
+ rsp, err := cli.Login()
+ if err != nil {
+ log.Errorf("重连失败: %v", err)
+ cli.Disconnect()
+ continue
+ }
+ if !rsp.Success {
+ switch rsp.Error {
+ case client.NeedCaptcha:
+ log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)")
+ case client.UnsafeDeviceError:
+ log.Fatalf("重连失败: 设备锁")
+ default:
+ log.Errorf("重连失败: %v", rsp.ErrorMessage)
+ if strings.Contains(rsp.ErrorMessage, "冻结") {
+ log.Fatalf("账号被冻结, 放弃重连")
+ }
+ cli.Disconnect()
+ continue
+ }
+ }
+ log.Info("重连成功")
+ return
+ }
+ log.Fatal("重连失败: 重连次数达到设置的上限值")
+ }
+ s.bot.Release()
+ log.Fatalf("Bot已离线:%v", e.Message)
+ })
+}
+
+func (s *webServer) admin(c *gin.Context) {
+ action := c.Param("action")
+ log.Debugf("WebServer接收到cgi调用: %v", action)
+ if f, ok := HttpuriAdmin[action]; ok {
+ f(s, c)
+ } else {
+ c.JSON(200, coolq.Failed(404))
+ }
+}
+
+// 获取当前配置文件信息
+func GetConf() *global.JSONConfig {
+ if JSONConfig != nil {
+ return JSONConfig
+ }
+ conf := global.LoadConfig("config.hjson")
+ return conf
+}
+
+// admin 控制器 登录验证
+func AuthMiddleWare() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ conf := GetConf()
+ //处理跨域问题
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
+ c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE")
+ c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+ c.Header("Access-Control-Allow-Credentials", "true")
+ // 放行所有OPTIONS方法,因为有的模板是要请求两次的
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(http.StatusNoContent)
+ }
+ if strings.Contains(c.Request.URL.Path, "debug") {
+ c.Next()
+ return
+ }
+ // 处理请求
+ if c.Request.Method != "GET" && c.Request.Method != "POST" {
+ log.Warnf("已拒绝客户端 %v 的请求: 方法错误", c.Request.RemoteAddr)
+ c.Status(404)
+ c.Abort()
+ }
+ if c.Request.Method == "POST" && strings.Contains(c.Request.Header.Get("Content-Type"), "application/json") {
+ d, err := c.GetRawData()
+ if err != nil {
+ log.Warnf("获取请求 %v 的Body时出现错误: %v", c.Request.RequestURI, err)
+ c.Status(400)
+ c.Abort()
+ }
+ if !gjson.ValidBytes(d) {
+ log.Warnf("已拒绝客户端 %v 的请求: 非法Json", c.Request.RemoteAddr)
+ c.Status(400)
+ c.Abort()
+ }
+ c.Set("json_body", gjson.ParseBytes(d))
+ }
+ authToken := conf.AccessToken
+ if auth := c.Request.Header.Get("Authorization"); auth != "" {
+ if strings.SplitN(auth, " ", 2)[1] != authToken {
+ c.AbortWithStatus(401)
+ return
+ }
+ } else if c.Query("access_token") != authToken {
+ c.AbortWithStatus(401)
+ return
+ } else {
+ c.Next()
+ }
+ }
+}
+
+func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin
+ JSONConfig = nil
+ conf := GetConf()
+ OldConf := s.Conf
+ cli := client.NewClient(conf.Uin, conf.Password)
+ log.Info("开始尝试登录并同步消息...")
+ log.Infof("使用协议: %v", func() string {
+ switch client.SystemDeviceInfo.Protocol {
+ case client.IPad:
+ return "iPad"
+ case client.AndroidPhone:
+ return "Android Phone"
+ case client.AndroidWatch:
+ return "Android Watch"
+ case client.MacOS:
+ return "MacOS"
+ }
+ return "未知"
+ }())
+ cli.OnLog(func(c *client.QQClient, e *client.LogEvent) {
+ switch e.Type {
+ case "INFO":
+ log.Info("Protocol -> " + e.Message)
+ case "ERROR":
+ log.Error("Protocol -> " + e.Message)
+ case "DEBUG":
+ log.Debug("Protocol -> " + e.Message)
+ }
+ })
+ cli.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
+ if !conf.UseSSOAddress {
+ log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
+ return false
+ }
+ log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
+ return true
+ })
+ s.Cli = cli
+ s.Dologin()
+ //关闭之前的 server
+ if OldConf.HTTPConfig != nil && OldConf.HTTPConfig.Enabled {
+ HttpServer.ShutDown()
+ }
+ //if OldConf.WSConfig != nil && OldConf.WSConfig.Enabled {
+ // server.WsShutdown()
+ //}
+ //s.UpServer()
+ s.ReloadServer()
+ s.Conf = conf
+}
+
+func (s *webServer) UpServer() {
+ conf := GetConf()
+ if conf.HTTPConfig != nil && conf.HTTPConfig.Enabled {
+ go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HTTPConfig.Host, conf.HTTPConfig.Port), conf.AccessToken, s.bot)
+ for k, v := range conf.HTTPConfig.PostUrls {
+ NewHttpClient().Run(k, v, conf.HTTPConfig.Timeout, s.bot)
+ }
+ }
+ if conf.WSConfig != nil && conf.WSConfig.Enabled {
+ go WebSocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, s.bot)
+ }
+ for _, rc := range conf.ReverseServers {
+ go NewWebSocketClient(rc, conf.AccessToken, s.bot).Run()
+ }
+}
+
+// 暂不支持ws服务的重启
+func (s *webServer) ReloadServer() {
+ conf := GetConf()
+ if conf.HTTPConfig != nil && conf.HTTPConfig.Enabled {
+ go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HTTPConfig.Host, conf.HTTPConfig.Port), conf.AccessToken, s.bot)
+ for k, v := range conf.HTTPConfig.PostUrls {
+ NewHttpClient().Run(k, v, conf.HTTPConfig.Timeout, s.bot)
+ }
+ }
+ for _, rc := range conf.ReverseServers {
+ go NewWebSocketClient(rc, conf.AccessToken, s.bot).Run()
+ }
+}
+
+// 热重启
+func AdminDoRestart(s *webServer, c *gin.Context) {
+ s.bot.Release()
+ s.bot = nil
+ s.Cli = nil
+ s.DoReLogin()
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+}
+
+// 进程重启
+func AdminProcessRestart(s *webServer, c *gin.Context) {
+ Restart <- struct{}{}
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+}
+
+// 冷重启
+func AdminDoRestartDocker(s *webServer, c *gin.Context) {
+ Console <- os.Kill
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+}
+
+// web输入 html 页面
+func AdminWebWrite(s *webServer, c *gin.Context) {
+ pic := global.ReadAllText("captcha.jpg")
+ var picbase64 string
+ var ispic = false
+ if pic != "" {
+ input := []byte(pic)
+ // base64编码
+ picbase64 = base64.StdEncoding.EncodeToString(input)
+ ispic = true
+ }
+ c.JSON(200, coolq.OK(coolq.MSG{
+ "ispic": ispic, //为空则为 设备锁 或者没有需要输入
+ "picbase64": picbase64, //web上显示图片
+ }))
+}
+
+// web输入 处理
+func AdminDoWebWrite(s *webServer, c *gin.Context) {
+ input := c.PostForm("input")
+ WebInput <- input
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+}
+
+// 普通配置修改
+func AdminDoConfigBase(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ conf.Uin, _ = strconv.ParseInt(c.PostForm("uin"), 10, 64)
+ conf.Password = c.PostForm("password")
+ if c.PostForm("enable_db") == "true" {
+ conf.EnableDB = true
+ } else {
+ conf.EnableDB = false
+ }
+ conf.AccessToken = c.PostForm("access_token")
+ if err := conf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ } else {
+ JSONConfig = nil
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+ }
+}
+
+// http配置修改
+func AdminDoConfigHttp(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16)
+ conf.HTTPConfig.Port = uint16(p)
+ conf.HTTPConfig.Host = c.PostForm("host")
+ if c.PostForm("enable") == "true" {
+ conf.HTTPConfig.Enabled = true
+ } else {
+ conf.HTTPConfig.Enabled = false
+ }
+ t, _ := strconv.ParseInt(c.PostForm("timeout"), 10, 32)
+ conf.HTTPConfig.Timeout = int32(t)
+ if c.PostForm("post_url") != "" {
+ conf.HTTPConfig.PostUrls[c.PostForm("post_url")] = c.PostForm("post_secret")
+ }
+ if err := conf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ } else {
+ JSONConfig = nil
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+ }
+}
+
+// ws配置修改
+func AdminDoConfigWs(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16)
+ conf.WSConfig.Port = uint16(p)
+ conf.WSConfig.Host = c.PostForm("host")
+ if c.PostForm("enable") == "true" {
+ conf.WSConfig.Enabled = true
+ } else {
+ conf.WSConfig.Enabled = false
+ }
+ if err := conf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ } else {
+ JSONConfig = nil
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+ }
+}
+
+// 反向ws配置修改
+func AdminDoConfigReverse(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ conf.ReverseServers[0].ReverseAPIURL = c.PostForm("reverse_api_url")
+ conf.ReverseServers[0].ReverseURL = c.PostForm("reverse_url")
+ conf.ReverseServers[0].ReverseEventURL = c.PostForm("reverse_event_url")
+ t, _ := strconv.ParseUint(c.PostForm("reverse_reconnect_interval"), 10, 16)
+ conf.ReverseServers[0].ReverseReconnectInterval = uint16(t)
+ if c.PostForm("enable") == "true" {
+ conf.ReverseServers[0].Enabled = true
+ } else {
+ conf.ReverseServers[0].Enabled = false
+ }
+ if err := conf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ } else {
+ JSONConfig = nil
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+ }
+}
+
+// config.json配置修改
+func AdminDoConfigJson(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ Json := c.PostForm("json")
+ err := json.Unmarshal([]byte(Json), &conf)
+ if err != nil {
+ log.Warnf("尝试加载配置文件 %v 时出现错误: %v", "config.hjson", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ return
+ }
+ if err := conf.Save("config.hjson"); err != nil {
+ log.Fatalf("保存 config.hjson 时出现错误: %v", err)
+ c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
+ } else {
+ JSONConfig = nil
+ c.JSON(200, coolq.OK(coolq.MSG{}))
+ }
+}
+
+// 拉取config.json配置
+func AdminGetConfigJson(s *webServer, c *gin.Context) {
+ conf := GetConf()
+ c.JSON(200, coolq.OK(coolq.MSG{"config": conf}))
+
+}
diff --git a/server/http.go b/server/http.go
index 9b10904..2ddfae2 100644
--- a/server/http.go
+++ b/server/http.go
@@ -5,11 +5,14 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
+ "net/http"
"os"
"strconv"
"strings"
"time"
+ "github.com/guonaihong/gout/dataflow"
+
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/gin-gonic/gin"
@@ -21,6 +24,7 @@ import (
type httpServer struct {
engine *gin.Engine
bot *coolq.CQBot
+ Http *http.Server
}
type httpClient struct {
@@ -31,6 +35,7 @@ type httpClient struct {
}
var HttpServer = &httpServer{}
+var Debug = false
func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
gin.SetMode(gin.ReleaseMode)
@@ -79,10 +84,14 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
go func() {
log.Infof("CQ HTTP 服务器已启动: %v", addr)
- err := s.engine.Run(addr)
- if err != nil {
+ s.Http = &http.Server{
+ Addr: addr,
+ Handler: s.engine,
+ }
+ if err := s.Http.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error(err)
- log.Infof("请检查端口是否被占用.")
+ log.Infof("HTTP 服务启动失败, 请检查端口是否被占用.")
+ log.Warnf("将在五秒后退出.")
time.Sleep(time.Second * 5)
os.Exit(1)
}
@@ -114,15 +123,28 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) {
}
if c.secret != "" {
mac := hmac.New(sha1.New, []byte(c.secret))
- mac.Write([]byte(m.ToJson()))
+ _, err := mac.Write([]byte(m.ToJson()))
+ if err != nil {
+ log.Error(err)
+ return nil
+ }
h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil))
}
return h
- }()).SetTimeout(time.Second * time.Duration(c.timeout)).Do()
+ }()).SetTimeout(time.Second * time.Duration(c.timeout)).F().Retry().Attempt(5).
+ WaitTime(time.Millisecond * 500).MaxWaitTime(time.Second * 5).
+ Func(func(con *dataflow.Context) error {
+ if con.Error != nil {
+ log.Warnf("上报Event到 HTTP 服务器 %v 时出现错误: %v 将重试.", c.addr, con.Error)
+ return con.Error
+ }
+ return nil
+ }).Do()
if err != nil {
log.Warnf("上报Event数据 %v 到 %v 失败: %v", m.ToJson(), c.addr, err)
return
}
+ log.Debugf("上报Event数据 %v 到 %v", m.ToJson(), c.addr)
if gjson.Valid(res) {
c.bot.CQHandleQuickOperation(gjson.Parse(m.ToJson()), gjson.Parse(res))
}
@@ -139,55 +161,84 @@ func (s *httpServer) HandleActions(c *gin.Context) {
}
}
-func (s *httpServer) GetLoginInfo(c *gin.Context) {
+func GetLoginInfo(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetLoginInfo())
}
-func (s *httpServer) GetFriendList(c *gin.Context) {
+func GetFriendList(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetFriendList())
}
-func (s *httpServer) GetGroupList(c *gin.Context) {
+func GetGroupList(s *httpServer, c *gin.Context) {
nc := getParamOrDefault(c, "no_cache", "false")
c.JSON(200, s.bot.CQGetGroupList(nc == "true"))
}
-func (s *httpServer) GetGroupInfo(c *gin.Context) {
+func GetGroupInfo(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
- c.JSON(200, s.bot.CQGetGroupInfo(gid))
+ nc := getParamOrDefault(c, "no_cache", "false")
+ c.JSON(200, s.bot.CQGetGroupInfo(gid, nc == "true"))
}
-func (s *httpServer) GetGroupMemberList(c *gin.Context) {
+func GetGroupMemberList(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
nc := getParamOrDefault(c, "no_cache", "false")
c.JSON(200, s.bot.CQGetGroupMemberList(gid, nc == "true"))
}
-func (s *httpServer) GetGroupMemberInfo(c *gin.Context) {
+func GetGroupMemberInfo(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
c.JSON(200, s.bot.CQGetGroupMemberInfo(gid, uid))
}
-func (s *httpServer) SendMessage(c *gin.Context) {
+func GetGroupFileSystemInfo(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetGroupFileSystemInfo(gid))
+}
+
+func GetGroupRootFiles(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetGroupRootFiles(gid))
+}
+
+func GetGroupFilesByFolderId(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ folderId := getParam(c, "folder_id")
+ c.JSON(200, s.bot.CQGetGroupFilesByFolderId(gid, folderId))
+}
+
+func GetGroupFileUrl(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ fid := getParam(c, "file_id")
+ busid, _ := strconv.ParseInt(getParam(c, "busid"), 10, 32)
+ c.JSON(200, s.bot.CQGetGroupFileUrl(gid, fid, int32(busid)))
+}
+
+func UploadGroupFile(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQUploadGroupFile(gid, getParam(c, "file"), getParam(c, "name"), getParam(c, "folder")))
+}
+
+func SendMessage(s *httpServer, c *gin.Context) {
if getParam(c, "message_type") == "private" {
- s.SendPrivateMessage(c)
+ SendPrivateMessage(s, c)
return
}
if getParam(c, "message_type") == "group" {
- s.SendGroupMessage(c)
+ SendGroupMessage(s, c)
return
}
if getParam(c, "group_id") != "" {
- s.SendGroupMessage(c)
+ SendGroupMessage(s, c)
return
}
if getParam(c, "user_id") != "" {
- s.SendPrivateMessage(c)
+ SendPrivateMessage(s, c)
}
}
-func (s *httpServer) SendPrivateMessage(c *gin.Context) {
+func SendPrivateMessage(s *httpServer, c *gin.Context) {
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
msg, t := getParamWithType(c, "message")
autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
@@ -198,7 +249,7 @@ func (s *httpServer) SendPrivateMessage(c *gin.Context) {
c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg, autoEscape))
}
-func (s *httpServer) SendGroupMessage(c *gin.Context) {
+func SendGroupMessage(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
msg, t := getParamWithType(c, "message")
autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
@@ -209,34 +260,34 @@ func (s *httpServer) SendGroupMessage(c *gin.Context) {
c.JSON(200, s.bot.CQSendGroupMessage(gid, msg, autoEscape))
}
-func (s *httpServer) SendGroupForwardMessage(c *gin.Context) {
+func SendGroupForwardMessage(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
msg := getParam(c, "messages")
c.JSON(200, s.bot.CQSendGroupForwardMessage(gid, gjson.Parse(msg)))
}
-func (s *httpServer) GetImage(c *gin.Context) {
+func GetImage(s *httpServer, c *gin.Context) {
file := getParam(c, "file")
c.JSON(200, s.bot.CQGetImage(file))
}
-func (s *httpServer) GetGroupMessage(c *gin.Context) {
+func GetMessage(s *httpServer, c *gin.Context) {
mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 32)
- c.JSON(200, s.bot.CQGetGroupMessage(int32(mid)))
+ c.JSON(200, s.bot.CQGetMessage(int32(mid)))
}
-func (s *httpServer) GetGroupHonorInfo(c *gin.Context) {
+func GetGroupHonorInfo(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQGetGroupHonorInfo(gid, getParam(c, "type")))
}
-func (s *httpServer) ProcessFriendRequest(c *gin.Context) {
+func ProcessFriendRequest(s *httpServer, c *gin.Context) {
flag := getParam(c, "flag")
approve := getParamOrDefault(c, "approve", "true")
c.JSON(200, s.bot.CQProcessFriendRequest(flag, approve == "true"))
}
-func (s *httpServer) ProcessGroupRequest(c *gin.Context) {
+func ProcessGroupRequest(s *httpServer, c *gin.Context) {
flag := getParam(c, "flag")
subType := getParam(c, "sub_type")
if subType == "" {
@@ -246,94 +297,146 @@ func (s *httpServer) ProcessGroupRequest(c *gin.Context) {
c.JSON(200, s.bot.CQProcessGroupRequest(flag, subType, getParam(c, "reason"), approve == "true"))
}
-func (s *httpServer) SetGroupCard(c *gin.Context) {
+func SetGroupCard(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupCard(gid, uid, getParam(c, "card")))
}
-func (s *httpServer) SetSpecialTitle(c *gin.Context) {
+func SetSpecialTitle(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupSpecialTitle(gid, uid, getParam(c, "special_title")))
}
-func (s *httpServer) SetGroupKick(c *gin.Context) {
+func SetGroupKick(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
msg := getParam(c, "message")
- c.JSON(200, s.bot.CQSetGroupKick(gid, uid, msg))
+ block := getParamOrDefault(c, "reject_add_request", "false")
+ c.JSON(200, s.bot.CQSetGroupKick(gid, uid, msg, block == "true"))
}
-func (s *httpServer) SetGroupBan(c *gin.Context) {
+func SetGroupBan(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
i, _ := strconv.ParseInt(getParamOrDefault(c, "duration", "1800"), 10, 64)
c.JSON(200, s.bot.CQSetGroupBan(gid, uid, uint32(i)))
}
-func (s *httpServer) SetWholeBan(c *gin.Context) {
+func SetWholeBan(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupWholeBan(gid, getParamOrDefault(c, "enable", "true") == "true"))
}
-func (s *httpServer) SetGroupName(c *gin.Context) {
+func SetGroupName(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupName(gid, getParam(c, "group_name")))
}
-func (s *httpServer) SetGroupAdmin(c *gin.Context) {
+func SetGroupAdmin(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupAdmin(gid, uid, getParamOrDefault(c, "enable", "true") == "true"))
}
-func (s *httpServer) SendGroupNotice(c *gin.Context) {
+func SendGroupNotice(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupMemo(gid, getParam(c, "content")))
}
-func (s *httpServer) SetGroupLeave(c *gin.Context) {
+func SetGroupLeave(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQSetGroupLeave(gid))
}
-func (s *httpServer) GetForwardMessage(c *gin.Context) {
+func SetRestart(s *httpServer, c *gin.Context) {
+ delay, _ := strconv.ParseInt(getParam(c, "delay"), 10, 64)
+ c.JSON(200, coolq.MSG{"data": nil, "retcode": 0, "status": "async"})
+ go func(delay int64) {
+ time.Sleep(time.Duration(delay) * time.Millisecond)
+ Restart <- struct{}{}
+ }(delay)
+
+}
+
+func GetForwardMessage(s *httpServer, c *gin.Context) {
resId := getParam(c, "message_id")
+ if resId == "" {
+ resId = getParam(c, "id")
+ }
c.JSON(200, s.bot.CQGetForwardMessage(resId))
}
-func (s *httpServer) DeleteMessage(c *gin.Context) {
+func GetGroupSystemMessage(s *httpServer, c *gin.Context) {
+ c.JSON(200, s.bot.CQGetGroupSystemMessages())
+}
+
+func DeleteMessage(s *httpServer, c *gin.Context) {
mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 32)
c.JSON(200, s.bot.CQDeleteMessage(int32(mid)))
}
-func (s *httpServer) CanSendImage(c *gin.Context) {
+func CanSendImage(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQCanSendImage())
}
-func (s *httpServer) CanSendRecord(c *gin.Context) {
+func CanSendRecord(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQCanSendRecord())
}
-func (s *httpServer) GetStatus(c *gin.Context) {
+func GetStatus(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetStatus())
}
-func (s *httpServer) GetVersionInfo(c *gin.Context) {
+func GetVersionInfo(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetVersionInfo())
}
-func (s *httpServer) ReloadEventFilter(c *gin.Context) {
+func ReloadEventFilter(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQReloadEventFilter())
}
-func (s *httpServer) GetVipInfo(c *gin.Context) {
+func GetVipInfo(s *httpServer, c *gin.Context) {
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
c.JSON(200, s.bot.CQGetVipInfo(uid))
}
-func (s *httpServer) HandleQuickOperation(c *gin.Context) {
+func GetStrangerInfo(s *httpServer, c *gin.Context) {
+ uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetStrangerInfo(uid))
+}
+
+func GetGroupAtAllRemain(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetAtAllRemain(gid))
+}
+
+func SetGroupAnonymousBan(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ d, _ := strconv.ParseInt(getParam(c, "duration"), 10, 64)
+ flag := getParam(c, "flag")
+ if flag == "" {
+ flag = getParam(c, "anonymous_flag")
+ }
+ if flag == "" {
+ o := gjson.Parse(getParam(c, "anonymous"))
+ flag = o.Get("flag").String()
+ }
+ c.JSON(200, s.bot.CQSetGroupAnonymousBan(gid, flag, int32(d)))
+}
+
+func GetGroupMessageHistory(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ seq, _ := strconv.ParseInt(getParam(c, "message_seq"), 10, 64)
+ c.JSON(200, s.bot.CQGetGroupMessageHistory(gid, seq))
+}
+
+func GetOnlineClients(s *httpServer, c *gin.Context) {
+ c.JSON(200, s.bot.CQGetOnlineClients(getParamOrDefault(c, "no_cache", "false") == "true"))
+}
+
+func HandleQuickOperation(s *httpServer, c *gin.Context) {
if c.Request.Method != "POST" {
c.AbortWithStatus(404)
return
@@ -344,6 +447,69 @@ func (s *httpServer) HandleQuickOperation(c *gin.Context) {
}
}
+func DownloadFile(s *httpServer, c *gin.Context) {
+ url := getParam(c, "url")
+ tc, _ := strconv.Atoi(getParam(c, "thread_count"))
+ h, t := getParamWithType(c, "headers")
+ headers := map[string]string{}
+ if t == gjson.Null || t == gjson.String {
+ lines := strings.Split(h, "\r\n")
+ for _, sub := range lines {
+ str := strings.SplitN(sub, "=", 2)
+ if len(str) == 2 {
+ headers[str[0]] = str[1]
+ }
+ }
+ }
+ if t == gjson.JSON {
+ arr := gjson.Parse(h)
+ for _, sub := range arr.Array() {
+ str := strings.SplitN(sub.String(), "=", 2)
+ if len(str) == 2 {
+ headers[str[0]] = str[1]
+ }
+ }
+ }
+ println(url, tc, h, t)
+ c.JSON(200, s.bot.CQDownloadFile(url, headers, tc))
+}
+
+func OcrImage(s *httpServer, c *gin.Context) {
+ img := getParam(c, "image")
+ c.JSON(200, s.bot.CQOcrImage(img))
+}
+
+func GetWordSlices(s *httpServer, c *gin.Context) {
+ content := getParam(c, "content")
+ c.JSON(200, s.bot.CQGetWordSlices(content))
+}
+
+func SetGroupPortrait(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ file := getParam(c, "file")
+ cache := getParam(c, "cache")
+ c.JSON(200, s.bot.CQSetGroupPortrait(gid, file, cache))
+}
+
+func SetEssenceMsg(s *httpServer, c *gin.Context) {
+ mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 64)
+ c.JSON(200, s.bot.CQSetEssenceMessage(int32(mid)))
+}
+
+func DeleteEssenceMsg(s *httpServer, c *gin.Context) {
+ mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 64)
+ c.JSON(200, s.bot.CQDeleteEssenceMessage(int32(mid)))
+}
+
+func GetEssenceMsgList(s *httpServer, c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetEssenceMessageList(gid))
+}
+
+func CheckUrlSafely(s *httpServer, c *gin.Context) {
+ c.JSON(200, s.bot.CQCheckUrlSafely(getParam(c, "url")))
+}
+
func getParamOrDefault(c *gin.Context, k, def string) string {
r := getParam(c, k)
if r != "" {
@@ -393,103 +559,69 @@ func getParamWithType(c *gin.Context, k string) (string, gjson.Type) {
}
var httpApi = map[string]func(s *httpServer, c *gin.Context){
- "get_login_info": func(s *httpServer, c *gin.Context) {
- s.GetLoginInfo(c)
- },
- "get_friend_list": func(s *httpServer, c *gin.Context) {
- s.GetFriendList(c)
- },
- "get_group_list": func(s *httpServer, c *gin.Context) {
- s.GetGroupList(c)
- },
- "get_group_info": func(s *httpServer, c *gin.Context) {
- s.GetGroupInfo(c)
- },
- "get_group_member_list": func(s *httpServer, c *gin.Context) {
- s.GetGroupMemberList(c)
- },
- "get_group_member_info": func(s *httpServer, c *gin.Context) {
- s.GetGroupMemberInfo(c)
- },
- "send_msg": func(s *httpServer, c *gin.Context) {
- s.SendMessage(c)
- },
- "send_group_msg": func(s *httpServer, c *gin.Context) {
- s.SendGroupMessage(c)
- },
- "send_group_forward_msg": func(s *httpServer, c *gin.Context) {
- s.SendGroupForwardMessage(c)
- },
- "send_private_msg": func(s *httpServer, c *gin.Context) {
- s.SendPrivateMessage(c)
- },
- "delete_msg": func(s *httpServer, c *gin.Context) {
- s.DeleteMessage(c)
- },
- "set_friend_add_request": func(s *httpServer, c *gin.Context) {
- s.ProcessFriendRequest(c)
- },
- "set_group_add_request": func(s *httpServer, c *gin.Context) {
- s.ProcessGroupRequest(c)
- },
- "set_group_card": func(s *httpServer, c *gin.Context) {
- s.SetGroupCard(c)
- },
- "set_group_special_title": func(s *httpServer, c *gin.Context) {
- s.SetSpecialTitle(c)
- },
- "set_group_kick": func(s *httpServer, c *gin.Context) {
- s.SetGroupKick(c)
- },
- "set_group_ban": func(s *httpServer, c *gin.Context) {
- s.SetGroupBan(c)
- },
- "set_group_whole_ban": func(s *httpServer, c *gin.Context) {
- s.SetWholeBan(c)
- },
- "set_group_name": func(s *httpServer, c *gin.Context) {
- s.SetGroupName(c)
- },
- "set_group_admin": func(s *httpServer, c *gin.Context) {
- s.SetGroupAdmin(c)
- },
- "_send_group_notice": func(s *httpServer, c *gin.Context) {
- s.SendGroupNotice(c)
- },
- "set_group_leave": func(s *httpServer, c *gin.Context) {
- s.SetGroupLeave(c)
- },
- "get_image": func(s *httpServer, c *gin.Context) {
- s.GetImage(c)
- },
- "get_forward_msg": func(s *httpServer, c *gin.Context) {
- s.GetForwardMessage(c)
- },
- "get_group_msg": func(s *httpServer, c *gin.Context) {
- s.GetGroupMessage(c)
- },
- "get_group_honor_info": func(s *httpServer, c *gin.Context) {
- s.GetGroupHonorInfo(c)
- },
- "can_send_image": func(s *httpServer, c *gin.Context) {
- s.CanSendImage(c)
- },
- "can_send_record": func(s *httpServer, c *gin.Context) {
- s.CanSendRecord(c)
- },
- "get_status": func(s *httpServer, c *gin.Context) {
- s.GetStatus(c)
- },
- "get_version_info": func(s *httpServer, c *gin.Context) {
- s.GetVersionInfo(c)
- },
- "_get_vip_info": func(s *httpServer, c *gin.Context) {
- s.GetVipInfo(c)
- },
- "reload_event_filter": func(s *httpServer, c *gin.Context) {
- s.ReloadEventFilter(c)
- },
- ".handle_quick_operation": func(s *httpServer, c *gin.Context) {
- s.HandleQuickOperation(c)
- },
+ "get_login_info": GetLoginInfo,
+ "get_friend_list": GetFriendList,
+ "get_group_list": GetGroupList,
+ "get_group_info": GetGroupInfo,
+ "get_group_member_list": GetGroupMemberList,
+ "get_group_member_info": GetGroupMemberInfo,
+ "get_group_file_system_info": GetGroupFileSystemInfo,
+ "get_group_root_files": GetGroupRootFiles,
+ "get_group_files_by_folder": GetGroupFilesByFolderId,
+ "get_group_file_url": GetGroupFileUrl,
+ "upload_group_file": UploadGroupFile,
+ "get_essence_msg_list": GetEssenceMsgList,
+ "send_msg": SendMessage,
+ "send_group_msg": SendGroupMessage,
+ "send_group_forward_msg": SendGroupForwardMessage,
+ "send_private_msg": SendPrivateMessage,
+ "delete_msg": DeleteMessage,
+ "delete_essence_msg": DeleteEssenceMsg,
+ "set_friend_add_request": ProcessFriendRequest,
+ "set_group_add_request": ProcessGroupRequest,
+ "set_group_card": SetGroupCard,
+ "set_group_special_title": SetSpecialTitle,
+ "set_group_kick": SetGroupKick,
+ "set_group_ban": SetGroupBan,
+ "set_group_whole_ban": SetWholeBan,
+ "set_group_name": SetGroupName,
+ "set_group_admin": SetGroupAdmin,
+ "set_essence_msg": SetEssenceMsg,
+ "set_restart": SetRestart,
+ "_send_group_notice": SendGroupNotice,
+ "set_group_leave": SetGroupLeave,
+ "get_image": GetImage,
+ "get_forward_msg": GetForwardMessage,
+ "get_msg": GetMessage,
+ "get_group_system_msg": GetGroupSystemMessage,
+ "get_group_honor_info": GetGroupHonorInfo,
+ "can_send_image": CanSendImage,
+ "can_send_record": CanSendRecord,
+ "get_status": GetStatus,
+ "get_version_info": GetVersionInfo,
+ "_get_vip_info": GetVipInfo,
+ "get_stranger_info": GetStrangerInfo,
+ "reload_event_filter": ReloadEventFilter,
+ "set_group_portrait": SetGroupPortrait,
+ "set_group_anonymous_ban": SetGroupAnonymousBan,
+ "get_group_msg_history": GetGroupMessageHistory,
+ "check_url_safely": CheckUrlSafely,
+ "download_file": DownloadFile,
+ ".handle_quick_operation": HandleQuickOperation,
+ ".ocr_image": OcrImage,
+ "ocr_image": OcrImage,
+ "get_group_at_all_remain": GetGroupAtAllRemain,
+ "get_online_clients": GetOnlineClients,
+ ".get_word_slices": GetWordSlices,
+}
+
+func (s *httpServer) ShutDown() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := s.Http.Shutdown(ctx); err != nil {
+ log.Fatal("http Server Shutdown:", err)
+ }
+ <-ctx.Done()
+ log.Println("timeout of 5 seconds.")
+ log.Println("http Server exiting")
}
diff --git a/server/websocket.go b/server/websocket.go
index ecad98a..998a9c4 100644
--- a/server/websocket.go
+++ b/server/websocket.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "runtime/debug"
"strconv"
"strings"
"sync"
@@ -16,36 +17,38 @@ import (
"github.com/tidwall/gjson"
)
-type websocketServer struct {
+type webSocketServer struct {
bot *coolq.CQBot
token string
- eventConn []*websocketConn
+ eventConn []*webSocketConn
eventConnMutex sync.Mutex
handshake string
}
-type websocketClient struct {
- conf *global.GoCQReverseWebsocketConfig
+//WebSocketClient Websocket客户端实例
+type WebSocketClient struct {
+ conf *global.GoCQReverseWebSocketConfig
token string
bot *coolq.CQBot
- universalConn *websocketConn
- eventConn *websocketConn
+ universalConn *webSocketConn
+ eventConn *webSocketConn
}
-type websocketConn struct {
+type webSocketConn struct {
*websocket.Conn
sync.Mutex
}
-var WebsocketServer = &websocketServer{}
+//WebSocketServer 初始化一个WebSocketServer实例
+var WebSocketServer = &webSocketServer{}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
-func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) {
+func (s *webSocketServer) Run(addr, authToken string, b *coolq.CQBot) {
s.token = authToken
s.bot = b
s.handshake = fmt.Sprintf(`{"_post_method":2,"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`,
@@ -60,29 +63,31 @@ func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) {
}()
}
-func NewWebsocketClient(conf *global.GoCQReverseWebsocketConfig, authToken string, b *coolq.CQBot) *websocketClient {
- return &websocketClient{conf: conf, token: authToken, bot: b}
+//NewWebSocketClient 初始化一个NWebSocket客户端
+func NewWebSocketClient(conf *global.GoCQReverseWebSocketConfig, authToken string, b *coolq.CQBot) *WebSocketClient {
+ return &WebSocketClient{conf: conf, token: authToken, bot: b}
}
-func (c *websocketClient) Run() {
+//Run 运行实例
+func (c *WebSocketClient) Run() {
if !c.conf.Enabled {
return
}
- if c.conf.ReverseUrl != "" {
+ if c.conf.ReverseURL != "" {
c.connectUniversal()
} else {
- if c.conf.ReverseApiUrl != "" {
- c.connectApi()
+ if c.conf.ReverseAPIURL != "" {
+ c.connectAPI()
}
- if c.conf.ReverseEventUrl != "" {
+ if c.conf.ReverseEventURL != "" {
c.connectEvent()
}
}
c.bot.OnEventPush(c.onBotPushEvent)
}
-func (c *websocketClient) connectApi() {
- log.Infof("开始尝试连接到反向Websocket API服务器: %v", c.conf.ReverseApiUrl)
+func (c *WebSocketClient) connectAPI() {
+ log.Infof("开始尝试连接到反向Websocket API服务器: %v", c.conf.ReverseAPIURL)
header := http.Header{
"X-Client-Role": []string{"API"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@@ -91,22 +96,22 @@ func (c *websocketClient) connectApi() {
if c.token != "" {
header["Authorization"] = []string{"Token " + c.token}
}
- conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseApiUrl, header)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseAPIURL, header)
if err != nil {
- log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err)
+ log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseAPIURL, err)
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
- c.connectApi()
+ c.connectAPI()
}
return
}
- log.Infof("已连接到反向Websocket API服务器 %v", c.conf.ReverseApiUrl)
- wrappedConn := &websocketConn{Conn: conn}
- go c.listenApi(wrappedConn, false)
+ log.Infof("已连接到反向Websocket API服务器 %v", c.conf.ReverseAPIURL)
+ wrappedConn := &webSocketConn{Conn: conn}
+ go c.listenAPI(wrappedConn, false)
}
-func (c *websocketClient) connectEvent() {
- log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventUrl)
+func (c *WebSocketClient) connectEvent() {
+ log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventURL)
header := http.Header{
"X-Client-Role": []string{"Event"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@@ -115,9 +120,9 @@ func (c *websocketClient) connectEvent() {
if c.token != "" {
header["Authorization"] = []string{"Token " + c.token}
}
- conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventUrl, header)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventURL, header)
if err != nil {
- log.Warnf("连接到反向Websocket Event服务器 %v 时出现错误: %v", c.conf.ReverseEventUrl, err)
+ log.Warnf("连接到反向Websocket Event服务器 %v 时出现错误: %v", c.conf.ReverseEventURL, err)
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectEvent()
@@ -132,12 +137,12 @@ func (c *websocketClient) connectEvent() {
log.Warnf("反向Websocket 握手时出现错误: %v", err)
}
- log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventUrl)
- c.eventConn = &websocketConn{Conn: conn}
+ log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventURL)
+ c.eventConn = &webSocketConn{Conn: conn}
}
-func (c *websocketClient) connectUniversal() {
- log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseUrl)
+func (c *WebSocketClient) connectUniversal() {
+ log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseURL)
header := http.Header{
"X-Client-Role": []string{"Universal"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@@ -146,9 +151,9 @@ func (c *websocketClient) connectUniversal() {
if c.token != "" {
header["Authorization"] = []string{"Token " + c.token}
}
- conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseUrl, header)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseURL, header)
if err != nil {
- log.Warnf("连接到反向Websocket Universal服务器 %v 时出现错误: %v", c.conf.ReverseUrl, err)
+ log.Warnf("连接到反向Websocket Universal服务器 %v 时出现错误: %v", c.conf.ReverseURL, err)
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectUniversal()
@@ -163,12 +168,12 @@ func (c *websocketClient) connectUniversal() {
log.Warnf("反向Websocket 握手时出现错误: %v", err)
}
- wrappedConn := &websocketConn{Conn: conn}
- go c.listenApi(wrappedConn, true)
+ wrappedConn := &webSocketConn{Conn: conn}
+ go c.listenAPI(wrappedConn, true)
c.universalConn = wrappedConn
}
-func (c *websocketClient) listenApi(conn *websocketConn, u bool) {
+func (c *WebSocketClient) listenAPI(conn *webSocketConn, u bool) {
defer conn.Close()
for {
_, buf, err := conn.ReadMessage()
@@ -183,12 +188,12 @@ func (c *websocketClient) listenApi(conn *websocketConn, u bool) {
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
if !u {
- go c.connectApi()
+ go c.connectAPI()
}
}
}
-func (c *websocketClient) onBotPushEvent(m coolq.MSG) {
+func (c *WebSocketClient) onBotPushEvent(m coolq.MSG) {
if c.eventConn != nil {
log.Debugf("向WS服务器 %v 推送Event: %v", c.eventConn.RemoteAddr().String(), m.ToJson())
conn := c.eventConn
@@ -221,7 +226,7 @@ func (c *websocketClient) onBotPushEvent(m coolq.MSG) {
}
}
-func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) {
+func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) {
if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@@ -245,14 +250,14 @@ func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) {
log.Infof("接受 Websocket 连接: %v (/event)", r.RemoteAddr)
- conn := &websocketConn{Conn: c}
+ conn := &webSocketConn{Conn: c}
s.eventConnMutex.Lock()
s.eventConn = append(s.eventConn, conn)
s.eventConnMutex.Unlock()
}
-func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) {
+func (s *webSocketServer) api(w http.ResponseWriter, r *http.Request) {
if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@@ -268,11 +273,11 @@ func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) {
return
}
log.Infof("接受 Websocket 连接: %v (/api)", r.RemoteAddr)
- conn := &websocketConn{Conn: c}
- go s.listenApi(conn)
+ conn := &webSocketConn{Conn: c}
+ go s.listenAPI(conn)
}
-func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) {
+func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@@ -294,12 +299,12 @@ func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) {
return
}
log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr)
- conn := &websocketConn{Conn: c}
+ conn := &webSocketConn{Conn: c}
s.eventConn = append(s.eventConn, conn)
- s.listenApi(conn)
+ s.listenAPI(conn)
}
-func (s *websocketServer) listenApi(c *websocketConn) {
+func (s *webSocketServer) listenAPI(c *webSocketConn) {
defer c.Close()
for {
t, payload, err := c.ReadMessage()
@@ -313,10 +318,10 @@ func (s *websocketServer) listenApi(c *websocketConn) {
}
}
-func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
+func (c *webSocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
defer func() {
if err := recover(); err != nil {
- log.Printf("处置WS命令时发生无法恢复的异常:%v", err)
+ log.Printf("处置WS命令时发生无法恢复的异常:%v\n%s", err, debug.Stack())
c.Close()
}
}()
@@ -324,7 +329,7 @@ func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
j := gjson.ParseBytes(payload)
t := strings.ReplaceAll(j.Get("action").Str, "_async", "")
log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw)
- if f, ok := wsApi[t]; ok {
+ if f, ok := wsAPI[t]; ok {
ret := f(bot, j.Get("params"))
if j.Get("echo").Exists() {
ret["echo"] = j.Get("echo").Value()
@@ -332,10 +337,18 @@ func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
c.Lock()
defer c.Unlock()
_ = c.WriteJSON(ret)
+ } else {
+ ret := coolq.Failed(1404, "API_NOT_FOUND", "API不存在")
+ if j.Get("echo").Exists() {
+ ret["echo"] = j.Get("echo").Value()
+ }
+ c.Lock()
+ defer c.Unlock()
+ _ = c.WriteJSON(ret)
}
}
-func (s *websocketServer) onBotPushEvent(m coolq.MSG) {
+func (s *webSocketServer) onBotPushEvent(m coolq.MSG) {
s.eventConnMutex.Lock()
defer s.eventConnMutex.Unlock()
for i, l := 0, len(s.eventConn); i < l; i++ {
@@ -359,7 +372,7 @@ func (s *websocketServer) onBotPushEvent(m coolq.MSG) {
}
}
-var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
+var wsAPI = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_login_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetLoginInfo()
},
@@ -370,7 +383,7 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
return bot.CQGetGroupList(p.Get("no_cache").Bool())
},
"get_group_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQGetGroupInfo(p.Get("group_id").Int())
+ return bot.CQGetGroupInfo(p.Get("group_id").Int(), p.Get("no_cache").Bool())
},
"get_group_member_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupMemberList(p.Get("group_id").Int(), p.Get("no_cache").Bool())
@@ -433,7 +446,7 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
return bot.CQSetGroupSpecialTitle(p.Get("group_id").Int(), p.Get("user_id").Int(), p.Get("special_title").Str)
},
"set_group_kick": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQSetGroupKick(p.Get("group_id").Int(), p.Get("user_id").Int(), p.Get("message").Str)
+ return bot.CQSetGroupKick(p.Get("group_id").Int(), p.Get("user_id").Int(), p.Get("message").Str, p.Get("reject_add_request").Bool())
},
"set_group_ban": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSetGroupBan(p.Get("group_id").Int(), p.Get("user_id").Int(), func() uint32 {
@@ -472,32 +485,139 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
return bot.CQGetImage(p.Get("file").Str)
},
"get_forward_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQGetForwardMessage(p.Get("message_id").Str)
+ id := p.Get("message_id").Str
+ if id == "" {
+ id = p.Get("id").Str
+ }
+ return bot.CQGetForwardMessage(id)
},
- "get_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQGetGroupMessage(int32(p.Get("message_id").Int()))
+ "get_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetMessage(int32(p.Get("message_id").Int()))
+ },
+ "download_file": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ headers := map[string]string{}
+ headersToken := p.Get("headers")
+ if headersToken.IsArray() {
+ for _, sub := range headersToken.Array() {
+ str := strings.SplitN(sub.String(), "=", 2)
+ if len(str) == 2 {
+ headers[str[0]] = str[1]
+ }
+ }
+ }
+ if headersToken.Type == gjson.String {
+ lines := strings.Split(headersToken.String(), "\r\n")
+ for _, sub := range lines {
+ str := strings.SplitN(sub, "=", 2)
+ if len(str) == 2 {
+ headers[str[0]] = str[1]
+ }
+ }
+ }
+ return bot.CQDownloadFile(p.Get("url").Str, headers, int(p.Get("thread_count").Int()))
},
"get_group_honor_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupHonorInfo(p.Get("group_id").Int(), p.Get("type").Str)
},
+ "set_restart": func(c *coolq.CQBot, p gjson.Result) coolq.MSG {
+ var delay int64
+ delay = p.Get("delay").Int()
+ if delay < 0 {
+ delay = 0
+ }
+ defer func(delay int64) {
+ time.Sleep(time.Duration(delay) * time.Millisecond)
+ Restart <- struct{}{}
+ }(delay)
+ return coolq.MSG{"data": nil, "retcode": 0, "status": "async"}
+
+ },
"can_send_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQCanSendImage()
},
"can_send_record": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQCanSendRecord()
},
+ "get_stranger_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetStrangerInfo(p.Get("user_id").Int())
+ },
"get_status": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetStatus()
},
"get_version_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetVersionInfo()
},
+ "get_group_system_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupSystemMessages()
+ },
+ "get_group_file_system_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupFileSystemInfo(p.Get("group_id").Int())
+ },
+ "get_group_root_files": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupRootFiles(p.Get("group_id").Int())
+ },
+ "get_group_files_by_folder": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupFilesByFolderId(p.Get("group_id").Int(), p.Get("folder_id").Str)
+ },
+ "get_group_file_url": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupFileUrl(p.Get("group_id").Int(), p.Get("file_id").Str, int32(p.Get("busid").Int()))
+ },
+ "upload_group_file": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQUploadGroupFile(p.Get("group_id").Int(), p.Get("file").Str, p.Get("name").Str, p.Get("folder").Str)
+ },
+ "get_group_msg_history": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupMessageHistory(p.Get("group_id").Int(), p.Get("message_seq").Int())
+ },
"_get_vip_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetVipInfo(p.Get("user_id").Int())
},
"reload_event_filter": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQReloadEventFilter()
},
+ ".ocr_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQOcrImage(p.Get("image").Str)
+ },
+ "ocr_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQOcrImage(p.Get("image").Str)
+ },
+ "get_group_at_all_remain": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetAtAllRemain(p.Get("group_id").Int())
+ },
+ "get_online_clients": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetOnlineClients(p.Get("no_cache").Bool())
+ },
+ ".get_word_slices": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetWordSlices(p.Get("content").Str)
+ },
+ "set_group_portrait": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQSetGroupPortrait(p.Get("group_id").Int(), p.Get("file").String(), p.Get("cache").String())
+ },
+ "set_essence_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQSetEssenceMessage(int32(p.Get("message_id").Int()))
+ },
+ "delete_essence_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQDeleteEssenceMessage(int32(p.Get("message_id").Int()))
+ },
+ "get_essence_msg_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetEssenceMessageList(p.Get("group_id").Int())
+ },
+ "check_url_safely": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQCheckUrlSafely(p.Get("url").String())
+ },
+ "set_group_anonymous_ban": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ obj := p.Get("anonymous")
+ flag := p.Get("anonymous_flag")
+ if !flag.Exists() {
+ flag = p.Get("flag")
+ }
+ if !flag.Exists() && !obj.Exists() {
+ return coolq.Failed(100, "FLAG_NOT_FOUND", "flag未找到")
+ }
+ if !flag.Exists() {
+ flag = obj.Get("flag")
+ }
+ return bot.CQSetGroupAnonymousBan(p.Get("group_id").Int(), flag.String(), int32(p.Get("duration").Int()))
+ },
".handle_quick_operation": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQHandleQuickOperation(p.Get("context"), p.Get("operation"))
},