1
0
mirror of https://github.com/Mrs4s/go-cqhttp.git synced 2025-06-30 03:43:25 +00:00

Compare commits

...

117 Commits

Author SHA1 Message Date
54dbccf63c Merge branch 'dev' 2022-02-09 17:34:19 +08:00
4b3ae1c779 fix #1351 2022-02-09 17:31:39 +08:00
0145879f37 ci(chore): Fix stylings 2022-02-09 07:02:21 +00:00
e1937e9f15 docs: document default env placeholder 2022-02-09 15:01:38 +08:00
8eefcc8cc8 config: impl env placeholder with default value
Fixes #1358
2022-02-09 13:41:19 +08:00
a4992c3f79 修复了自动创建的批处理文件无法执行的Bug (#1355)
* 修复了自动创建的批处理文件无法执行的Bug

* 调整代码
2022-02-08 23:54:36 +08:00
b70db344a6 .github: update golint.yml 2022-02-08 21:53:43 +08:00
d33f17e727 fix: uploadForwardElement upload image error (#1354) 2022-02-07 19:49:24 +08:00
8773ff9bd1 Merge pull request #1341 from Ink-33/patch-5
style: add some comments
2022-02-06 22:41:05 +08:00
82b0c69b1b config: remove old env config
Fixes #1219
2022-02-06 22:19:30 +08:00
57d1be0ee5 db/leveldb: register message.RedBagMessageType
Fixes #1323
2022-02-06 18:39:22 +08:00
f88f51ceed cmd/gocq: switch faststart over to a flag 2022-02-06 16:04:03 +08:00
f8fa906a95 .github: fix issue link 2022-01-30 16:05:37 +08:00
b0fb02e890 .github: update bug-report template 2022-01-30 16:02:26 +08:00
1f55bde728 ci: make lint happy 2022-01-30 16:00:30 +08:00
e8bf497022 fix: fix btree iterate for empty db 2022-01-30 15:50:02 +08:00
6c7445772c fix: fix btree iterate 2022-01-30 15:35:14 +08:00
d2a58014bc feat: support foreach in btree 2022-01-30 14:52:29 +08:00
be3b17dc6b fix: support join query from http query&form
Fixes #1241
Fixes #1325
2022-01-30 14:28:22 +08:00
7dc4a0bf5e style: add some comments 2022-01-26 20:30:41 +08:00
a5dd0bfa1f feat: disable temp session by default 2022-01-24 19:52:13 +08:00
8723ff8713 dep: update websocket again 2022-01-23 22:58:17 +08:00
81d910fb05 dep: update MiraiGo & websocket 2022-01-23 22:47:13 +08:00
0cc0a20848 .github: update bug-report template 2022-01-22 21:51:20 +08:00
6916fb7d47 dep: use RomiChan forked websocket 2022-01-12 22:08:39 +08:00
9152185ebc fix: PutBuffer panic on nil event buffer (#1299) 2021-12-31 11:19:58 +08:00
b98f75ccab style: run gofumpt 2021-12-29 14:04:51 +08:00
18487d6353 dep: update dependencies 2021-12-28 22:12:24 +08:00
59bc7b4bae internal/cache: always use CacheDB
Updates #1075
2021-12-28 17:33:14 +08:00
a49becfff4 api-gen: replace aliases with multi-path 2021-12-28 17:17:47 +08:00
05f7eeb5c9 fix: fix api-generator panic 2021-12-28 16:56:36 +08:00
a10aee8c8d fix: make linter happy 2021-12-26 17:54:03 +08:00
b8fe459c75 feat(coolq): upload images/videos in forward message concurrently 2021-12-26 17:50:40 +08:00
80e35fc800 fix(server): fix Mutex change in reconnect 2021-12-26 15:48:08 +08:00
ef2223828f feat: add WriteDeadline 2021-12-26 13:10:40 +08:00
94ec3ccbcd ci(chore): Fix stylings 2021-12-24 14:27:39 +00:00
bfc29a8c97 feat: support set max retries and retries interval (close #1252) (#1289)
* feat: support set max retries and retries interval

* fix: httpresponse using `res` before checking for errors

* fix: `HttpServerPost` now be unexported

* refactor: pretty `httpDefault`
2021-12-24 22:27:07 +08:00
024ec34fbe fix: bus_id compatibility 2021-12-23 21:18:01 +08:00
Sam
e9fdefd162 [update] Bumped version (#1284) 2021-12-23 17:29:36 +08:00
c775d91e1c fix: private reply message serialize 2021-12-19 22:48:09 +08:00
897119cbca ci: make lint happy 2021-12-19 15:48:07 +08:00
3f67bee51e dep: update MiraiGo
may Fixes #1276
2021-12-19 15:44:20 +08:00
7785c819fb doc: guild api get_topic_channel_feeds 2021-12-17 23:29:42 +08:00
38000611c3 dep: update MiraiGo 2021-12-17 21:05:27 +08:00
20ce02f0c9 ci: fix lint by adding // nolint 2021-12-17 12:44:53 +08:00
5a180cb8c2 dep: update MiraiGo 2021-12-17 12:38:52 +08:00
2f1077e795 feat: expand forward message
if received a nested forward, the content would be an array rather than a `[CQ:forward]`
2021-12-17 12:36:17 +08:00
cb16c08ac8 fix: download & delete group file api error 2021-12-17 02:50:37 +08:00
304667a822 feat: error dump 2021-12-16 17:48:39 +08:00
3ebcb70fce Merge branch 'master' into dev 2021-12-16 17:20:51 +08:00
0accc89693 ci(package): remove useless nightly package (#1260) 2021-12-14 22:27:36 +08:00
9506dc21ab style: delete useless return 2021-12-14 17:53:51 +08:00
8da29c292e feat: get_guild_msg api no_cache 2021-12-14 17:50:36 +08:00
3b0e9e67cf Merge pull request #1253 from sam01101/patch-4
修复模版错误
2021-12-14 12:41:24 +08:00
8f4286074b [fix] Checkbox cause github 500 error 2021-12-14 11:08:46 +08:00
32aa82f914 feat: get_guild_msg api 2021-12-13 02:34:05 +08:00
f5ef0c188b feat: guild_channel_recall event 2021-12-13 02:21:20 +08:00
1bc3818510 feat: basic guild music share support 2021-12-12 17:15:42 +08:00
bc2901f4bd Merge branch 'dev' of github.com:/Mrs4s/go-cqhttp into dev 2021-12-12 00:24:38 +08:00
120b925fd6 update: remove internal id from guild message id 2021-12-12 00:24:24 +08:00
abf42ce6b1 ci(chore): Fix stylings 2021-12-11 15:36:58 +00:00
54e69acb8b feat: support guild image decode to db 2021-12-11 23:35:49 +08:00
57b2ce4c04 feat: database support for guild channel message 2021-12-11 23:16:00 +08:00
2461ff58a5 doc: update guild doc 2021-12-11 03:10:53 +08:00
5d81267c12 feat: get_guild_member_profile api 2021-12-11 02:57:17 +08:00
d561d4fd80 Merge pull request #1184 from Ink-33/patch-3
fix typo
2021-12-10 14:00:09 +08:00
49aedc99fe fix(expand env): change os.ExpandEnv to regex (#1231)
* fix(expand env): change os.ExpandEnv to regex

* fix: MustCompile

* fix: regex
2021-12-09 17:53:40 +08:00
0211a0ea96 doc: guild api get_guild_member_list 2021-12-09 16:28:43 +08:00
c40f1b8191 doc: fix typo 2021-12-09 01:00:23 +08:00
9c3997d11f doc: update guild doc 2021-12-09 00:55:50 +08:00
9f7f5a6dea doc: update guild doc 2021-12-09 00:54:11 +08:00
a9c1f2e5f3 ci: fix version path 2021-12-08 22:52:37 +08:00
8ba93cbf7c ci: use commit sha version 2021-12-08 22:47:00 +08:00
c61de34732 ci(chore): Fix stylings 2021-12-08 13:52:55 +00:00
e69051e88b feat: get_guild_member_list api 2021-12-08 21:52:00 +08:00
7278f99ed9 api: temporary delete get_guild_members api 2021-12-08 17:26:13 +08:00
44e0ff44fb update dep 2021-12-08 17:17:33 +08:00
8b8a59f6d1 update dep 2021-12-08 13:49:29 +08:00
26a7a1f0b6 feat(config): separate config & server (#1212)
* feat(server): add RegisterCustom

* feat(config): seprate config & server

* fix: make lint happy

* fix: make lint happy

* fix: ParseEnv nil pointer error

* typo(config): generateConfig hint

* fix(config): panic on range overflow
2021-12-07 22:39:30 +08:00
3eade331bf update dep 2021-12-07 13:50:51 +08:00
aa2caac3f7 feat: support expand env 2021-12-06 22:31:02 +08:00
0ead592114 update protobuf (#1217)
Co-authored-by: hecheng337 <root@OpenWrt>
2021-12-06 21:07:06 +08:00
dfec28dac6 server: fix concurrent write in ws 2021-12-05 22:40:44 +08:00
a06a891186 feat: 将程序的主体部分移动到 cmd/gocq (#1215)
* feat: move main into cmd/gocq

* fix: make lint happy

* fix: rename Boot to Main
2021-12-04 13:48:12 +08:00
fbf0d7d1e0 update dep 2021-12-02 01:07:01 +08:00
fe1bfeb948 Fix image..jpg (#1198) 2021-11-29 12:58:50 +08:00
b4bd22ea11 ci(chore): Fix stylings 2021-11-27 08:02:04 +00:00
bf06f50a83 add comments 2021-11-27 16:01:09 +08:00
3e9920b31b feat: get_topic_channel_feeds api 2021-11-27 15:58:13 +08:00
30edae64b3 fix: guild api use string-id type 2021-11-24 01:39:33 +08:00
bb769941ab fix: role api issues 2021-11-24 00:30:57 +08:00
b4d797a7b3 merge master 2021-11-23 23:55:32 +08:00
e990860632 Merge pull request #1188 from Bluefissure/feature/guild-role
feat: guild role API
2021-11-23 23:47:59 +08:00
b4d92fcae3 fix: guild event use string-id type 2021-11-23 23:36:32 +08:00
52743a8a5f Merge branch 'master' into dev 2021-11-23 20:30:31 +08:00
b1a09591d2 Merge pull request #1185 from ACodinghusky/patch-1
Update config.yml template
2021-11-23 17:40:25 +08:00
e73417bebf feat: Add guild role APIs
chore: lint code

refactor: rename by global.MSG

refactor: CQSetGuildMemberRole

fix: regenerate api
2021-11-22 17:34:09 -06:00
6b9f94c0f7 Update config.yml template
Update config.yml template
2021-11-20 23:23:50 +08:00
d8445c2d8d fix typo 2021-11-20 22:52:23 +08:00
2500d2dc6a Merge branch 'master' into dev 2021-11-20 20:59:44 +08:00
fadc460f4a update template 2021-11-20 20:58:51 +08:00
67fe1f661f template: fix typo 2021-11-20 20:49:40 +08:00
2fc7f995f2 coolq: make animated sticker subset of face message 2021-11-20 18:45:35 +08:00
7e573f9be6 feat: support animated sticker message 2021-11-19 23:20:42 +08:00
6f3c6f3681 Merge pull request #1180 from openwrt2223/master
Update modules/config/config.go
2021-11-19 01:47:02 +08:00
72fec47622 Update modules/config/config.go
https://github.com/Mrs4s/go-cqhttp/issues/1169#issuecomment-970192648
2021-11-19 00:44:27 +08:00
150ce2950a fix: allow sending guild short video
Fixes #1176
Fixes #1170
2021-11-18 21:31:46 +08:00
a2d1e88ed5 fix: use gorilla/websocket 2021-11-18 14:23:17 +08:00
c2c1fb00e5 fix: fix set_group_special_title param
Fixes #1178
2021-11-18 13:13:52 +08:00
c6119cf9ea del: Removed issuebot due to issue template (#1174) 2021-11-18 09:58:46 +08:00
bb7f83201e feat(http): enable v12 style http endpoint 2021-11-17 22:11:15 +08:00
7b02f8b670 fix(server): create new request for every post trial
Updates #1169
2021-11-17 21:45:51 +08:00
562e886e60 Merge pull request #1171 from sam01101/patch-2
更新新的 Bug 回报模板
2021-11-17 15:50:45 +08:00
c478870870 fix: don't listenAPI in Event connection
bug introduced by last commit
2021-11-17 11:49:33 +08:00
dba2bf2881 fix: fix ws-reverse connect info 2021-11-17 11:41:55 +08:00
ea2bda523f 修正 HTTP POST 内容类型为 json (#1168) 2021-11-17 10:36:23 +08:00
aa712ed4ac feat: Update bug template 2021-11-16 19:25:59 +08:00
46 changed files with 2327 additions and 1495 deletions

View File

@ -1,46 +0,0 @@
---
name: Bug汇报
about: 遇到了bug? 你可以在这里开始汇报(仅限软件本体问题)
title: ''
labels: bug?
assignees: ''
---
<!--
!请不要删除此处内容!
在您发布此Issue前, 请您花一点时间查看下面几条指引🔽
1: ❗ | 确定没有相同问题的ISSUE已被提出. (教程: https://github.com/Mrs4s/go-cqhttp/issues/633)
2: 🌎| 请准确填写环境信息.
3: ❔ | 打开DEBUG模式复现并提供出现问题前后至少 10 秒的完整日志内容。请自行删除日志内存在的个人信息及敏感内容。
4: ⚠ | 如果涉及内存泄漏/CPU占用异常请打开DEBUG模式并下载pprof性能分析.
注: 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》
(链接: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
请确保您已经仔细阅读此教程,并勾选下方的确认框。(将 [ ] 修改为 [x])
--------
- [ ] 我已经仔细阅读上述教程和"提问前需知 [图+文]": https://github.com/Mrs4s/go-cqhttp/issues/633
- [ ] 我已知晓并同意,如果我不遵循以下格式提交 Issue或者我使用的并非最新版本或者我没有提供足够的环境信息则我的 Issue 可能会被无条件自动关闭和锁定。
- [ ] 我已知晓并同意,我仅需要把选项前的 [ ] 替换为 [x]。如果我删除、修改这些复选框的其他部分,或是在 x 之前或之后留了空格,则我的 Issue 可能会被无条件自动关闭和锁定。
- [ ] 我已知晓并同意,此处仅用于汇报程序中存在的问题。若这个 Issue 是关于其他非程序本身问题或是新功能需求,则我的 Issue 可能会被无条件自动关闭和锁定。(这些问题应当在 Discussion 板块提出。)
--------
-->
**环境信息**
<!-- 请根据实际使用环境修改以下信息。请勿删除或留空。 -->
go-cqhttp版本:
运行环境:
连接方式:
使用协议:
**在最新的release版本中能否复现**
**bug内容**
<!-- 请在这里详细描述bug的内容 -->
**复现方法**
<!-- 请在这里分步骤的描述如何复现这个bug -->

157
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View File

@ -0,0 +1,157 @@
name: 回报错误
description: 在使用 go-cqhttp 的过程中遇到了错误
title: '[Bug]: '
labels: [ "bug?" ]
body:
# User's README and agreement
- type: markdown
attributes:
value: |
## 感谢您愿意填写错误回报!
## 以下是一些注意事项,请务必阅读让我们能够更容易处理
### ❗ | 确定没有相同问题的ISSUE已被提出. (教程: https://github.com/Mrs4s/go-cqhttp/issues/633)
### 🌎| 请准确填写环境信息
### ❔ | 打开DEBUG模式复现并提供出现问题前后至少 10 秒的完整日志内容。请自行删除日志内存在的个人信息及敏感内容。
### ⚠ | 如果涉及内存泄漏/CPU占用异常请打开DEBUG模式并下载pprof性能分析.
## 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》
链接: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
---
- type: checkboxes
id: terms
attributes:
label: 请确保您已阅读以上注意事项,并勾选下方的确认框。
options:
- label: "我已经仔细阅读上述教程和 [\"提问前需知\"](https://github.com/Mrs4s/go-cqhttp/issues/633)"
required: true
- label: "我已经使用 [dev分支版本](https://github.com/Mrs4s/go-cqhttp/actions/workflows/ci.yml) 测试过,问题依旧存在。"
required: true
- label: "我已经在 [Issue Tracker](https://github.com/Mrs4s/go-cqhttp/issues) 中找过我要提出的问题没有找到相同问题的ISSUE。"
required: true
- label: 我已知晓并同意,此处仅用于汇报程序中存在的问题。若这个 Issue 是关于其他非程序本身问题,则我的 Issue 可能会被无条件自动关闭或/并锁定。(这些问题应当在 Discussion 板块提出。)
required: true
# User's data
- type: markdown
attributes:
value: |
## 环境信息
请根据实际使用环境修改以下信息。
# Env | go-cqhttp Version
- type: input
id: env-gocq-ver
attributes:
label: go-cqhttp 版本
validations:
required: true
# Env | VM Version
- type: dropdown
id: env-vm-ver
attributes:
label: 运行环境
description: 选择运行 go-cqhttp 的系统版本
options:
- Windows (64)
- Windows (32/x84)
- MacOS
- Linux
- Ubuntu
- CentOS
- ArchLinux
- UNIX (Android)
- 其它(请在下方说明)
validations:
required: true
# Env | VM Arch
- type: dropdown
id: env-vm-arch
attributes:
label: 运行架构
description: (可选) 选择运行 go-cqhttp 的系统架构
options:
- AMD64
- x86
- ARM [32] (别名AArch32 / ARMv7
- ARM [64] (别名AArch64 / ARMv8
- 其它
# Env | Connection type
- type: dropdown
id: env-conn-type
attributes:
label: 连接方式
description: 选择对接机器人的连接方式
options:
- HTTP
- WebSocket (正向)
- WebSocket (反向)
- LambdaServer
validations:
required: true
# Env | Protocol
- type: dropdown
id: env-protocol
attributes:
label: 使用协议
description: 选择使用的协议
options:
- 0 | iPad
- 1 | Android Phone
- 2 | Android Watch
- 3 | MacOS
- 4 | 企点
validations:
required: true
# Input | Reproduce
- type: textarea
id: reproduce-steps
attributes:
label: 重现步骤
description: |
我们需要执行哪些操作才能让 bug 出现?
简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
validations:
required: true
# Input | Expected result
- type: textarea
id: expected
attributes:
label: 期望的结果是什么?
validations:
required: true
# Input | Actual result
- type: textarea
id: actual
attributes:
label: 实际的结果是什么?
validations:
required: true
# Optional | Reproduce code
- type: textarea
id: reproduce-code
attributes:
label: 简单的复现代码/链接(可选)
render: golang
# Optional | Logging
- type: textarea
id: logging
attributes:
label: 日志记录(可选)
render: golang
# Optional | Extra description
- type: textarea
id: extra-desc
attributes:
label: 补充说明(可选)

View File

@ -7,9 +7,7 @@ on:
# Sequence of patterns matched against refs/tags # Sequence of patterns matched against refs/tags
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule:
# 每晚十点生成 nightly 镜像
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@ -5,8 +5,8 @@ on: [push, pull_request,workflow_dispatch]
env: env:
BINARY_PREFIX: "go-cqhttp_" BINARY_PREFIX: "go-cqhttp_"
BINARY_SUFFIX: "" BINARY_SUFFIX: ""
COMMIT_ID: "${{ github.sha }}"
PR_PROMPT: "::warning:: Build artifact will not be uploaded due to the workflow is trigged by pull request." PR_PROMPT: "::warning:: Build artifact will not be uploaded due to the workflow is trigged by pull request."
LD_FLAGS: "-w -s"
jobs: jobs:
build: build:
@ -46,6 +46,7 @@ jobs:
if $IS_PR ; then echo $PR_PROMPT; fi if $IS_PR ; then echo $PR_PROMPT; fi
export BINARY_NAME="$BINARY_PREFIX$GOOS_$GOARCH$BINARY_SUFFIX" export BINARY_NAME="$BINARY_PREFIX$GOOS_$GOARCH$BINARY_SUFFIX"
export CGO_ENABLED=0 export CGO_ENABLED=0
export LD_FLAGS="-w -s -X github.com/Mrs4s/go-cqhttp/internal/base.Version=${COMMIT_ID::7}"
go build -o "output/$BINARY_NAME" -trimpath -ldflags "$LD_FLAGS" . go build -o "output/$BINARY_NAME" -trimpath -ldflags "$LD_FLAGS" .
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

View File

@ -37,4 +37,5 @@ jobs:
if: ${{ github.event.pull_request }} if: ${{ github.event.pull_request }}
uses: reviewdog/action-suggester@v1 uses: reviewdog/action-suggester@v1
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tool_name: golangci-lint tool_name: golangci-lint

View File

@ -1,12 +0,0 @@
name: Issuebot
on:
issues:
types: [opened, edited]
jobs:
new_issue:
name: Run Issuebot on new Issue
runs-on: ubuntu-latest
steps:
- uses: wfjsw/actions-gocqhttp-issue-prefilter@master

View File

@ -28,6 +28,7 @@ linters:
- errcheck - errcheck
- exportloopref - exportloopref
- exhaustive - exhaustive
- bidichk
#- funlen #- funlen
#- goconst #- goconst
- gocritic - gocritic

View File

@ -22,10 +22,9 @@ type Param struct {
} }
type Router struct { type Router struct {
Func string Func string
Path string Path []string
Aliases []string Params []Param
Params []Param
} }
type generator struct { type generator struct {
@ -53,9 +52,12 @@ func (g *generator) generate(routers []Router) {
} }
func (g *generator) router(router Router) { func (g *generator) router(router Router) {
g.WriteString(`case ` + strconv.Quote(router.Path)) g.WriteString(`case `)
for _, alias := range router.Aliases { for i, p := range router.Path {
g.WriteString(`, ` + strconv.Quote(alias)) if i != 0 {
g.WriteString(`, `)
}
g.WriteString(strconv.Quote(p))
} }
g.WriteString(":\n") g.WriteString(":\n")
@ -92,10 +94,12 @@ func conv(v, t string) string {
return v + ".Bool()" return v + ".Bool()"
case "string": case "string":
return v + ".String()" return v + ".String()"
case "int32", "uint32", "int": case "int32", "int":
return t + "(" + v + ".Int())" return t + "(" + v + ".Int())"
case "uint64": case "uint64":
return v + ".Uint()" return v + ".Uint()"
case "uint32":
return "uint32(" + v + ".Uint())"
} }
} }
@ -112,7 +116,7 @@ func main() {
for _, decl := range file.Decls { for _, decl := range file.Decls {
switch decl := decl.(type) { switch decl := decl.(type) {
case *ast.FuncDecl: case *ast.FuncDecl:
if decl.Recv == nil { if !decl.Name.IsExported() || decl.Recv == nil {
continue continue
} }
if st, ok := decl.Recv.List[0].Type.(*ast.StarExpr); !ok || st.X.(*ast.Ident).Name != "CQBot" { if st, ok := decl.Recv.List[0].Type.(*ast.StarExpr); !ok || st.X.(*ast.Ident).Name != "CQBot" {
@ -137,12 +141,10 @@ func main() {
for _, comment := range decl.Doc.List { for _, comment := range decl.Doc.List {
annotation, args := match(comment.Text) annotation, args := match(comment.Text)
switch annotation { switch annotation {
case "":
continue
case "route": case "route":
router.Path = args for _, route := range strings.Split(args, ",") {
case "alias": router.Path = append(router.Path, unquote(route))
router.Aliases = append(router.Aliases, args) }
case "default": case "default":
for name, value := range parseMap(args, "=") { for name, value := range parseMap(args, "=") {
for i, p := range router.Params { for i, p := range router.Params {
@ -160,8 +162,11 @@ func main() {
} }
} }
} }
sort.Slice(router.Path, func(i, j int) bool {
return router.Path[i] < router.Path[j]
})
} }
if router.Path != "" { if router.Path != nil {
routers = append(routers, router) routers = append(routers, router)
} else { } else {
println(decl.Name.Name) println(decl.Name.Name)
@ -170,7 +175,7 @@ func main() {
} }
sort.Slice(routers, func(i, j int) bool { sort.Slice(routers, func(i, j int) bool {
return routers[i].Path < routers[j].Path return routers[i].Path[0] < routers[j].Path[0]
}) })
out := new(bytes.Buffer) out := new(bytes.Buffer)

View File

@ -1,4 +1,4 @@
package main package gocq
import ( import (
"bufio" "bufio"

404
cmd/gocq/main.go Normal file
View File

@ -0,0 +1,404 @@
// Package gocq 程序的主体部分
package gocq
import (
"crypto/aes"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"fmt"
"os"
"path"
"sync"
"time"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
para "github.com/fumiama/go-hide-param"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/term"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/global/terminal"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/cache"
"github.com/Mrs4s/go-cqhttp/internal/selfdiagnosis"
"github.com/Mrs4s/go-cqhttp/internal/selfupdate"
"github.com/Mrs4s/go-cqhttp/modules/servers"
"github.com/Mrs4s/go-cqhttp/server"
)
// 允许通过配置文件设置的状态列表
var allowStatus = [...]client.UserOnlineStatus{
client.StatusOnline, client.StatusAway, client.StatusInvisible, client.StatusBusy,
client.StatusListening, client.StatusConstellation, client.StatusWeather, client.StatusMeetSpring,
client.StatusTimi, client.StatusEatChicken, client.StatusLoving, client.StatusWangWang, client.StatusCookedRice,
client.StatusStudy, client.StatusStayUp, client.StatusPlayBall, client.StatusSignal, client.StatusStudyOnline,
client.StatusGaming, client.StatusVacationing, client.StatusWatchingTV, client.StatusFitness,
}
// Main 启动主程序
func Main() {
base.Parse()
if !base.FastStart && terminal.RunningByDoubleClick() {
err := terminal.NoMoreDoubleClick()
if err != nil {
log.Errorf("遇到错误: %v", err)
time.Sleep(time.Second * 5)
}
return
}
switch {
case base.LittleH:
base.Help()
case base.LittleD:
server.Daemon()
case base.LittleWD != "":
base.ResetWorkingDir()
}
base.Init()
rotateOptions := []rotatelogs.Option{
rotatelogs.WithRotationTime(time.Hour * 24),
}
rotateOptions = append(rotateOptions, rotatelogs.WithMaxAge(base.LogAging))
if base.LogForceNew {
rotateOptions = append(rotateOptions, rotatelogs.ForceNewFile())
}
w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotateOptions...)
if err != nil {
log.Errorf("rotatelogs init err: %v", err)
panic(err)
}
consoleFormatter := global.LogFormat{EnableColor: base.LogColorful}
fileFormatter := global.LogFormat{EnableColor: false}
log.AddHook(global.NewLocalHook(w, consoleFormatter, fileFormatter, global.GetLogLevel(base.LogLevel)...))
mkCacheDir := func(path string, _type string) {
if !global.PathExists(path) {
if err := os.MkdirAll(path, 0o755); err != nil {
log.Fatalf("创建%s缓存文件夹失败: %v", _type, err)
}
}
}
mkCacheDir(global.ImagePath, "图片")
mkCacheDir(global.VoicePath, "语音")
mkCacheDir(global.VideoPath, "视频")
mkCacheDir(global.CachePath, "发送图片")
mkCacheDir(path.Join(global.ImagePath, "guild-images"), "频道图片缓存")
cache.Init()
db.Init()
if err := db.Open(); err != nil {
log.Fatalf("打开数据库失败: %v", err)
}
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.SelfUpdate(arg[i+1])
} else {
selfupdate.SelfUpdate("")
}
case "key":
p := i + 1
if len(arg) > p {
byteKey = []byte(arg[p])
para.Hide(p)
}
}
}
}
if (base.Account.Uin == 0 || (base.Account.Password == "" && !base.Account.Encrypt)) && !global.PathExists("session.token") {
log.Warn("账号密码未配置, 将使用二维码登录.")
if !base.FastStart {
log.Warn("将在 5秒 后继续.")
time.Sleep(time.Second * 5)
}
}
log.Info("当前版本:", base.Version)
if base.Debug {
log.SetLevel(log.DebugLevel)
log.SetReportCaller(true)
log.Warnf("已开启Debug模式.")
// log.Debugf("开发交流群: 192548878")
}
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice()
_ = os.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0o644)
log.Info("已生成设备信息并保存到 device.json 文件.")
} else {
log.Info("将使用 device.json 内的设备信息运行Bot.")
if err := client.SystemDeviceInfo.ReadJson([]byte(global.ReadAllText("device.json"))); err != nil {
log.Fatalf("加载设备信息失败: %v", err)
}
}
if base.Account.Encrypt {
if !global.PathExists("password.encrypt") {
if base.Account.Password == "" {
log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.")
readLine()
os.Exit(0)
}
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
base.PasswordHash = md5.Sum([]byte(base.Account.Password))
_ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(base.PasswordHash[:], byteKey)), 0o644)
log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
} else {
if base.Account.Password != "" {
log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
}
if len(byteKey) == 0 {
log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
cancel := make(chan struct{}, 1)
state, _ := term.GetState(int(os.Stdin.Fd()))
go func() {
select {
case <-cancel:
return
case <-time.After(time.Second * 45):
log.Infof("解密key输入超时")
time.Sleep(3 * time.Second)
_ = term.Restore(int(os.Stdin.Fd()), state)
os.Exit(0)
}
}()
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
cancel <- struct{}{}
} else {
log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
}
encrypt, _ := os.ReadFile("password.encrypt")
ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
if err != nil {
log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
}
copy(base.PasswordHash[:], ph)
}
} else if len(base.Account.Password) > 0 {
base.PasswordHash = md5.Sum([]byte(base.Account.Password))
}
if !base.FastStart {
log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
time.Sleep(time.Second * 5)
}
log.Info("开始尝试登录并同步消息...")
log.Infof("使用协议: %s", client.SystemDeviceInfo.Protocol)
cli = newClient()
isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt
isTokenLogin := false
saveToken := func() {
base.AccountToken = cli.GenToken()
_ = os.WriteFile("session.token", base.AccountToken, 0o644)
}
if global.PathExists("session.token") {
token, err := os.ReadFile("session.token")
if err == nil {
if base.Account.Uin != 0 {
r := binary.NewReader(token)
cu := r.ReadInt64()
if cu != base.Account.Uin {
log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", base.Account.Uin, cu)
log.Warnf("1. 使用会话缓存继续.")
log.Warnf("2. 删除会话缓存并重启.")
log.Warnf("请选择: (5秒后自动选1)")
text := readLineTimeout(time.Second*5, "1")
if text == "2" {
_ = os.Remove("session.token")
log.Infof("缓存已删除.")
os.Exit(0)
}
}
}
if err = cli.TokenLogin(token); err != nil {
_ = os.Remove("session.token")
log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
time.Sleep(time.Second)
cli.Disconnect()
cli.Release()
cli = newClient()
} else {
isTokenLogin = true
}
}
}
if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} {
cli.Uin = base.Account.Uin
cli.PasswordMd5 = base.PasswordHash
}
if !isTokenLogin {
if !isQRCodeLogin {
if err := commonLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
} else {
if err := qrcodeLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
}
}
var times uint = 1 // 重试次数
var reLoginLock sync.Mutex
cli.OnDisconnected(func(q *client.QQClient, e *client.ClientDisconnectedEvent) {
reLoginLock.Lock()
defer reLoginLock.Unlock()
times = 1
if cli.Online.Load() {
return
}
log.Warnf("Bot已离线: %v", e.Message)
time.Sleep(time.Second * time.Duration(base.Reconnect.Delay))
for {
if base.Reconnect.Disabled {
log.Warnf("未启用自动重连, 将退出.")
os.Exit(1)
}
if times > base.Reconnect.MaxTimes && base.Reconnect.MaxTimes != 0 {
log.Fatalf("Bot重连次数超过限制, 停止")
}
times++
if base.Reconnect.Interval > 0 {
log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v/%v", base.Reconnect.Interval, times, base.Reconnect.MaxTimes)
time.Sleep(time.Second * time.Duration(base.Reconnect.Interval))
} else {
time.Sleep(time.Second)
}
if cli.Online.Load() {
log.Infof("登录已完成")
break
}
log.Warnf("尝试重连...")
err := cli.TokenLogin(base.AccountToken)
if err == nil {
saveToken()
return
}
log.Warnf("快速重连失败: %v", err)
if isQRCodeLogin {
log.Fatalf("快速重连失败, 扫码登录无法恢复会话.")
}
log.Warnf("快速重连失败, 尝试普通登录. 这可能是因为其他端强行T下线导致的.")
time.Sleep(time.Second)
if err := commonLogin(); err != nil {
log.Errorf("登录时发生致命错误: %v", err)
} else {
saveToken()
break
}
}
})
saveToken()
cli.AllowSlider = true
log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
log.Info("开始加载好友列表...")
global.Check(cli.ReloadFriendList(), true)
log.Infof("共加载 %v 个好友.", len(cli.FriendList))
log.Infof("开始加载群列表...")
global.Check(cli.ReloadGroupList(), true)
log.Infof("共加载 %v 个群.", len(cli.GroupList))
if uint(base.Account.Status) >= uint(len(allowStatus)) {
base.Account.Status = 0
}
cli.SetOnlineStatus(allowStatus[base.Account.Status])
servers.Run(coolq.NewQQBot(cli))
log.Info("资源初始化完成, 开始处理信息.")
log.Info("アトリは、高性能ですから!")
go selfupdate.CheckUpdate()
go func() {
time.Sleep(5 * time.Second)
go selfdiagnosis.NetworkDiagnosis(cli)
}()
<-global.SetupMainSignalHandler()
}
// 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
}
func newClient() *client.QQClient {
c := client.NewClientEmpty()
c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
if !base.UseSSOAddress {
log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
return false
}
log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
return true
})
if global.PathExists("address.txt") {
log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
addr := global.ReadAddrFile("address.txt")
if len(addr) > 0 {
c.SetCustomServer(addr)
}
log.Infof("读取到 %v 个自定义地址.", len(addr))
}
c.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)
case "DUMP":
if !global.PathExists(global.DumpsPath) {
_ = os.MkdirAll(global.DumpsPath, 0o755)
}
dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix()))
log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", e.Message, dumpFile)
_ = os.WriteFile(dumpFile, e.Dump, 0o644)
}
})
return c
}

View File

@ -12,8 +12,11 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/segmentio/asm/base64"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/message"
@ -29,6 +32,19 @@ import (
"github.com/Mrs4s/go-cqhttp/modules/filter" "github.com/Mrs4s/go-cqhttp/modules/filter"
) )
type guildMemberPageToken struct {
guildID uint64
nextIndex uint32
nextRoleID uint64
nextQueryParam string
}
var defaultPageToken = &guildMemberPageToken{
guildID: 0,
nextIndex: 0,
nextRoleID: 2,
}
// CQGetLoginInfo 获取登录号信息 // CQGetLoginInfo 获取登录号信息
// //
// https://git.io/Jtz1I // https://git.io/Jtz1I
@ -55,7 +71,7 @@ func (bot *CQBot) CQGetQiDianAccountInfo() global.MSG {
func (bot *CQBot) CQGetGuildServiceProfile() global.MSG { func (bot *CQBot) CQGetGuildServiceProfile() global.MSG {
return OK(global.MSG{ return OK(global.MSG{
"nickname": bot.Client.GuildService.Nickname, "nickname": bot.Client.GuildService.Nickname,
"tiny_id": bot.Client.GuildService.TinyId, "tiny_id": fU64(bot.Client.GuildService.TinyId),
"avatar_url": bot.Client.GuildService.AvatarUrl, "avatar_url": bot.Client.GuildService.AvatarUrl,
}) })
} }
@ -76,9 +92,9 @@ func (bot *CQBot) CQGetGuildList() global.MSG {
} }
*/ */
fs = append(fs, global.MSG{ fs = append(fs, global.MSG{
"guild_id": info.GuildId, "guild_id": fU64(info.GuildId),
"guild_name": info.GuildName, "guild_name": info.GuildName,
"guild_display_id": info.GuildCode, "guild_display_id": fU64(info.GuildCode),
// "channels": channels, // "channels": channels,
}) })
} }
@ -94,7 +110,7 @@ func (bot *CQBot) CQGetGuildMetaByGuest(guildID uint64) global.MSG {
return Failed(100, "API_ERROR", err.Error()) return Failed(100, "API_ERROR", err.Error())
} }
return OK(global.MSG{ return OK(global.MSG{
"guild_id": meta.GuildId, "guild_id": fU64(meta.GuildId),
"guild_name": meta.GuildName, "guild_name": meta.GuildName,
"guild_profile": meta.GuildProfile, "guild_profile": meta.GuildProfile,
"create_time": meta.CreateTime, "create_time": meta.CreateTime,
@ -102,7 +118,7 @@ func (bot *CQBot) CQGetGuildMetaByGuest(guildID uint64) global.MSG {
"max_robot_count": meta.MaxRobotCount, "max_robot_count": meta.MaxRobotCount,
"max_admin_count": meta.MaxAdminCount, "max_admin_count": meta.MaxAdminCount,
"member_count": meta.MemberCount, "member_count": meta.MemberCount,
"owner_id": meta.OwnerId, "owner_id": fU64(meta.OwnerId),
}) })
} }
@ -116,7 +132,7 @@ func (bot *CQBot) CQGetGuildChannelList(guildID uint64, noCache bool) global.MSG
if noCache { if noCache {
channels, err := bot.Client.GuildService.FetchChannelList(guildID) channels, err := bot.Client.GuildService.FetchChannelList(guildID)
if err != nil { if err != nil {
log.Errorf("获取频道 %v 子频道列表时出现错误: %v", guildID, err) log.Warnf("获取频道 %v 子频道列表时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error()) return Failed(100, "API_ERROR", err.Error())
} }
guild.Channels = channels guild.Channels = channels
@ -129,31 +145,184 @@ func (bot *CQBot) CQGetGuildChannelList(guildID uint64, noCache bool) global.MSG
} }
// CQGetGuildMembers 获取频道成员列表 // CQGetGuildMembers 获取频道成员列表
// @route(get_guild_members) // @route(get_guild_member_list)
func (bot *CQBot) CQGetGuildMembers(guildID uint64) global.MSG { func (bot *CQBot) CQGetGuildMembers(guildID uint64, nextToken string) global.MSG {
guild := bot.Client.GuildService.FindGuild(guildID) guild := bot.Client.GuildService.FindGuild(guildID)
if guild == nil { if guild == nil {
return Failed(100, "GUILD_NOT_FOUND") return Failed(100, "GUILD_NOT_FOUND")
} }
members := make([]global.MSG, len(guild.Members)) token := defaultPageToken
bots := make([]global.MSG, len(guild.Bots)) if nextToken != "" {
admins := make([]global.MSG, len(guild.Admins)) i, exists := bot.nextTokenCache.Get(nextToken)
for i, m := range guild.Members { if !exists {
members[i] = convertGuildMemberInfo(m) return Failed(100, "NEXT_TOKEN_NOT_EXISTS")
}
token = i.(*guildMemberPageToken)
if token.guildID != guildID {
return Failed(100, "GUILD_NOT_MATCH")
}
} }
for i, m := range guild.Bots { ret, err := bot.Client.GuildService.FetchGuildMemberListWithRole(guildID, 0, token.nextIndex, token.nextRoleID, token.nextQueryParam)
bots[i] = convertGuildMemberInfo(m) if err != nil {
return Failed(100, "API_ERROR", err.Error())
} }
for i, m := range guild.Admins { res := global.MSG{
admins[i] = convertGuildMemberInfo(m) "members": convertGuildMemberInfo(ret.Members),
"finished": ret.Finished,
"next_token": nil,
}
if !ret.Finished {
next := &guildMemberPageToken{
guildID: guildID,
nextIndex: ret.NextIndex,
nextRoleID: ret.NextRoleId,
nextQueryParam: ret.NextQueryParam,
}
id := base64.StdEncoding.EncodeToString(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt64(uint64(time.Now().UnixNano()))
w.WriteString(utils.RandomString(5))
}))
bot.nextTokenCache.Add(id, next, time.Minute*10)
res["next_token"] = id
}
return OK(res)
}
// CQGetGuildMemberProfile 获取频道成员资料
// @route(get_guild_member_profile)
func (bot *CQBot) CQGetGuildMemberProfile(guildID, userID uint64) global.MSG {
if bot.Client.GuildService.FindGuild(guildID) == nil {
return Failed(100, "GUILD_NOT_FOUND")
}
profile, err := bot.Client.GuildService.FetchGuildMemberProfileInfo(guildID, userID)
if err != nil {
log.Warnf("获取频道 %v 成员 %v 资料时出现错误: %v", guildID, userID, err)
return Failed(100, "API_ERROR", err.Error())
}
roles := make([]global.MSG, 0, len(profile.Roles))
for _, role := range profile.Roles {
roles = append(roles, global.MSG{
"role_id": fU64(role.RoleId),
"role_name": role.RoleName,
})
} }
return OK(global.MSG{ return OK(global.MSG{
"members": members, "tiny_id": fU64(profile.TinyId),
"bots": bots, "nickname": profile.Nickname,
"admins": admins, "avatar_url": profile.AvatarUrl,
"join_time": profile.JoinTime,
"roles": roles,
}) })
} }
// CQGetGuildRoles 获取频道角色列表
// @route(get_guild_roles)
func (bot *CQBot) CQGetGuildRoles(guildID uint64) global.MSG {
r, err := bot.Client.GuildService.GetGuildRoles(guildID)
if err != nil {
log.Warnf("获取频道 %v 角色列表时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error())
}
roles := make([]global.MSG, len(r))
for i, role := range r {
roles[i] = global.MSG{
"role_id": fU64(role.RoleId),
"role_name": role.RoleName,
"argb_color": role.ArgbColor,
"independent": role.Independent,
"member_count": role.Num,
"max_count": role.MaxNum,
"owned": role.Owned,
"disabled": role.Disabled,
}
}
return OK(roles)
}
// CQCreateGuildRole 创建频道角色
// @route(create_guild_role)
func (bot *CQBot) CQCreateGuildRole(guildID uint64, name string, color uint32, independent bool, initialUsers gjson.Result) global.MSG {
userSlice := []uint64{}
if initialUsers.IsArray() {
for _, user := range initialUsers.Array() {
userSlice = append(userSlice, user.Uint())
}
}
role, err := bot.Client.GuildService.CreateGuildRole(guildID, name, color, independent, userSlice)
if err != nil {
log.Warnf("创建频道 %v 角色时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error())
}
return OK(global.MSG{
"role_id": fU64(role),
})
}
// CQDeleteGuildRole 删除频道角色
// @route(delete_guild_role)
func (bot *CQBot) CQDeleteGuildRole(guildID uint64, roleID uint64) global.MSG {
err := bot.Client.GuildService.DeleteGuildRole(guildID, roleID)
if err != nil {
log.Warnf("删除频道 %v 角色时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error())
}
return OK(nil)
}
// CQSetGuildMemberRole 设置用户在频道中的角色
// @route(set_guild_member_role)
func (bot *CQBot) CQSetGuildMemberRole(guildID uint64, set bool, roleID uint64, users gjson.Result) global.MSG {
userSlice := []uint64{}
if users.IsArray() {
for _, user := range users.Array() {
userSlice = append(userSlice, user.Uint())
}
}
err := bot.Client.GuildService.SetUserRoleInGuild(guildID, set, roleID, userSlice)
if err != nil {
log.Warnf("设置用户在频道 %v 中的角色时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error())
}
return OK(nil)
}
// CQModifyRoleInGuild 修改频道角色
// @route(update_guild_role)
func (bot *CQBot) CQModifyRoleInGuild(guildID uint64, roleID uint64, name string, color uint32, indepedent bool) global.MSG {
err := bot.Client.GuildService.ModifyRoleInGuild(guildID, roleID, name, color, indepedent)
if err != nil {
log.Warnf("修改频道 %v 角色时出现错误: %v", guildID, err)
return Failed(100, "API_ERROR", err.Error())
}
return OK(nil)
}
// CQGetTopicChannelFeeds 获取话题频道帖子列表
// @route(get_topic_channel_feeds)
func (bot *CQBot) CQGetTopicChannelFeeds(guildID, channelID uint64) global.MSG {
guild := bot.Client.GuildService.FindGuild(guildID)
if guild == nil {
return Failed(100, "GUILD_NOT_FOUND")
}
channel := guild.FindChannel(channelID)
if channel == nil {
return Failed(100, "CHANNEL_NOT_FOUND")
}
if channel.ChannelType != client.ChannelTypeTopic {
return Failed(100, "CHANNEL_TYPE_ERROR")
}
feeds, err := bot.Client.GuildService.GetTopicChannelFeeds(guildID, channelID)
if err != nil {
log.Warnf("获取频道 %v 帖子时出现错误: %v", channelID, err)
return Failed(100, "API_ERROR", err.Error())
}
c := make([]global.MSG, 0, len(feeds))
for _, feed := range feeds {
c = append(c, convertChannelFeedInfo(feed))
}
return OK(c)
}
// CQGetFriendList 获取好友列表 // CQGetFriendList 获取好友列表
// //
// https://git.io/Jtz1L // https://git.io/Jtz1L
@ -176,7 +345,7 @@ func (bot *CQBot) CQGetFriendList() global.MSG {
func (bot *CQBot) CQGetUnidirectionalFriendList() global.MSG { func (bot *CQBot) CQGetUnidirectionalFriendList() global.MSG {
list, err := bot.Client.GetUnidirectionalFriendList() list, err := bot.Client.GetUnidirectionalFriendList()
if err != nil { if err != nil {
log.Errorf("获取单向好友列表时出现错误: %v", err) log.Warnf("获取单向好友列表时出现错误: %v", err)
return Failed(100, "API_ERROR", err.Error()) return Failed(100, "API_ERROR", err.Error())
} }
fs := make([]global.MSG, 0, len(list)) fs := make([]global.MSG, 0, len(list))
@ -197,13 +366,13 @@ func (bot *CQBot) CQGetUnidirectionalFriendList() global.MSG {
func (bot *CQBot) CQDeleteUnidirectionalFriend(uin int64) global.MSG { func (bot *CQBot) CQDeleteUnidirectionalFriend(uin int64) global.MSG {
list, err := bot.Client.GetUnidirectionalFriendList() list, err := bot.Client.GetUnidirectionalFriendList()
if err != nil { if err != nil {
log.Errorf("获取单向好友列表时出现错误: %v", err) log.Warnf("获取单向好友列表时出现错误: %v", err)
return Failed(100, "API_ERROR", err.Error()) return Failed(100, "API_ERROR", err.Error())
} }
for _, f := range list { for _, f := range list {
if f.Uin == uin { if f.Uin == uin {
if err = bot.Client.DeleteUnidirectionalFriend(uin); err != nil { if err = bot.Client.DeleteUnidirectionalFriend(uin); err != nil {
log.Errorf("删除单向好友时出现错误: %v", err) log.Warnf("删除单向好友时出现错误: %v", err)
return Failed(100, "API_ERROR", err.Error()) return Failed(100, "API_ERROR", err.Error())
} }
return OK(nil) return OK(nil)
@ -220,7 +389,7 @@ func (bot *CQBot) CQDeleteFriend(uin int64) global.MSG {
return Failed(100, "FRIEND_NOT_FOUND", "好友不存在") return Failed(100, "FRIEND_NOT_FOUND", "好友不存在")
} }
if err := bot.Client.DeleteFriend(uin); err != nil { if err := bot.Client.DeleteFriend(uin); err != nil {
log.Errorf("删除好友时出现错误: %v", err) log.Warnf("删除好友时出现错误: %v", err)
return Failed(100, "DELETE_API_ERROR", err.Error()) return Failed(100, "DELETE_API_ERROR", err.Error())
} }
return OK(nil) return OK(nil)
@ -348,7 +517,7 @@ func (bot *CQBot) CQGetGroupMemberInfo(groupID, userID int64, noCache bool) glob
func (bot *CQBot) CQGetGroupFileSystemInfo(groupID int64) global.MSG { func (bot *CQBot) CQGetGroupFileSystemInfo(groupID int64) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
return OK(fs) return OK(fs)
@ -361,12 +530,12 @@ func (bot *CQBot) CQGetGroupFileSystemInfo(groupID int64) global.MSG {
func (bot *CQBot) CQGetGroupRootFiles(groupID int64) global.MSG { func (bot *CQBot) CQGetGroupRootFiles(groupID int64) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
files, folders, err := fs.Root() files, folders, err := fs.Root()
if err != nil { if err != nil {
log.Errorf("获取群 %v 根目录文件失败: %v", groupID, err) log.Warnf("获取群 %v 根目录文件失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
return OK(global.MSG{ return OK(global.MSG{
@ -382,12 +551,12 @@ func (bot *CQBot) CQGetGroupRootFiles(groupID int64) global.MSG {
func (bot *CQBot) CQGetGroupFilesByFolderID(groupID int64, folderID string) global.MSG { func (bot *CQBot) CQGetGroupFilesByFolderID(groupID int64, folderID string) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
files, folders, err := fs.GetFilesByFolder(folderID) files, folders, err := fs.GetFilesByFolder(folderID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 根目录 %v 子文件失败: %v", groupID, folderID, err) log.Warnf("获取群 %v 根目录 %v 子文件失败: %v", groupID, folderID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
return OK(global.MSG{ return OK(global.MSG{
@ -400,6 +569,7 @@ func (bot *CQBot) CQGetGroupFilesByFolderID(groupID int64, folderID string) glob
// //
// https://docs.go-cqhttp.org/api/#%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 // https://docs.go-cqhttp.org/api/#%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
// @route(get_group_file_url) // @route(get_group_file_url)
// @rename(bus_id->"[busid\x2Cbus_id].0")
func (bot *CQBot) CQGetGroupFileURL(groupID int64, fileID string, busID int32) global.MSG { func (bot *CQBot) CQGetGroupFileURL(groupID int64, fileID string, busID int32) global.MSG {
url := bot.Client.GetGroupFileUrl(groupID, fileID, busID) url := bot.Client.GetGroupFileUrl(groupID, fileID, busID)
if url == "" { if url == "" {
@ -416,19 +586,19 @@ func (bot *CQBot) CQGetGroupFileURL(groupID int64, fileID string, busID int32) g
// @route(upload_group_file) // @route(upload_group_file)
func (bot *CQBot) CQUploadGroupFile(groupID int64, file, name, folder string) global.MSG { func (bot *CQBot) CQUploadGroupFile(groupID int64, file, name, folder string) global.MSG {
if !global.PathExists(file) { if !global.PathExists(file) {
log.Errorf("上传群文件 %v 失败: 文件不存在", file) log.Warnf("上传群文件 %v 失败: 文件不存在", file)
return Failed(100, "FILE_NOT_FOUND", "文件不存在") return Failed(100, "FILE_NOT_FOUND", "文件不存在")
} }
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
if folder == "" { if folder == "" {
folder = "/" folder = "/"
} }
if err = fs.UploadFile(file, name, folder); err != nil { if err = fs.UploadFile(file, name, folder); err != nil {
log.Errorf("上传群 %v 文件 %v 失败: %v", groupID, file, err) log.Warnf("上传群 %v 文件 %v 失败: %v", groupID, file, err)
return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error())
} }
return OK(nil) return OK(nil)
@ -440,11 +610,11 @@ func (bot *CQBot) CQUploadGroupFile(groupID int64, file, name, folder string) gl
func (bot *CQBot) CQGroupFileCreateFolder(groupID int64, parentID, name string) global.MSG { func (bot *CQBot) CQGroupFileCreateFolder(groupID int64, parentID, name string) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
if err = fs.CreateFolder(parentID, name); err != nil { if err = fs.CreateFolder(parentID, name); err != nil {
log.Errorf("创建群 %v 文件夹失败: %v", groupID, err) log.Warnf("创建群 %v 文件夹失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
return OK(nil) return OK(nil)
@ -457,11 +627,11 @@ func (bot *CQBot) CQGroupFileCreateFolder(groupID int64, parentID, name string)
func (bot *CQBot) CQGroupFileDeleteFolder(groupID int64, id string) global.MSG { func (bot *CQBot) CQGroupFileDeleteFolder(groupID int64, id string) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
if err = fs.DeleteFolder(id); err != nil { if err = fs.DeleteFolder(id); err != nil {
log.Errorf("删除群 %v 文件夹 %v 时出现文件: %v", groupID, id, err) log.Warnf("删除群 %v 文件夹 %v 时出现文件: %v", groupID, id, err)
return Failed(200, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(200, "FILE_SYSTEM_API_ERROR", err.Error())
} }
return OK(nil) return OK(nil)
@ -470,15 +640,15 @@ func (bot *CQBot) CQGroupFileDeleteFolder(groupID int64, id string) global.MSG {
// CQGroupFileDeleteFile 拓展API-删除群文件 // CQGroupFileDeleteFile 拓展API-删除群文件
// //
// @route(delete_group_file) // @route(delete_group_file)
// @rename(id->file_id) // @rename(id->file_id, bus_id->"[busid\x2Cbus_id].0")
func (bot *CQBot) CQGroupFileDeleteFile(groupID int64, id string, busID int32) global.MSG { func (bot *CQBot) CQGroupFileDeleteFile(groupID int64, id string, busID int32) global.MSG {
fs, err := bot.Client.GetGroupFileSystem(groupID) fs, err := bot.Client.GetGroupFileSystem(groupID)
if err != nil { if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupID, err) log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error()) return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
} }
if res := fs.DeleteFile("", id, busID); res != "" { if res := fs.DeleteFile("", id, busID); res != "" {
log.Errorf("删除群 %v 文件 %v 时出现文件: %v", groupID, id, res) log.Warnf("删除群 %v 文件 %v 时出现文件: %v", groupID, id, res)
return Failed(200, "FILE_SYSTEM_API_ERROR", res) return Failed(200, "FILE_SYSTEM_API_ERROR", res)
} }
return OK(nil) return OK(nil)
@ -584,7 +754,7 @@ func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.R
fixAt := func(elem []message.IMessageElement) { fixAt := func(elem []message.IMessageElement) {
for _, e := range elem { for _, e := range elem {
if at, ok := e.(*message.AtElement); ok && at.Target != 0 && at.Display == "" { if at, ok := e.(*message.AtElement); ok && at.Target != 0 && at.Display == "" {
mem := guild.FindMember(uint64(at.Target)) mem, _ := bot.Client.GuildService.FetchGuildMemberProfileInfo(guildID, uint64(at.Target))
if mem != nil { if mem != nil {
at.Display = "@" + mem.Nickname at.Display = "@" + mem.Nickname
} else { } else {
@ -618,43 +788,33 @@ func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.R
return OK(global.MSG{"message_id": mid}) return OK(global.MSG{"message_id": mid})
} }
// CQSendGroupForwardMessage 扩展API-发送合并转发(群) func (bot *CQBot) uploadForwardElement(m gjson.Result, groupID int64) *message.ForwardElement {
//
// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4
// @route(send_group_forward_msg)
// @rename(m->messages)
func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) global.MSG {
if m.Type != gjson.JSON {
return Failed(100)
}
fm := message.NewForwardMessage()
ts := time.Now().Add(-time.Minute * 5) ts := time.Now().Add(-time.Minute * 5)
hasCustom := false fm := message.NewForwardMessage()
m.ForEach(func(_, item gjson.Result) bool {
if item.Get("data.uin").Exists() || item.Get("data.user_id").Exists() {
hasCustom = true
return false
}
return true
})
var lazyUpload []func()
var wg sync.WaitGroup
resolveElement := func(elems []message.IMessageElement) []message.IMessageElement { resolveElement := func(elems []message.IMessageElement) []message.IMessageElement {
for i, elem := range elems { for i, elem := range elems {
switch elem.(type) { iescape := i
switch o := elem.(type) {
case *LocalImageElement, *LocalVideoElement: case *LocalImageElement, *LocalVideoElement:
gm, err := bot.uploadMedia(elem, groupID, true) wg.Add(1)
if err != nil { lazyUpload = append(lazyUpload, func() {
log.Warnf("警告: 群 %d %s上传失败: %v", groupID, elem.Type().String(), err) defer wg.Done()
continue gm, err := bot.uploadMedia(o, groupID, true)
} if err != nil {
elems[i] = gm log.Warnf("警告: 群 %d %s上传失败: %v", groupID, o.Type().String(), err)
} else {
elems[iescape] = gm
}
})
} }
} }
return elems return elems
} }
var convert func(e gjson.Result) *message.ForwardNode convert := func(e gjson.Result) *message.ForwardNode {
convert = func(e gjson.Result) *message.ForwardNode {
if e.Get("type").Str != "node" { if e.Get("type").Str != "node" {
return nil return nil
} }
@ -668,7 +828,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa
SenderName: m.Attribute.SenderName, SenderName: m.Attribute.SenderName,
Time: func() int32 { Time: func() int32 {
msgTime := m.Attribute.Timestamp msgTime := m.Attribute.Timestamp
if hasCustom && msgTime == 0 { if msgTime == 0 {
return int32(ts.Unix()) return int32(ts.Unix())
} }
return int32(msgTime) return int32(msgTime)
@ -696,23 +856,16 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa
return true return true
}) })
if nested { // 处理嵌套 if nested { // 处理嵌套
nest := message.NewForwardMessage() fe := bot.uploadForwardElement(c, groupID)
for _, item := range c.Array() {
node := convert(item)
if node != nil {
nest.AddNode(node)
}
}
elem := bot.Client.UploadGroupForwardMessage(groupID, nest)
return &message.ForwardNode{ return &message.ForwardNode{
SenderId: uin, SenderId: uin,
SenderName: name, SenderName: name,
Time: int32(msgTime), Time: int32(msgTime),
Message: []message.IMessageElement{elem}, Message: []message.IMessageElement{fe},
} }
} }
} }
content := bot.ConvertObjectMessage(e.Get("data.content"), MessageSourceGroup) content := bot.ConvertObjectMessage(c, MessageSourceGroup)
if uin != 0 && name != "" && len(content) > 0 { if uin != 0 && name != "" && len(content) > 0 {
return &message.ForwardNode{ return &message.ForwardNode{
SenderId: uin, SenderId: uin,
@ -724,6 +877,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa
log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content)) log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content))
return nil return nil
} }
if m.IsArray() { if m.IsArray() {
for _, item := range m.Array() { for _, item := range m.Array() {
node := convert(item) node := convert(item)
@ -737,8 +891,27 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa
fm.AddNode(node) fm.AddNode(node)
} }
} }
if fm.Length() > 0 {
fe := bot.Client.UploadGroupForwardMessage(groupID, fm) for _, upload := range lazyUpload {
go upload()
}
wg.Wait()
return bot.Client.UploadGroupForwardMessage(groupID, fm)
}
// CQSendGroupForwardMessage 扩展API-发送合并转发(群)
//
// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4
// @route(send_group_forward_msg)
// @rename(m->messages)
func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) global.MSG {
if m.Type != gjson.JSON {
return Failed(100)
}
fe := bot.uploadForwardElement(m, groupID)
if fe != nil {
ret := bot.Client.SendGroupForwardMessage(groupID, fe) ret := bot.Client.SendGroupForwardMessage(groupID, fe)
if ret == nil || ret.Id == -1 { if ret == nil || ret.Id == -1 {
log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.") log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.")
@ -797,6 +970,7 @@ func (bot *CQBot) CQSetGroupCard(groupID, userID int64, card string) global.MSG
// //
// https://git.io/Jtz10 // https://git.io/Jtz10
// @route(set_group_special_title) // @route(set_group_special_title)
// @rename(title->special_title)
func (bot *CQBot) CQSetGroupSpecialTitle(groupID, userID int64, title string) global.MSG { func (bot *CQBot) CQSetGroupSpecialTitle(groupID, userID int64, title string) global.MSG {
if g := bot.Client.FindGroup(groupID); g != nil { if g := bot.Client.FindGroup(groupID); g != nil {
if m := g.FindMember(userID); m != nil { if m := g.FindMember(userID); m != nil {
@ -956,14 +1130,14 @@ func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) global.MSG {
func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bool) global.MSG { func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bool) global.MSG {
msgs, err := bot.Client.GetGroupSystemMessages() msgs, err := bot.Client.GetGroupSystemMessages()
if err != nil { if err != nil {
log.Errorf("获取群系统消息失败: %v", err) log.Warnf("获取群系统消息失败: %v", err)
return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error()) return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error())
} }
if subType == "add" { if subType == "add" {
for _, req := range msgs.JoinRequests { for _, req := range msgs.JoinRequests {
if strconv.FormatInt(req.RequestId, 10) == flag { if strconv.FormatInt(req.RequestId, 10) == flag {
if req.Checked { if req.Checked {
log.Errorf("处理群系统消息失败: 无法操作已处理的消息.") log.Warnf("处理群系统消息失败: 无法操作已处理的消息.")
return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理") return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
} }
if approve { if approve {
@ -978,7 +1152,7 @@ func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bo
for _, req := range msgs.InvitedRequests { for _, req := range msgs.InvitedRequests {
if strconv.FormatInt(req.RequestId, 10) == flag { if strconv.FormatInt(req.RequestId, 10) == flag {
if req.Checked { if req.Checked {
log.Errorf("处理群系统消息失败: 无法操作已处理的消息.") log.Warnf("处理群系统消息失败: 无法操作已处理的消息.")
return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理") return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
} }
if approve { if approve {
@ -990,7 +1164,7 @@ func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bo
} }
} }
} }
log.Errorf("处理群系统消息失败: 消息 %v 不存在.", flag) log.Warnf("处理群系统消息失败: 消息 %v 不存在.", flag)
return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在") return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在")
} }
@ -1254,7 +1428,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) global
func (bot *CQBot) CQGetImage(file string) global.MSG { func (bot *CQBot) CQGetImage(file string) global.MSG {
var b []byte var b []byte
var err error var err error
if cache.EnableCacheDB && strings.HasSuffix(file, ".image") { if strings.HasSuffix(file, ".image") {
var f []byte var f []byte
f, err = hex.DecodeString(strings.TrimSuffix(file, ".image")) f, err = hex.DecodeString(strings.TrimSuffix(file, ".image"))
b = cache.Image.Get(f) b = cache.Image.Get(f)
@ -1275,7 +1449,7 @@ func (bot *CQBot) CQGetImage(file string) global.MSG {
"filename": r.ReadString(), "filename": r.ReadString(),
"url": r.ReadString(), "url": r.ReadString(),
} }
local := path.Join(global.CachePath, file+"."+path.Ext(msg["filename"].(string))) local := path.Join(global.CachePath, file+path.Ext(msg["filename"].(string)))
if !global.PathExists(local) { if !global.PathExists(local) {
if body, err := global.HTTPGetReadCloser(msg["url"].(string)); err == nil { if body, err := global.HTTPGetReadCloser(msg["url"].(string)); err == nil {
f, _ := os.OpenFile(local, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o0644) f, _ := os.OpenFile(local, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o0644)
@ -1345,20 +1519,31 @@ func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG {
if m == nil { if m == nil {
return Failed(100, "MSG_NOT_FOUND", "消息不存在") return Failed(100, "MSG_NOT_FOUND", "消息不存在")
} }
r := make([]global.MSG, 0, len(m.Nodes))
for _, n := range m.Nodes { var transformNodes func(nodes []*message.ForwardNode) []global.MSG
bot.checkMedia(n.Message, 0) transformNodes = func(nodes []*message.ForwardNode) []global.MSG {
r = append(r, global.MSG{ r := make([]global.MSG, len(nodes))
"sender": global.MSG{ for i, n := range nodes {
"user_id": n.SenderId, bot.checkMedia(n.Message, 0)
"nickname": n.SenderName, content := ToFormattedMessage(n.Message, MessageSource{SourceType: MessageSourceGroup}, false)
}, if len(n.Message) == 1 {
"time": n.Time, if forward, ok := n.Message[0].(*message.ForwardMessage); ok {
"content": ToFormattedMessage(n.Message, MessageSource{SourceType: MessageSourceGroup}, false), content = transformNodes(forward.Nodes)
}) }
}
r[i] = global.MSG{
"sender": global.MSG{
"user_id": n.SenderId,
"nickname": n.SenderName,
},
"time": n.Time,
"content": content,
}
}
return r
} }
return OK(global.MSG{ return OK(global.MSG{
"messages": r, "messages": transformNodes(m.Nodes),
}) })
} }
@ -1395,6 +1580,70 @@ func (bot *CQBot) CQGetMessage(messageID int32) global.MSG {
return OK(m) return OK(m)
} }
// CQGetGuildMessage 获取频道消息
// @route(get_guild_msg)
func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) global.MSG {
source, seq := decodeGuildMessageID(messageID)
if source == nil {
log.Warnf("获取消息时出现错误: 无效消息ID")
return Failed(100, "INVALID_MESSAGE_ID", "无效消息ID")
}
m := global.MSG{
"message_id": messageID,
"message_source": func() string {
if source.SourceType == MessageSourceGuildDirect {
return "direct"
}
return "channel"
}(),
"message_seq": seq,
"guild_id": fU64(source.PrimaryID),
"reactions": []int{},
}
// nolint: exhaustive
switch source.SourceType {
case MessageSourceGuildChannel:
m["channel_id"] = fU64(source.SubID)
if noCache {
pull, err := bot.Client.GuildService.PullGuildChannelMessage(source.PrimaryID, source.SubID, seq, seq)
if err != nil {
log.Warnf("获取消息时出现错误: %v", err)
return Failed(100, "API_ERROR", err.Error())
}
if len(m) == 0 {
log.Warnf("获取消息时出现错误: 消息不存在")
return Failed(100, "MSG_NOT_FOUND", "消息不存在")
}
m["time"] = pull[0].Time
m["sender"] = global.MSG{
"user_id": pull[0].Sender.TinyId,
"tiny_id": fU64(pull[0].Sender.TinyId),
"nickname": pull[0].Sender.Nickname,
}
m["message"] = ToFormattedMessage(pull[0].Elements, *source, false)
m["reactions"] = convertReactions(pull[0].Reactions)
bot.InsertGuildChannelMessage(pull[0])
} else {
channelMsgByDB, err := db.GetGuildChannelMessageByID(messageID)
if err != nil {
log.Warnf("获取消息时出现错误: %v", err)
return Failed(100, "MSG_NOT_FOUND", "消息不存在")
}
m["time"] = channelMsgByDB.Attribute.Timestamp
m["sender"] = global.MSG{
"user_id": channelMsgByDB.Attribute.SenderTinyID,
"tiny_id": fU64(channelMsgByDB.Attribute.SenderTinyID),
"nickname": channelMsgByDB.Attribute.SenderName,
}
m["message"] = ToFormattedMessage(bot.ConvertContentMessage(channelMsgByDB.Content, MessageSourceGuildChannel), *source)
}
case MessageSourceGuildDirect:
// todo(mrs4s): 支持 direct 消息
m["tiny_id"] = fU64(source.SubID)
}
return OK(m)
}
// CQGetGroupSystemMessages 扩展API-获取群文件系统消息 // CQGetGroupSystemMessages 扩展API-获取群文件系统消息
// //
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF // https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF
@ -1485,8 +1734,7 @@ func (bot *CQBot) CQCanSendRecord() global.MSG {
// CQOcrImage 扩展API-图片OCR // CQOcrImage 扩展API-图片OCR
// //
// https://docs.go-cqhttp.org/api/#%E5%9B%BE%E7%89%87-ocr // https://docs.go-cqhttp.org/api/#%E5%9B%BE%E7%89%87-ocr
// @route(ocr_image) // @route(ocr_image,".ocr_image")
// @alias(.ocr_image)
// @rename(image_id->image) // @rename(image_id->image)
func (bot *CQBot) CQOcrImage(imageID string) global.MSG { func (bot *CQBot) CQOcrImage(imageID string) global.MSG {
img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageID}, false, MessageSourceGroup) img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageID}, false, MessageSourceGroup)
@ -1554,8 +1802,8 @@ func (bot *CQBot) CQGetStatus() global.MSG {
"app_enabled": true, "app_enabled": true,
"plugins_good": nil, "plugins_good": nil,
"app_good": true, "app_good": true,
"online": bot.Client.Online, "online": bot.Client.Online.Load(),
"good": bot.Client.Online, "good": bot.Client.Online.Load(),
"stat": bot.Client.GetStatistics(), "stat": bot.Client.GetStatistics(),
}) })
} }
@ -1653,7 +1901,7 @@ func (bot *CQBot) CQGetVersionInfo() global.MSG {
"version": base.Version, "version": base.Version,
"protocol": func() int { "protocol": func() int {
switch client.SystemDeviceInfo.Protocol { switch client.SystemDeviceInfo.Protocol {
case client.IPad: case client.Unset, client.IPad:
return 0 return 0
case client.AndroidPhone: case client.AndroidPhone:
return 1 return 1

View File

@ -12,15 +12,15 @@ import (
"sync" "sync"
"time" "time"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/MiraiGo/utils" "github.com/Mrs4s/MiraiGo/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/segmentio/asm/base64"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base" "github.com/Mrs4s/go-cqhttp/internal/base"
) )
@ -34,6 +34,7 @@ type CQBot struct {
friendReqCache sync.Map friendReqCache sync.Map
tempSessionCache sync.Map tempSessionCache sync.Map
nextTokenCache *utils.Cache
} }
// Event 事件 // Event 事件
@ -67,7 +68,8 @@ func (e *Event) JSONString() string {
// NewQQBot 初始化一个QQBot实例 // NewQQBot 初始化一个QQBot实例
func NewQQBot(cli *client.QQClient) *CQBot { func NewQQBot(cli *client.QQClient) *CQBot {
bot := &CQBot{ bot := &CQBot{
Client: cli, Client: cli,
nextTokenCache: utils.NewCache(time.Second * 10),
} }
bot.Client.OnPrivateMessage(bot.privateMessageEvent) bot.Client.OnPrivateMessage(bot.privateMessageEvent)
bot.Client.OnGroupMessage(bot.groupMessageEvent) bot.Client.OnGroupMessage(bot.groupMessageEvent)
@ -78,6 +80,7 @@ func NewQQBot(cli *client.QQClient) *CQBot {
bot.Client.OnTempMessage(bot.tempMessageEvent) bot.Client.OnTempMessage(bot.tempMessageEvent)
bot.Client.GuildService.OnGuildChannelMessage(bot.guildChannelMessageEvent) bot.Client.GuildService.OnGuildChannelMessage(bot.guildChannelMessageEvent)
bot.Client.GuildService.OnGuildMessageReactionsUpdated(bot.guildMessageReactionsUpdatedEvent) bot.Client.GuildService.OnGuildMessageReactionsUpdated(bot.guildMessageReactionsUpdatedEvent)
bot.Client.GuildService.OnGuildMessageRecalled(bot.guildChannelMessageRecalledEvent)
bot.Client.GuildService.OnGuildChannelUpdated(bot.guildChannelUpdatedEvent) bot.Client.GuildService.OnGuildChannelUpdated(bot.guildChannelUpdatedEvent)
bot.Client.GuildService.OnGuildChannelCreated(bot.guildChannelCreatedEvent) bot.Client.GuildService.OnGuildChannelCreated(bot.guildChannelCreatedEvent)
bot.Client.GuildService.OnGuildChannelDestroyed(bot.guildChannelDestroyedEvent) bot.Client.GuildService.OnGuildChannelDestroyed(bot.guildChannelDestroyedEvent)
@ -199,6 +202,18 @@ func (bot *CQBot) UploadLocalImageAsGuildChannel(guildID, channelID uint64, img
return bot.Client.GuildService.UploadGuildImage(guildID, channelID, img.Stream) return bot.Client.GuildService.UploadGuildImage(guildID, channelID, img.Stream)
} }
func (bot *CQBot) uploadGuildVideo(i *LocalVideoElement, guildID, channelID uint64) (*message.ShortVideoElement, error) {
video, err := os.Open(i.File)
if err != nil {
return nil, err
}
defer func() { _ = video.Close() }()
_, _ = video.Seek(0, io.SeekStart)
_, _ = i.thumb.Seek(0, io.SeekStart)
n, err := bot.Client.UploadGuildShortVideo(guildID, channelID, video, i.thumb)
return n, err
}
// SendGroupMessage 发送群消息 // SendGroupMessage 发送群消息
func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) int32 { func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) int32 {
newElem := make([]message.IMessageElement, 0, len(m.Elements)) newElem := make([]message.IMessageElement, 0, len(m.Elements))
@ -299,6 +314,10 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen
id = bot.InsertPrivateMessage(msg) id = bot.InsertPrivateMessage(msg)
} }
case ok || groupID != 0: // 临时会话 case ok || groupID != 0: // 临时会话
if !base.AllowTempSession {
log.Warnf("发送临时会话消息失败: 已关闭临时会话信息发送功能")
return -1
}
switch { switch {
case groupID != 0 && bot.Client.FindGroup(groupID) == nil: case groupID != 0 && bot.Client.FindGroup(groupID) == nil:
log.Errorf("错误: 找不到群(%v)", groupID) log.Errorf("错误: 找不到群(%v)", groupID)
@ -352,7 +371,20 @@ func (bot *CQBot) SendGuildChannelMessage(guildID, channelID uint64, m *message.
continue continue
} }
e = n e = n
case *LocalVideoElement, *LocalVoiceElement, *PokeElement, *message.MusicShareElement:
case *LocalVideoElement:
n, err := bot.uploadGuildVideo(i, guildID, channelID)
if err != nil {
log.Warnf("警告: 频道 %d 消息%s上传失败: %v", channelID, e.Type().String(), err)
continue
}
e = n
case *message.MusicShareElement:
bot.Client.SendGuildMusicShare(guildID, channelID, i)
return "-1" // todo: fix this
case *LocalVoiceElement, *PokeElement:
log.Warnf("警告: 频道暂不支持发送 %v 消息", i.Type().String()) log.Warnf("警告: 频道暂不支持发送 %v 消息", i.Type().String())
continue continue
} }
@ -489,9 +521,31 @@ func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32
} }
*/ */
// InsertGuildChannelMessage 频道消息入数据库
func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMessage) string {
id := encodeGuildMessageID(m.GuildId, m.ChannelId, m.Id, MessageSourceGuildChannel)
msg := &db.StoredGuildChannelMessage{
ID: id,
Attribute: &db.StoredGuildMessageAttribute{
MessageSeq: m.Id,
InternalID: m.InternalId,
SenderTinyID: m.Sender.TinyId,
SenderName: m.Sender.Nickname,
Timestamp: m.Time,
},
GuildID: m.GuildId,
ChannelID: m.ChannelId,
Content: ToMessageContent(m.Elements),
}
if err := db.InsertGuildChannelMessage(msg); err != nil {
log.Warnf("记录聊天数据时出现错误: %v", err)
return ""
}
return msg.ID
}
// Release 释放Bot实例 // Release 释放Bot实例
func (bot *CQBot) Release() { func (bot *CQBot) Release() {
} }
func (bot *CQBot) dispatchEventMessage(m global.MSG) { func (bot *CQBot) dispatchEventMessage(m global.MSG) {
@ -519,7 +573,9 @@ func (bot *CQBot) dispatchEventMessage(m global.MSG) {
}(f) }(f)
} }
wg.Wait() wg.Wait()
global.PutBuffer(event.buffer) if event.buffer != nil {
global.PutBuffer(event.buffer)
}
} }
func formatGroupName(group *client.GroupInfo) string { func formatGroupName(group *client.GroupInfo) string {
@ -558,3 +614,30 @@ func encodeMessageID(target int64, seq int32) string {
w.WriteUInt32(uint32(seq)) w.WriteUInt32(uint32(seq))
})) }))
} }
// encodeGuildMessageID 将频道信息编码为字符串
// 当信息来源为 Channel 时 primaryID 为 guildID , subID 为 channelID
// 当信息来源为 Direct 时 primaryID 为 guildID , subID 为 tinyID
func encodeGuildMessageID(primaryID, subID, seq uint64, source MessageSourceType) string {
return base64.StdEncoding.EncodeToString(binary.NewWriterF(func(w *binary.Writer) {
w.WriteByte(byte(source))
w.WriteUInt64(primaryID)
w.WriteUInt64(subID)
w.WriteUInt64(seq)
}))
}
func decodeGuildMessageID(id string) (source *MessageSource, seq uint64) {
b, _ := base64.StdEncoding.DecodeString(id)
if len(b) < 25 {
return
}
r := binary.NewReader(b)
source = &MessageSource{
SourceType: MessageSourceType(r.ReadByte()),
PrimaryID: uint64(r.ReadInt64()),
SubID: uint64(r.ReadInt64()),
}
seq = uint64(r.ReadInt64())
return
}

View File

@ -3,6 +3,8 @@ package coolq
import ( import (
"strconv" "strconv"
"github.com/Mrs4s/MiraiGo/topic"
"github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/message"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -50,13 +52,17 @@ func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) global.MSG
} }
} }
func convertGuildMemberInfo(m *client.GuildMemberInfo) global.MSG { func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) {
return global.MSG{ for _, mem := range m {
"tiny_id": m.TinyId, r = append(r, global.MSG{
"title": m.Title, "tiny_id": fU64(mem.TinyId),
"nickname": m.Nickname, "title": mem.Title,
"role": m.Role, "nickname": mem.Nickname,
"role_id": fU64(mem.Role),
"role_name": mem.RoleName,
})
} }
return
} }
func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) global.MSG { func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) global.MSG {
@ -144,12 +150,11 @@ func convertChannelInfo(c *client.ChannelInfo) global.MSG {
}) })
} }
return global.MSG{ return global.MSG{
"channel_id": c.ChannelId, "channel_id": fU64(c.ChannelId),
"channel_type": c.ChannelType, "channel_type": c.ChannelType,
"channel_name": c.ChannelName, "channel_name": c.ChannelName,
"owner_guild_id": c.Meta.GuildId, "owner_guild_id": fU64(c.Meta.GuildId),
"creator_id": c.Meta.CreatorUin, "creator_tiny_id": fU64(c.Meta.CreatorTinyId),
"creator_tiny_id": c.Meta.CreatorTinyId,
"create_time": c.Meta.CreateTime, "create_time": c.Meta.CreateTime,
"current_slow_mode": c.Meta.CurrentSlowMode, "current_slow_mode": c.Meta.CurrentSlowMode,
"talk_permission": c.Meta.TalkPermission, "talk_permission": c.Meta.TalkPermission,
@ -157,3 +162,64 @@ func convertChannelInfo(c *client.ChannelInfo) global.MSG {
"slow_modes": slowModes, "slow_modes": slowModes,
} }
} }
func convertChannelFeedInfo(f *topic.Feed) global.MSG {
m := global.MSG{
"id": f.Id,
"title": f.Title,
"sub_title": f.SubTitle,
"create_time": f.CreateTime,
"guild_id": fU64(f.GuildId),
"channel_id": fU64(f.ChannelId),
"poster_info": global.MSG{
"tiny_id": f.Poster.TinyIdStr,
"nickname": f.Poster.Nickname,
"icon_url": f.Poster.IconUrl,
},
"contents": FeedContentsToArrayMessage(f.Contents),
}
images := make([]global.MSG, 0, len(f.Images))
videos := make([]global.MSG, 0, len(f.Videos))
for _, image := range f.Images {
images = append(images, global.MSG{
"file_id": image.FileId,
"pattern_id": image.PatternId,
"url": image.Url,
"width": image.Width,
"height": image.Height,
})
}
for _, video := range f.Videos {
videos = append(videos, global.MSG{
"file_id": video.FileId,
"pattern_id": video.PatternId,
"url": video.Url,
"width": video.Width,
"height": video.Height,
})
}
m["resource"] = global.MSG{
"images": images,
"videos": videos,
}
return m
}
func convertReactions(reactions []*message.GuildMessageEmojiReaction) (r []global.MSG) {
r = make([]global.MSG, len(reactions))
for i, re := range reactions {
r[i] = global.MSG{
"emoji_id": re.EmojiId,
"emoji_index": re.Face.Index,
"emoji_type": re.EmojiType,
"emoji_name": re.Face.Name,
"count": re.Count,
"clicked": re.Clicked,
}
}
return
}
func fU64(v uint64) string {
return strconv.FormatUint(v, 10)
}

View File

@ -72,13 +72,14 @@ type MessageSource struct {
} }
// MessageSourceType 消息来源类型 // MessageSourceType 消息来源类型
type MessageSourceType int32 type MessageSourceType byte
// MessageSourceType 常量 // MessageSourceType 常量
const ( const (
MessageSourcePrivate MessageSourceType = 0 MessageSourcePrivate MessageSourceType = 1 << iota
MessageSourceGroup MessageSourceType = 1 MessageSourceGroup
MessageSourceGuildChannel MessageSourceType = 2 MessageSourceGuildChannel
MessageSourceGuildDirect
) )
const ( const (
@ -110,7 +111,7 @@ func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []glob
_, ok := e.(*message.ReplyElement) _, ok := e.(*message.ReplyElement)
return ok return ok
}) })
if reply != nil && source.SourceType == MessageSourceGroup { if reply != nil && source.SourceType&(MessageSourceGroup|MessageSourcePrivate) != 0 {
replyElem := reply.(*message.ReplyElement) replyElem := reply.(*message.ReplyElement)
rid := int64(source.PrimaryID) rid := int64(source.PrimaryID)
if rid == 0 { if rid == 0 {
@ -166,7 +167,7 @@ func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []glob
} else { } else {
m = global.MSG{ m = global.MSG{
"type": "at", "type": "at",
"data": map[string]string{"qq": strconv.FormatInt(o.Target, 10)}, "data": map[string]string{"qq": strconv.FormatUint(uint64(o.Target), 10)},
} }
} }
case *message.RedBagElement: case *message.RedBagElement:
@ -244,6 +245,11 @@ func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []glob
"data": map[string]string{"data": o.Content, "resid": strconv.FormatInt(int64(o.Id), 10)}, "data": map[string]string{"data": o.Content, "resid": strconv.FormatInt(int64(o.Id), 10)},
} }
} }
case *message.AnimatedSticker:
m = global.MSG{
"type": "face",
"data": map[string]string{"id": strconv.FormatInt(int64(o.ID), 10), "type": "sticker"},
}
default: default:
continue continue
} }
@ -271,7 +277,7 @@ func ToStringMessage(e []message.IMessageElement, source MessageSource, isRaw ..
_, ok := e.(*message.ReplyElement) _, ok := e.(*message.ReplyElement)
return ok return ok
}) })
if reply != nil && source.SourceType == MessageSourceGroup { if reply != nil && source.SourceType&(MessageSourceGroup|MessageSourcePrivate) != 0 {
replyElem := reply.(*message.ReplyElement) replyElem := reply.(*message.ReplyElement)
rid := int64(source.PrimaryID) rid := int64(source.PrimaryID)
if rid == 0 { if rid == 0 {
@ -305,7 +311,7 @@ func ToStringMessage(e []message.IMessageElement, source MessageSource, isRaw ..
write("[CQ:at,qq=all]") write("[CQ:at,qq=all]")
continue continue
} }
write("[CQ:at,qq=%d]", o.Target) write("[CQ:at,qq=%d]", uint64(o.Target))
case *message.RedBagElement: case *message.RedBagElement:
write("[CQ:redbag,title=%s]", o.Title) write("[CQ:redbag,title=%s]", o.Title)
case *message.ForwardElement: case *message.ForwardElement:
@ -361,6 +367,8 @@ func ToStringMessage(e []message.IMessageElement, source MessageSource, isRaw ..
} }
case *message.LightAppElement: case *message.LightAppElement:
write(`[CQ:json,data=%s]`, CQCodeEscapeValue(o.Content)) write(`[CQ:json,data=%s]`, CQCodeEscapeValue(o.Content))
case *message.AnimatedSticker:
write(`[CQ:face,id=%d,type=sticker]`, o.ID)
} }
} }
r = sb.String() // 内部已拷贝 r = sb.String() // 内部已拷贝
@ -406,7 +414,7 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) {
case *message.RedBagElement: case *message.RedBagElement:
m = global.MSG{ m = global.MSG{
"type": "redbag", "type": "redbag",
"data": global.MSG{"title": o.Title, "type": o.MsgType}, "data": global.MSG{"title": o.Title, "type": int(o.MsgType)},
} }
case *message.ForwardElement: case *message.ForwardElement:
m = global.MSG{ m = global.MSG{
@ -441,6 +449,11 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) {
"type": "image", "type": "image",
"data": data, "data": data,
} }
case *message.GuildImageElement:
m = global.MSG{
"type": "image",
"data": global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url},
}
case *message.FriendImageElement: case *message.FriendImageElement:
data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url} data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url}
if o.Flash { if o.Flash {
@ -466,6 +479,11 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) {
"data": global.MSG{"data": o.Content, "resid": o.Id}, "data": global.MSG{"data": o.Content, "resid": o.Id},
} }
} }
case *message.AnimatedSticker:
m = global.MSG{
"type": "face",
"data": global.MSG{"id": o.ID, "type": "sticker"},
}
default: default:
continue continue
} }
@ -654,7 +672,7 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType MessageSourceT
d := make(map[string]string) d := make(map[string]string)
convertElem := func(e gjson.Result) { convertElem := func(e gjson.Result) {
t := e.Get("type").Str t := e.Get("type").Str
if t == "reply" && sourceType == MessageSourceGroup { if t == "reply" && sourceType&(MessageSourceGroup|MessageSourcePrivate) != 0 {
if len(r) > 0 { if len(r) > 0 {
if _, ok := r[0].(*message.ReplyElement); ok { if _, ok := r[0].(*message.ReplyElement); ok {
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
@ -927,17 +945,12 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou
if err != nil { if err != nil {
return nil, err return nil, err
} }
return message.NewFace(int32(id)), nil if d["type"] == "sticker" {
case "mention": return &message.AnimatedSticker{ID: int32(id)}, nil
if !base.AcceptOneBot12Message {
return nil, errors.New("unsupported onebot 12 style")
} }
fallthrough return message.NewFace(int32(id)), nil
case "at": case "at":
qq := d["qq"] qq := d["qq"]
if base.AcceptOneBot12Message && qq == "" {
qq = d["user_id"]
}
if qq == "all" { if qq == "all" {
return message.AtAll(), nil return message.AtAll(), nil
} }
@ -1296,7 +1309,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy
} }
rawPath := path.Join(global.ImagePath, f) rawPath := path.Join(global.ImagePath, f)
if video { if video {
if strings.HasSuffix(f, ".video") && cache.EnableCacheDB { if strings.HasSuffix(f, ".video") {
hash, err := hex.DecodeString(strings.TrimSuffix(f, ".video")) hash, err := hex.DecodeString(strings.TrimSuffix(f, ".video"))
if err == nil { if err == nil {
if b := cache.Video.Get(hash); b != nil { if b := cache.Video.Get(hash); b != nil {
@ -1321,7 +1334,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy
return &LocalImageElement{File: cacheFile}, nil return &LocalImageElement{File: cacheFile}, nil
} }
} }
if strings.HasSuffix(f, ".image") && cache.EnableCacheDB { if strings.HasSuffix(f, ".image") {
hash, err := hex.DecodeString(strings.TrimSuffix(f, ".image")) hash, err := hex.DecodeString(strings.TrimSuffix(f, ".image"))
if err == nil { if err == nil {
if b := cache.Image.Get(hash); b != nil { if b := cache.Image.Get(hash); b != nil {

View File

@ -148,33 +148,29 @@ func (bot *CQBot) guildChannelMessageEvent(c *client.QQClient, m *message.GuildC
if guild == nil { if guild == nil {
return return
} }
var channel *client.ChannelInfo channel := guild.FindChannel(m.ChannelId)
for _, c := range guild.Channels {
if c.ChannelId == m.ChannelId {
channel = c
}
}
source := MessageSource{ source := MessageSource{
SourceType: MessageSourceGuildChannel, SourceType: MessageSourceGuildChannel,
PrimaryID: m.GuildId, PrimaryID: m.GuildId,
SubID: m.ChannelId, SubID: m.ChannelId,
} }
log.Infof("收到来自频道 %v(%v) 子频道 %v(%v) 内 %v(%v) 的消息: %v", guild.GuildName, guild.GuildId, channel.ChannelName, m.ChannelId, m.Sender.Nickname, m.Sender.TinyId, ToStringMessage(m.Elements, source, true)) log.Infof("收到来自频道 %v(%v) 子频道 %v(%v) 内 %v(%v) 的消息: %v", guild.GuildName, guild.GuildId, channel.ChannelName, m.ChannelId, m.Sender.Nickname, m.Sender.TinyId, ToStringMessage(m.Elements, source, true))
// todo: 数据库支持 id := bot.InsertGuildChannelMessage(m)
bot.dispatchEventMessage(global.MSG{ bot.dispatchEventMessage(global.MSG{
"post_type": "message", "post_type": "message",
"message_type": "guild", "message_type": "guild",
"sub_type": "channel", "sub_type": "channel",
"guild_id": m.GuildId, "guild_id": fU64(m.GuildId),
"channel_id": m.ChannelId, "channel_id": fU64(m.ChannelId),
"message_id": fmt.Sprintf("%v-%v", m.Id, m.InternalId), "message_id": id,
"user_id": m.Sender.TinyId, "user_id": fU64(m.Sender.TinyId),
"message": ToFormattedMessage(m.Elements, source, false), // todo: 增加对频道消息 Reply 的支持 "message": ToFormattedMessage(m.Elements, source, false), // todo: 增加对频道消息 Reply 的支持
"self_id": bot.Client.Uin, "self_id": bot.Client.Uin,
"self_tiny_id": bot.Client.GuildService.TinyId, "self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"time": m.Time, "time": m.Time,
"sender": global.MSG{ "sender": global.MSG{
"user_id": m.Sender.TinyId, "user_id": m.Sender.TinyId,
"tiny_id": fU64(m.Sender.TinyId),
"nickname": m.Sender.Nickname, "nickname": m.Sender.Nickname,
}, },
}) })
@ -185,7 +181,8 @@ func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *clien
if guild == nil { if guild == nil {
return return
} }
str := fmt.Sprintf("频道 %v(%v) 消息 %v 表情贴片已更新: ", guild.GuildName, guild.GuildId, e.MessageId) msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, MessageSourceGuildChannel)
str := fmt.Sprintf("频道 %v(%v) 消息 %v 表情贴片已更新: ", guild.GuildName, guild.GuildId, msgID)
currentReactions := make([]global.MSG, len(e.CurrentReactions)) currentReactions := make([]global.MSG, len(e.CurrentReactions))
for i, r := range e.CurrentReactions { for i, r := range e.CurrentReactions {
str += fmt.Sprintf("%v*%v ", r.Face.Name, r.Count) str += fmt.Sprintf("%v*%v ", r.Face.Name, r.Count)
@ -203,18 +200,47 @@ func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *clien
} }
log.Infof(str) log.Infof(str)
bot.dispatchEventMessage(global.MSG{ bot.dispatchEventMessage(global.MSG{
"post_type": "notice", "post_type": "notice",
"notice_type": "message_reactions_updated", "notice_type": "message_reactions_updated",
"message_sender_uin": e.MessageSenderUin, "guild_id": fU64(e.GuildId),
"guild_id": e.GuildId, "channel_id": fU64(e.ChannelId),
"channel_id": e.ChannelId, "message_id": msgID,
"message_id": fmt.Sprint(e.MessageId), // todo: 支持数据库后转换为数据库id "operator_id": fU64(e.OperatorId),
"operator_id": e.OperatorId, "current_reactions": currentReactions,
"current_reactions": currentReactions, "time": time.Now().Unix(),
"time": time.Now().Unix(), "self_id": bot.Client.Uin,
"self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"self_tiny_id": bot.Client.GuildService.TinyId, "user_id": e.OperatorId,
"user_id": e.OperatorId, })
}
func (bot *CQBot) guildChannelMessageRecalledEvent(c *client.QQClient, e *client.GuildMessageRecalledEvent) {
guild := c.GuildService.FindGuild(e.GuildId)
if guild == nil {
return
}
channel := guild.FindChannel(e.ChannelId)
if channel == nil {
return
}
operator, err := c.GuildService.FetchGuildMemberProfileInfo(e.GuildId, e.OperatorId)
if err != nil {
log.Errorf("处理频道撤回事件时出现错误: 获取操作者资料时出现错误 %v", err)
return
}
msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, MessageSourceGuildChannel)
log.Infof("用户 %v(%v) 撤回了频道 %v(%v) 子频道 %v(%v) 的消息 %v", operator.Nickname, operator.TinyId, guild.GuildName, guild.GuildId, channel.ChannelName, channel.ChannelId, msgID)
bot.dispatchEventMessage(global.MSG{
"post_type": "notice",
"notice_type": "guild_channel_recall",
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelId),
"operator_id": fU64(e.OperatorId),
"message_id": msgID,
"time": time.Now().Unix(),
"self_id": bot.Client.Uin,
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId,
}) })
} }
@ -227,12 +253,12 @@ func (bot *CQBot) guildChannelUpdatedEvent(c *client.QQClient, e *client.GuildCh
bot.dispatchEventMessage(global.MSG{ bot.dispatchEventMessage(global.MSG{
"post_type": "notice", "post_type": "notice",
"notice_type": "channel_updated", "notice_type": "channel_updated",
"guild_id": e.GuildId, "guild_id": fU64(e.GuildId),
"channel_id": e.ChannelId, "channel_id": fU64(e.ChannelId),
"operator_id": e.OperatorId, "operator_id": fU64(e.OperatorId),
"time": time.Now().Unix(), "time": time.Now().Unix(),
"self_id": bot.Client.Uin, "self_id": bot.Client.Uin,
"self_tiny_id": bot.Client.GuildService.TinyId, "self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId, "user_id": e.OperatorId,
"old_info": convertChannelInfo(e.OldChannelInfo), "old_info": convertChannelInfo(e.OldChannelInfo),
"new_info": convertChannelInfo(e.NewChannelInfo), "new_info": convertChannelInfo(e.NewChannelInfo),
@ -244,19 +270,19 @@ func (bot *CQBot) guildChannelCreatedEvent(c *client.QQClient, e *client.GuildCh
if guild == nil { if guild == nil {
return return
} }
member := guild.FindMember(e.OperatorId) member, _ := c.GuildService.FetchGuildMemberProfileInfo(e.GuildId, e.OperatorId)
if member == nil { if member == nil {
member = &client.GuildMemberInfo{Nickname: "未知"} member = &client.GuildUserProfile{Nickname: "未知"}
} }
log.Infof("频道 %v(%v) 内用户 %v(%v) 创建了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId) log.Infof("频道 %v(%v) 内用户 %v(%v) 创建了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId)
bot.dispatchEventMessage(global.MSG{ bot.dispatchEventMessage(global.MSG{
"post_type": "notice", "post_type": "notice",
"notice_type": "channel_created", "notice_type": "channel_created",
"guild_id": e.GuildId, "guild_id": fU64(e.GuildId),
"channel_id": e.ChannelInfo.ChannelId, "channel_id": fU64(e.ChannelInfo.ChannelId),
"operator_id": e.OperatorId, "operator_id": fU64(e.OperatorId),
"self_id": bot.Client.Uin, "self_id": bot.Client.Uin,
"self_tiny_id": bot.Client.GuildService.TinyId, "self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId, "user_id": e.OperatorId,
"time": time.Now().Unix(), "time": time.Now().Unix(),
"channel_info": convertChannelInfo(e.ChannelInfo), "channel_info": convertChannelInfo(e.ChannelInfo),
@ -268,19 +294,19 @@ func (bot *CQBot) guildChannelDestroyedEvent(c *client.QQClient, e *client.Guild
if guild == nil { if guild == nil {
return return
} }
member := guild.FindMember(e.OperatorId) member, _ := c.GuildService.FetchGuildMemberProfileInfo(e.GuildId, e.OperatorId)
if member == nil { if member == nil {
member = &client.GuildMemberInfo{Nickname: "未知"} member = &client.GuildUserProfile{Nickname: "未知"}
} }
log.Infof("频道 %v(%v) 内用户 %v(%v) 删除了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId) log.Infof("频道 %v(%v) 内用户 %v(%v) 删除了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId)
bot.dispatchEventMessage(global.MSG{ bot.dispatchEventMessage(global.MSG{
"post_type": "notice", "post_type": "notice",
"notice_type": "channel_destroyed", "notice_type": "channel_destroyed",
"guild_id": e.GuildId, "guild_id": fU64(e.GuildId),
"channel_id": e.ChannelInfo.ChannelId, "channel_id": fU64(e.ChannelInfo.ChannelId),
"operator_id": e.OperatorId, "operator_id": fU64(e.OperatorId),
"self_id": bot.Client.Uin, "self_id": bot.Client.Uin,
"self_tiny_id": bot.Client.GuildService.TinyId, "self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId, "user_id": e.OperatorId,
"time": time.Now().Unix(), "time": time.Now().Unix(),
"channel_info": convertChannelInfo(e.ChannelInfo), "channel_info": convertChannelInfo(e.ChannelInfo),
@ -700,7 +726,6 @@ func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.Group
} }
func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) { func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
// TODO(wdvxdr): remove these old cache file in v1.0.0
for _, elem := range e { for _, elem := range e {
switch i := elem.(type) { switch i := elem.(type) {
case *message.GroupImageElement: case *message.GroupImageElement:
@ -718,12 +743,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
w.WriteString(i.ImageId) w.WriteString(i.ImageId)
w.WriteString(i.Url) w.WriteString(i.Url)
}) })
filename := hex.EncodeToString(i.Md5) + ".image" cache.Image.Insert(i.Md5, data)
if cache.EnableCacheDB {
cache.Image.Insert(i.Md5, data)
} else if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = os.WriteFile(path.Join(global.ImagePath, filename), data, 0o644)
}
case *message.GuildImageElement: case *message.GuildImageElement:
data := binary.NewWriterF(func(w *binary.Writer) { data := binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
@ -732,14 +753,9 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
w.WriteString(i.Url) w.WriteString(i.Url)
}) })
filename := hex.EncodeToString(i.Md5) + ".image" filename := hex.EncodeToString(i.Md5) + ".image"
if cache.EnableCacheDB { cache.Image.Insert(i.Md5, data)
cache.Image.Insert(i.Md5, data)
} else if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = os.WriteFile(path.Join(global.ImagePath, filename), data, 0o644)
}
if i.Url != "" && !global.PathExists(path.Join(global.ImagePath, "guild-images", filename)) { if i.Url != "" && !global.PathExists(path.Join(global.ImagePath, "guild-images", filename)) {
if err := global.DownloadFile(i.Url, path.Join(global.ImagePath, "guild-images", filename), -1, map[string]string{}); err != nil { if err := global.DownloadFile(i.Url, path.Join(global.ImagePath, "guild-images", filename), -1, nil); err != nil {
log.Warnf("下载频道图片时出现错误: %v", err) log.Warnf("下载频道图片时出现错误: %v", err)
} }
} }
@ -750,12 +766,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
w.WriteString(i.ImageId) w.WriteString(i.ImageId)
w.WriteString(i.Url) w.WriteString(i.Url)
}) })
filename := hex.EncodeToString(i.Md5) + ".image" cache.Image.Insert(i.Md5, data)
if cache.EnableCacheDB {
cache.Image.Insert(i.Md5, data)
} else if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = os.WriteFile(path.Join(global.ImagePath, filename), data, 0o644)
}
case *message.VoiceElement: case *message.VoiceElement:
// todo: don't download original file? // todo: don't download original file?
i.Name = strings.ReplaceAll(i.Name, "{", "") i.Name = strings.ReplaceAll(i.Name, "{", "")
@ -778,11 +790,7 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
w.Write(i.Uuid) w.Write(i.Uuid)
}) })
filename := hex.EncodeToString(i.Md5) + ".video" filename := hex.EncodeToString(i.Md5) + ".video"
if cache.EnableCacheDB { cache.Video.Insert(i.Md5, data)
cache.Video.Insert(i.Md5, data)
} else if !global.PathExists(path.Join(global.VideoPath, filename)) {
_ = os.WriteFile(path.Join(global.VideoPath, filename), data, 0o644)
}
i.Name = filename i.Name = filename
i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5) i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5)
} }

53
coolq/feed.go Normal file
View File

@ -0,0 +1,53 @@
package coolq
import (
"github.com/Mrs4s/MiraiGo/topic"
"github.com/Mrs4s/go-cqhttp/global"
)
// FeedContentsToArrayMessage 将话题频道帖子内容转换为 Array Message
func FeedContentsToArrayMessage(contents []topic.IFeedRichContentElement) []global.MSG {
r := make([]global.MSG, 0, len(contents))
for _, e := range contents {
var m global.MSG
switch elem := e.(type) {
case *topic.TextElement:
m = global.MSG{
"type": "text",
"data": global.MSG{"text": elem.Content},
}
case *topic.AtElement:
m = global.MSG{
"type": "at",
"data": global.MSG{"id": elem.Id, "qq": elem.Id},
}
case *topic.EmojiElement:
m = global.MSG{
"type": "face",
"data": global.MSG{"id": elem.Id},
}
case *topic.ChannelQuoteElement:
m = global.MSG{
"type": "channel_quote",
"data": global.MSG{
"guild_id": fU64(elem.GuildId),
"channel_id": fU64(elem.ChannelId),
"display_text": elem.DisplayText,
},
}
case *topic.UrlQuoteElement:
m = global.MSG{
"type": "url_quote",
"data": global.MSG{
"url": elem.Url,
"display_text": elem.DisplayText,
},
}
}
if m != nil {
r = append(r, m)
}
}
return r
}

View File

@ -19,11 +19,15 @@ type (
GetGroupMessageByGlobalID(int32) (*StoredGroupMessage, error) GetGroupMessageByGlobalID(int32) (*StoredGroupMessage, error)
// GetPrivateMessageByGlobalID 通过 GlobalID 来获取私聊消息 // GetPrivateMessageByGlobalID 通过 GlobalID 来获取私聊消息
GetPrivateMessageByGlobalID(int32) (*StoredPrivateMessage, error) GetPrivateMessageByGlobalID(int32) (*StoredPrivateMessage, error)
// GetGuildChannelMessageByID 通过 ID 来获取频道消息
GetGuildChannelMessageByID(string) (*StoredGuildChannelMessage, error)
// InsertGroupMessage 向数据库写入新的群消息 // InsertGroupMessage 向数据库写入新的群消息
InsertGroupMessage(*StoredGroupMessage) error InsertGroupMessage(*StoredGroupMessage) error
// InsertPrivateMessage 向数据库写入新的私聊消息 // InsertPrivateMessage 向数据库写入新的私聊消息
InsertPrivateMessage(*StoredPrivateMessage) error InsertPrivateMessage(*StoredPrivateMessage) error
// InsertGuildChannelMessage 向数据库写入新的频道消息
InsertGuildChannelMessage(*StoredGuildChannelMessage) error
} }
StoredMessage interface { StoredMessage interface {
@ -58,6 +62,16 @@ type (
Content []global.MSG `bson:"content"` Content []global.MSG `bson:"content"`
} }
// StoredGuildChannelMessage 持久化频道消息
StoredGuildChannelMessage struct {
ID string `bson:"_id"`
Attribute *StoredGuildMessageAttribute `bson:"attribute"`
GuildID uint64 `bson:"guildId"`
ChannelID uint64 `bson:"channelId"`
QuotedInfo *QuotedInfo `bson:"quotedInfo"`
Content []global.MSG `bson:"content"`
}
// StoredMessageAttribute 持久化消息属性 // StoredMessageAttribute 持久化消息属性
StoredMessageAttribute struct { StoredMessageAttribute struct {
MessageSeq int32 `bson:"messageSeq"` MessageSeq int32 `bson:"messageSeq"`
@ -67,6 +81,15 @@ type (
Timestamp int64 `bson:"timestamp"` Timestamp int64 `bson:"timestamp"`
} }
// StoredGuildMessageAttribute 持久化频道消息属性
StoredGuildMessageAttribute struct {
MessageSeq uint64 `bson:"messageSeq"`
InternalID uint64 `bson:"internalId"`
SenderTinyID uint64 `bson:"senderTinyId"`
SenderName string `bson:"senderName"`
Timestamp int64 `bson:"timestamp"`
}
// QuotedInfo 引用回复 // QuotedInfo 引用回复
QuotedInfo struct { QuotedInfo struct {
PrevID string `bson:"prevId"` PrevID string `bson:"prevId"`

View File

@ -5,6 +5,8 @@ import (
"encoding/gob" "encoding/gob"
"path" "path"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
@ -21,16 +23,19 @@ type LevelDBImpl struct {
} }
const ( const (
group byte = 0x0 group byte = 0x0
private byte = 0x1 private byte = 0x1
guildChannel byte = 0x2
) )
func init() { func init() {
gob.Register(db.StoredMessageAttribute{}) gob.Register(db.StoredMessageAttribute{})
gob.Register(db.StoredGuildMessageAttribute{})
gob.Register(db.QuotedInfo{}) gob.Register(db.QuotedInfo{})
gob.Register(global.MSG{}) gob.Register(global.MSG{})
gob.Register(db.StoredGroupMessage{}) gob.Register(db.StoredGroupMessage{})
gob.Register(db.StoredPrivateMessage{}) gob.Register(db.StoredPrivateMessage{})
gob.Register(db.StoredGuildChannelMessage{})
db.Register("leveldb", func(node yaml.Node) db.Database { db.Register("leveldb", func(node yaml.Node) db.Database {
conf := new(config.LevelDBConfig) conf := new(config.LevelDBConfig)
@ -48,7 +53,7 @@ func (ldb *LevelDBImpl) Open() error {
WriteBuffer: 128 * opt.KiB, WriteBuffer: 128 * opt.KiB,
}) })
if err != nil { if err != nil {
return errors.Wrap(err, "open level ldb error") return errors.Wrap(err, "open leveldb error")
} }
ldb.db = d ldb.db = d
return nil return nil
@ -102,6 +107,24 @@ func (ldb *LevelDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivate
return p, nil return p, nil
} }
func (ldb *LevelDBImpl) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) {
v, err := ldb.db.Get([]byte(id), nil)
if err != nil {
return nil, errors.Wrap(err, "get value error")
}
r := binary.NewReader(v)
switch r.ReadByte() {
case guildChannel:
g := &db.StoredGuildChannelMessage{}
if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(g); err != nil {
return nil, errors.Wrap(err, "decode message error")
}
return g, nil
default:
return nil, errors.New("unknown message flag")
}
}
func (ldb *LevelDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error { func (ldb *LevelDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error {
buf := global.NewBuffer() buf := global.NewBuffer()
defer global.PutBuffer(buf) defer global.PutBuffer(buf)
@ -127,3 +150,16 @@ func (ldb *LevelDBImpl) InsertPrivateMessage(msg *db.StoredPrivateMessage) error
}), nil) }), nil)
return errors.Wrap(err, "put data error") return errors.Wrap(err, "put data error")
} }
func (ldb *LevelDBImpl) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
buf := global.NewBuffer()
defer global.PutBuffer(buf)
if err := gob.NewEncoder(buf).Encode(msg); err != nil {
return errors.Wrap(err, "encode message error")
}
err := ldb.db.Put(utils.S2B(msg.ID), binary.NewWriterF(func(w *binary.Writer) {
w.WriteByte(guildChannel)
w.Write(buf.Bytes())
}), nil)
return errors.Wrap(err, "put data error")
}

View File

@ -20,8 +20,9 @@ type MongoDBImpl struct {
} }
const ( const (
MongoGroupMessageCollection = "group-messages" MongoGroupMessageCollection = "group-messages"
MongoPrivateMessageCollection = "private-messages" MongoPrivateMessageCollection = "private-messages"
MongoGuildChannelMessageCollection = "guild-channel-messages"
) )
func init() { func init() {
@ -72,14 +73,29 @@ func (m *MongoDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMe
return &ret, nil return &ret, nil
} }
func (m *MongoDBImpl) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) {
coll := m.mongo.Collection(MongoGuildChannelMessageCollection)
var ret db.StoredGuildChannelMessage
if err := coll.FindOne(context.Background(), bson.D{{"_id", id}}).Decode(&ret); err != nil {
return nil, errors.Wrap(err, "query error")
}
return &ret, nil
}
func (m *MongoDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error { func (m *MongoDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error {
coll := m.mongo.Collection(MongoGroupMessageCollection) coll := m.mongo.Collection(MongoGroupMessageCollection)
_, err := coll.InsertOne(context.Background(), msg) _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
return errors.Wrap(err, "insert error") return errors.Wrap(err, "insert error")
} }
func (m *MongoDBImpl) InsertPrivateMessage(msg *db.StoredPrivateMessage) error { func (m *MongoDBImpl) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
coll := m.mongo.Collection(MongoPrivateMessageCollection) coll := m.mongo.Collection(MongoPrivateMessageCollection)
_, err := coll.InsertOne(context.Background(), msg) _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
return errors.Wrap(err, "insert error")
}
func (m *MongoDBImpl) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
coll := m.mongo.Collection(MongoGuildChannelMessageCollection)
_, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
return errors.Wrap(err, "insert error") return errors.Wrap(err, "insert error")
} }

View File

@ -69,6 +69,13 @@ func GetPrivateMessageByGlobalID(id int32) (*StoredPrivateMessage, error) {
return backends[0].GetPrivateMessageByGlobalID(id) return backends[0].GetPrivateMessageByGlobalID(id)
} }
func GetGuildChannelMessageByID(id string) (*StoredGuildChannelMessage, error) {
if len(backends) == 0 {
return nil, DatabaseDisabledError
}
return backends[0].GetGuildChannelMessageByID(id)
}
func InsertGroupMessage(m *StoredGroupMessage) error { func InsertGroupMessage(m *StoredGroupMessage) error {
for _, b := range backends { for _, b := range backends {
if err := b.InsertGroupMessage(m); err != nil { if err := b.InsertGroupMessage(m); err != nil {
@ -86,3 +93,12 @@ func InsertPrivateMessage(m *StoredPrivateMessage) error {
} }
return nil return nil
} }
func InsertGuildChannelMessage(m *StoredGuildChannelMessage) error {
for _, b := range backends {
if err := b.InsertGuildChannelMessage(m); err != nil {
return errors.Wrap(err, "insert message to backend error")
}
}
return nil
}

View File

@ -15,11 +15,10 @@ account: # 账号相关
uin: 1233456 # QQ账号 uin: 1233456 # QQ账号
password: '' # 密码为空时使用扫码登录 password: '' # 密码为空时使用扫码登录
encrypt: false # 是否开启密码加密 encrypt: false # 是否开启密码加密
status: 0 # 在线状态,详情请查看下方的在线状态 status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
relogin: # 重连设置 relogin: # 重连设置
# disabled: false delay: 3 # 首次重连延迟, 单位秒
delay: 3 # 重连延迟, 单位秒 interval: 3 # 重连间隔
interval: 0 # 重连间隔
max-times: 0 # 最大重连次数, 0为无限制 max-times: 0 # 最大重连次数, 0为无限制
# 是否使用服务器下发的新地址进行重连 # 是否使用服务器下发的新地址进行重连
@ -27,11 +26,11 @@ account: # 账号相关
use-sso-address: true use-sso-address: true
heartbeat: heartbeat:
# disabled: false # 是否开启心跳事件上报
# 心跳频率, 单位秒 # 心跳频率, 单位秒
# -1 为关闭心跳 # -1 为关闭心跳
interval: 5 interval: 5
message:
# 上报数据类型 # 上报数据类型
# 可选: string,array # 可选: string,array
post-format: string post-format: string
@ -57,6 +56,10 @@ heartbeat:
output: output:
# 日志等级 trace,debug,info,warn,error # 日志等级 trace,debug,info,warn,error
log-level: warn log-level: warn
# 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留.
log-aging: 15
# 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写
log-force-new: true
# 是否启用 DEBUG # 是否启用 DEBUG
debug: false # 开启调试模式 debug: false # 开启调试模式
@ -76,6 +79,14 @@ default-middlewares: &default
frequency: 1 # 令牌回复频率, 单位秒 frequency: 1 # 令牌回复频率, 单位秒
bucket: 1 # 令牌桶大小 bucket: 1 # 令牌桶大小
database: # 数据库相关设置
leveldb:
# 是否启用内置leveldb数据库
# 启用将会增加10-20MB的内存占用和一定的磁盘空间
# 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable: true
# 连接服务列表
servers: servers:
# HTTP 通信设置 # HTTP 通信设置
- http: - http:
@ -156,6 +167,16 @@ database: # 数据库相关设置
> 注5关于MIME扫描 详见[MIME](file.md#MIME) > 注5关于MIME扫描 详见[MIME](file.md#MIME)
### 环境变量
go-cqhttp 配置文件可以使用占位符来读取**环境变量**的值。
```yaml
account: # 账号相关
uin: ${CQ_UIN} # 读取环境变量 CQ_UIN
password: ${CQ_PWD:123456} # 当 CQ_PWD 为空时使用默认值 123456
```
## 在线状态 ## 在线状态
| 状态 | 值 | | 状态 | 值 |

View File

@ -29,6 +29,10 @@ API以及字段相关命名均为参考QQ官方命名或相似产品命名规则
- `at` 消息的 `target` 依然使用 `qq` 字段, 以保证一致性. 但内容为 `tiny_id` - `at` 消息的 `target` 依然使用 `qq` 字段, 以保证一致性. 但内容为 `tiny_id`
- 所有事件的 `self_id` 均为 BOT 的QQ号. `tiny_id` 将放在 `self_tiny_id` 字段 - 所有事件的 `self_id` 均为 BOT 的QQ号. `tiny_id` 将放在 `self_tiny_id` 字段
- 遵循我们一贯的原则, 将不会支持主动加频道/主动拉人/红包相关消息类型 - 遵循我们一贯的原则, 将不会支持主动加频道/主动拉人/红包相关消息类型
- 频道相关的API仅能在 `Android Phone``iPad` 协议上使用.
- 由于频道相关ID的数据类型均为 `uint64` , 为保证不超过某些语言的安全值范围, 在 `v1.0.0-beta8-fix3` 以后, 所有ID相关数据将转换为 `string` 类型, API调用 `uint64`
`string` 均可接受.
- 为保证一致性, 所有频道接口返回的 `用户ID` 均命名为 `tiny_id`, 所有频道相关接口的 `用户ID` 入参均命名为 `user_id`
## API ## API
@ -41,7 +45,7 @@ API以及字段相关命名均为参考QQ官方命名或相似产品命名规则
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `nickname` | string | 昵称 | | `nickname` | string | 昵称 |
| `tiny_id` | uint64 | 自身的ID | | `tiny_id` | string | 自身的ID |
| `avatar_url` | string | 头像链接 | | `avatar_url` | string | 头像链接 |
### 获取频道列表 ### 获取频道列表
@ -56,7 +60,7 @@ GuildInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
| `guild_name` | string | 频道名称 | | `guild_name` | string | 频道名称 |
| `guild_display_id` | int64 | 频道显示ID, 公测后可能作为搜索ID使用 | | `guild_display_id` | int64 | 频道显示ID, 公测后可能作为搜索ID使用 |
@ -68,13 +72,13 @@ GuildInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ---------- | ----- | ---- | | ---------- | ----- | ---- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
**响应数据** **响应数据**
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
| `guild_name` | string | 频道名称 | | `guild_name` | string | 频道名称 |
| `guild_profile` | string | 频道简介 | | `guild_profile` | string | 频道简介 |
| `create_time` | int64 | 创建时间 | | `create_time` | int64 | 创建时间 |
@ -82,7 +86,7 @@ GuildInfo:
| `max_robot_count` | int64 | 频道BOT数上限 | | `max_robot_count` | int64 | 频道BOT数上限 |
| `max_admin_count` | int64 | 频道管理员人数上限 | | `max_admin_count` | int64 | 频道管理员人数上限 |
| `member_count` | int64 | 已加入人数 | | `member_count` | int64 | 已加入人数 |
| `owner_id` | uint64 | 创建者ID | | `owner_id` | string | 创建者ID |
### 获取子频道列表 ### 获取子频道列表
@ -92,7 +96,7 @@ GuildInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ---------- | ----- | ---- | | ---------- | ----- | ---- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
| `no_cache` | bool | 是否无视缓存 | | `no_cache` | bool | 是否无视缓存 |
**响应数据** **响应数据**
@ -103,13 +107,12 @@ ChannelInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `owner_guild_id` | uint64 | 所属频道ID | | `owner_guild_id` | string | 所属频道ID |
| `channel_id` | uint64 | 子频道ID | | `channel_id` | string | 子频道ID |
| `channel_type` | int32 | 子频道类型 | | `channel_type` | int32 | 子频道类型 |
| `channel_name` | string | 频道名称 | | `channel_name` | string | 频道名称 |
| `create_time` | int64 | 创建时间 | | `create_time` | int64 | 创建时间 |
| `creator_id` | int64 | 创建者QQ号 | | `creator_tiny_id` | string | 创建者ID |
| `creator_tiny_id` | uint64 | 创建者ID |
| `talk_permission` | int32 | 发言权限类型 | | `talk_permission` | int32 | 发言权限类型 |
| `visible_type` | int32 | 可视性类型 | | `visible_type` | int32 | 可视性类型 |
| `current_slow_mode` | int32 | 当前启用的慢速模式Key | | `current_slow_mode` | int32 | 当前启用的慢速模式Key |
@ -131,35 +134,74 @@ SlowModeInfo:
| 1 | 文字频道 | | 1 | 文字频道 |
| 2 | 语音频道 | | 2 | 语音频道 |
| 5 | 直播频道 | | 5 | 直播频道 |
| 7 | 主题频道 |
### 获取频道成员列表 ### 获取频道成员列表
终结点: `/get_guild_members` 终结点: `/get_guild_member_list`
> 由于频道人数较多(数万), 请尽量不要全量拉取成员列表, 这将会导致严重的性能问题
>
> 尽量使用 `get_guild_member_profile` 接口代替全量拉取
**参数** **参数**
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ---------- | ----- | ---- | | ---------- | ----- | ---- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
| `next_token` | string | 翻页Token |
> `next_token` 为空的情况下, 将返回第一页的数据, 并在返回值附带下一页的 `token`
**响应数据** **响应数据**
> 注意: 类型内无任何成员将返回 `null`
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `members` | []GuildMemberInfo | 普通成员列表 | | `members` | []GuildMemberInfo | 成员列表 |
| `bots` | []GuildMemberInfo | 机器人列表 | | `finished` | bool | 是否最终页 |
| `admins` | []GuildMemberInfo | 管理员列表 | | `next_token` | string | 翻页Token |
GuildMemberInfo: GuildMemberInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `tiny_id` | uint64 | 成员ID | | `tiny_id` | string | 成员ID |
| `title` | string | 成员头衔 | | `title` | string | 成员头衔 |
| `nickname` | string | 成员昵称 | | `nickname` | string | 成员昵称 |
| `role` | int32 | 成员权限 | | `role_id` | string | 所在权限组ID |
| `role_name` | string | 所在权限组名称 |
> 默认情况下频道管理员的权限组ID为 `2`, 部分频道可能会另行创建, 需手动判断
>
> 此接口仅展现最新的权限组, 获取用户加入的所有权限组请使用 `get_guild_member_profile` 接口
### 单独获取频道成员信息
终结点: `/get_guild_member_profile`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
| `user_id` | string | 用户ID |
**响应数据**
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `tiny_id` | string | 用户ID |
| `nickname` | string | 用户昵称 |
| `avatar_url` | string | 头像地址 |
| `join_time` | int64 | 加入时间 |
| `roles` | []RoleInfo | 加入的所有权限组 |
RoleInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `role_id` | string | 权限组ID |
| `role_name` | string | 权限组名称 |
### 发送信息到子频道 ### 发送信息到子频道
@ -169,8 +211,8 @@ GuildMemberInfo:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ---------- | ----- | ---- | | ---------- | ----- | ---- |
| `guild_id` | uint64 | 频道ID | | `guild_id` | string | 频道ID |
| `channel_id` | uint64 | 子频道ID | | `channel_id` | string | 子频道ID |
| `message` | Message | 消息, 与原有消息类型相同 | | `message` | Message | 消息, 与原有消息类型相同 |
**响应数据** **响应数据**
@ -179,6 +221,108 @@ GuildMemberInfo:
| ------------- | ----- | ---------- | | ------------- | ----- | ---------- |
| `message_id` | string | 消息ID | | `message_id` | string | 消息ID |
### 获取话题频道帖子
终结点: `/get_topic_channel_feeds`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
| `channel_id` | string | 子频道ID |
**响应数据**
返回 `FeedInfo` 数组
FeedInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `id` | string | 帖子ID |
| `channel_id` | string | 子频道ID |
| `guild_id` | string | 频道ID |
| `create_time` | int64 | 发帖时间 |
| `title` | string | 帖子标题 |
| `sub_title` | string | 帖子副标题 |
| `poster_info` | PosterInfo | 发帖人信息 |
| `resource` | ResourceInfo | 媒体资源信息 |
| `resource.images` | []FeedMedia | 帖子附带的图片列表 |
| `resource.videos` | []FeedMedia | 帖子附带的视频列表 |
| `contents` | []FeedContent | 帖子内容 |
PosterInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `tiny_id` | string | 发帖人ID |
| `nickname` | string | 发帖人昵称 |
| `icon_url` | string | 发帖人头像链接 |
FeedMedia:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `file_id` | string | 媒体ID |
| `pattern_id` | string | 控件ID?(不确定) |
| `url` | string | 媒体链接 |
| `height` | int32 | 媒体高度 |
| `width` | int32 | 媒体宽度 |
FeedContent:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `type` | string | 内容类型 |
| `data` | Data | 内容数据 |
#### 内容类型列表:
| 类型 | 说明 |
| ----- | ---------- |
| `text` | 文本 |
| `face` | 表情 |
| `at` | At |
| `url_quote` | 链接引用 |
| `channel_quote` | 子频道引用 |
#### 内容类型对应数据列表:
- `text`
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `text` | string | 文本内容 |
- `face`
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `id` | string | 表情ID |
- `at`
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `id` | string | 目标ID |
| `qq` | string | 目标ID, 为确保和 `array message` 的一致性保留 |
- `url_quote`
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `display_text` | string | 显示文本 |
| `url` | string | 链接 |
- `channel_quote`
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `display_text` | string | 显示文本 |
| `guild_id` | string | 频道ID |
| `channel_id` | string | 子频道ID |
## 事件 ## 事件
### 收到频道消息 ### 收到频道消息
@ -190,13 +334,15 @@ GuildMemberInfo:
| `post_type` | string | `message` | 上报类型 | | `post_type` | string | `message` | 上报类型 |
| `message_type` | string | `guild` | 消息类型 | | `message_type` | string | `guild` | 消息类型 |
| `sub_type` | string | `channel` | 消息子类型 | | `sub_type` | string | `channel` | 消息子类型 |
| `guild_id` | uint64 | | 频道ID | | `guild_id` | string | | 频道ID |
| `channel_id` | uint64 | | 子频道ID | | `channel_id` | string | | 子频道ID |
| `user_id` | uint64 | | 消息发送者ID | | `user_id` | string | | 消息发送者ID |
| `message_id` | string | | 消息ID | | `message_id` | string | | 消息ID |
| `sender` | Sender | | 发送者 | | `sender` | Sender | | 发送者 |
| `message` | Message | | 消息内容 | | `message` | Message | | 消息内容 |
> 注: 此处的 `Sender` 对象为保证一致性, `user_id` 为 `uint64` 类型, 并添加了 `string` 类型的 `tiny_id` 字段
### 频道消息表情贴更新 ### 频道消息表情贴更新
**上报数据** **上报数据**
@ -205,9 +351,9 @@ GuildMemberInfo:
| ------------- | ------ | -------------- | -------------- | | ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 | | `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `message_reactions_updated` | 消息类型 | | `notice_type` | string | `message_reactions_updated` | 消息类型 |
| `guild_id` | uint64 | | 频道ID | | `guild_id` | string | | 频道ID |
| `channel_id` | uint64 | | 子频道ID | | `channel_id` | string | | 子频道ID |
| `user_id` | uint64 | | 操作者ID | | `user_id` | string | | 操作者ID |
| `message_id` | string | | 消息ID | | `message_id` | string | | 消息ID |
| `current_reactions` | []ReactionInfo | | 当前消息被贴表情列表 | | `current_reactions` | []ReactionInfo | | 当前消息被贴表情列表 |
@ -230,10 +376,10 @@ ReactionInfo:
| ------------- | ------ | -------------- | -------------- | | ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 | | `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_updated` | 消息类型 | | `notice_type` | string | `channel_updated` | 消息类型 |
| `guild_id` | uint64 | | 频道ID | | `guild_id` | string | | 频道ID |
| `channel_id` | uint64 | | 子频道ID | | `channel_id` | string | | 子频道ID |
| `user_id` | uint64 | | 操作者ID | | `user_id` | string | | 操作者ID |
| `operator_id` | uint64 | | 操作者ID | | `operator_id` | string | | 操作者ID |
| `old_info` | ChannelInfo | | 更新前的频道信息 | | `old_info` | ChannelInfo | | 更新前的频道信息 |
| `new_info` | ChannelInfo | | 更新后的频道信息 | | `new_info` | ChannelInfo | | 更新后的频道信息 |
@ -245,10 +391,10 @@ ReactionInfo:
| ------------- | ------ | -------------- | -------------- | | ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 | | `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_created` | 消息类型 | | `notice_type` | string | `channel_created` | 消息类型 |
| `guild_id` | uint64 | | 频道ID | | `guild_id` | string | | 频道ID |
| `channel_id` | uint64 | | 子频道ID | | `channel_id` | string | | 子频道ID |
| `user_id` | uint64 | | 操作者ID | | `user_id` | string | | 操作者ID |
| `operator_id` | uint64 | | 操作者ID | | `operator_id` | string | | 操作者ID |
| `channel_info` | ChannelInfo | | 频道信息 | | `channel_info` | ChannelInfo | | 频道信息 |
### 子频道删除 ### 子频道删除
@ -259,8 +405,8 @@ ReactionInfo:
| ------------- | ------ | -------------- | -------------- | | ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 | | `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_destroyed` | 消息类型 | | `notice_type` | string | `channel_destroyed` | 消息类型 |
| `guild_id` | uint64 | | 频道ID | | `guild_id` | string | | 频道ID |
| `channel_id` | uint64 | | 子频道ID | | `channel_id` | string | | 子频道ID |
| `user_id` | uint64 | | 操作者ID | | `user_id` | string | | 操作者ID |
| `operator_id` | uint64 | | 操作者ID | | `operator_id` | string | | 操作者ID |
| `channel_info` | ChannelInfo | | 频道信息 | | `channel_info` | ChannelInfo | | 频道信息 |

View File

@ -32,6 +32,8 @@ const (
VideoPath = "data/videos" VideoPath = "data/videos"
// CachePath go-cqhttp使用的缓存目录 // CachePath go-cqhttp使用的缓存目录
CachePath = "data/cache" CachePath = "data/cache"
// DumpsPath go-cqhttp使用错误转储目录
DumpsPath = "dumps"
) )
var ( var (

View File

@ -71,9 +71,6 @@ func DownloadFile(url, path string, limit int64, headers map[string]string) erro
req.Header.Set(k, v) req.Header.Set(k, v)
} }
if _, ok := headers["User-Agent"]; !ok {
req.Header["User-Agent"] = []string{UserAgent}
}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err

View File

@ -5,6 +5,7 @@ package terminal
import ( import (
"os" "os"
"path/filepath"
"syscall" "syscall"
"unsafe" "unsafe"
@ -44,7 +45,10 @@ func NoMoreDoubleClick() error {
return errors.Errorf("打开go-cqhttp.bat失败: %v", err) return errors.Errorf("打开go-cqhttp.bat失败: %v", err)
} }
_ = f.Truncate(0) _ = f.Truncate(0)
_, err = f.WriteString("%Created by go-cqhttp. DO NOT EDIT ME!%\nstart cmd /K go-cqhttp.exe")
ex, _ := os.Executable()
exPath := filepath.Base(ex)
_, err = f.WriteString("%Created by go-cqhttp. DO NOT EDIT ME!%\nstart cmd /K \"" + exPath + "\"")
if err != nil { if err != nil {
return errors.Errorf("写入go-cqhttp.bat失败: %v", err) return errors.Errorf("写入go-cqhttp.bat失败: %v", err)
} }

45
go.mod
View File

@ -5,7 +5,8 @@ go 1.17
require ( require (
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Microsoft/go-winio v0.5.1 github.com/Microsoft/go-winio v0.5.1
github.com/Mrs4s/MiraiGo v0.0.0-20211114170854-511e8c41edd2 github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17
github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/fumiama/go-hide-param v0.1.4 github.com/fumiama/go-hide-param v0.1.4
github.com/gabriel-vasile/mimetype v1.4.0 github.com/gabriel-vasile/mimetype v1.4.0
@ -13,33 +14,34 @@ require (
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/klauspost/compress v1.13.6 github.com/klauspost/compress v1.13.6
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-colorable v0.1.11 github.com/mattn/go-colorable v0.1.12
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/segmentio/asm v1.1.0 github.com/segmentio/asm v1.1.3
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/tidwall/gjson v1.11.0 github.com/tidwall/gjson v1.12.1
github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60 github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60
go.mongodb.org/mongo-driver v1.7.4 go.mongodb.org/mongo-driver v1.8.1
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
nhooyr.io/websocket v1.8.7
) )
require ( require (
github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect github.com/fumiama/imgsz v0.0.2 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gocq/rs v1.0.1 // indirect github.com/gocq/rs v1.0.1 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/google/uuid v1.1.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/lestrrat-go/strftime v1.0.5 // indirect github.com/lestrrat-go/strftime v1.0.5 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pierrec/lz4/v4 v4.1.11 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
@ -48,14 +50,13 @@ require (
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 // indirect golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.27.1 // indirect modernc.org/libc v1.8.1 // indirect
modernc.org/libc v1.11.70 // indirect modernc.org/mathutil v1.2.2 // indirect
modernc.org/mathutil v1.4.1 // indirect modernc.org/memory v1.0.4 // indirect
modernc.org/memory v1.0.5 // indirect
) )

281
go.sum
View File

@ -1,10 +1,13 @@
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Mrs4s/MiraiGo v0.0.0-20211114170854-511e8c41edd2 h1:4Lbei62HbTfp90vV9zRzbe6TkMlns7QMqSeeyjJuauU= github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17 h1:OQVnlWt5if0TOFoNGDjILazBjVTncSlPxGXabD+4MvE=
github.com/Mrs4s/MiraiGo v0.0.0-20211114170854-511e8c41edd2/go.mod h1:UOO08UBb5mVisfwrjduINu5GF9+gmsn33f2tnr83xnE= github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17/go.mod h1:rtKLkhMEi2YjsrXaNztT4uagUOPBxf6a+TNREkG097I=
github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956 h1:hnaAkKz4t+xpSNVp5mnuloRMd3Rj2Lfg5biZ3emv//c=
github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956/go.mod h1:CKKOWC7mBxd36zxsCB1V8DTrwlTNRQvkSVbYqyUiGEE=
github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc h1:AAx50/fb/xS4lvsdQg+bFbGvqSDhyV1MF+p2PLCamZ0=
github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc/go.mod h1:OMmITAib6POA37xCichWM0aRnoVpSMZO1rB/G01wrr0=
github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY= github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY=
github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -15,130 +18,58 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fumiama/go-hide-param v0.1.4 h1:y7TRTzZMdCH9GOXnIzU3B+1BSkcmvejVGmGsz4t0DGU= github.com/fumiama/go-hide-param v0.1.4 h1:y7TRTzZMdCH9GOXnIzU3B+1BSkcmvejVGmGsz4t0DGU=
github.com/fumiama/go-hide-param v0.1.4/go.mod h1:vJkQlJIEI56nIyp7tCQu1/2QOyKtZpudsnJkGk9U1aY= github.com/fumiama/go-hide-param v0.1.4/go.mod h1:vJkQlJIEI56nIyp7tCQu1/2QOyKtZpudsnJkGk9U1aY=
github.com/fumiama/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak=
github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
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.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.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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98 h1:NJDZEa7gibUa0w4tie8qKeQFKdeKFUbecWyQDPdRx40= github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98 h1:NJDZEa7gibUa0w4tie8qKeQFKdeKFUbecWyQDPdRx40=
github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98/go.mod h1:E5TBHc60dsWtOL7sbXCb3P9i4xrj2J7Zm5sEJftIc1w= github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98/go.mod h1:E5TBHc60dsWtOL7sbXCb3P9i4xrj2J7Zm5sEJftIc1w=
github.com/gocq/rs v1.0.1 h1:ng7nhXmnx3SnfM0DOqmbP6GmQp1xGwRG9XmBiLFDWuM= github.com/gocq/rs v1.0.1 h1:ng7nhXmnx3SnfM0DOqmbP6GmQp1xGwRG9XmBiLFDWuM=
github.com/gocq/rs v1.0.1/go.mod h1:8oaQnRvqn1fMh8i5zsetgQo03OUXksJV1k+dpmExxcY= github.com/gocq/rs v1.0.1/go.mod h1:8oaQnRvqn1fMh8i5zsetgQo03OUXksJV1k+dpmExxcY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/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 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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 h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= 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/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE= github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE=
github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.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 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pierrec/lz4/v4 v4.1.11 h1:LVs17FAZJFOjgmJXl9Tf13WfLUvZq7/RjfEJrnwZ9OE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pierrec/lz4/v4 v4.1.11/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -146,42 +77,29 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/segmentio/asm v1.1.0 h1:fkVr8k5J4sKoFjTGVD6r1yKvDKqmvrEh3K7iyVxgBs8=
github.com/segmentio/asm v1.1.0/go.mod h1:4EUJGaKsB8ImLUwOGORVsNd9vTRDeh44JGsY4aKp5I4=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/gjson v1.11.0 h1:C16pk7tQNiH6VlCrtIXL1w8GaOsi1X3W8KDkE1BuYd4=
github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60 h1:lRKf10iIOW0VsH5WDF621ihzR+R2wEBZVtNRHuLLCb4= github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60 h1:lRKf10iIOW0VsH5WDF621ihzR+R2wEBZVtNRHuLLCb4=
github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60/go.mod h1:ecFKZPX81BaB70I6ruUgEwYcDOtuNgJGnjdK+MIl5ko= github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60/go.mod h1:ecFKZPX81BaB70I6ruUgEwYcDOtuNgJGnjdK+MIl5ko=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
@ -190,191 +108,74 @@ github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= go.mongodb.org/mongo-driver v1.8.1 h1:OZE4Wni/SJlrcmSIBRYNzunX5TKxjrTS4jKSnA99oKU=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= go.mongodb.org/mongo-driver v1.8.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.mongodb.org/mongo-driver v1.7.4 h1:sllcioag8Mec0LYkftYWq+cKNPIR4Kqq3iv9ZXY0g/E= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.mongodb.org/mongo-driver v1.7.4/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo= golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/libc v1.8.1 h1:y9oPIhwcaFXxX7kMp6Qb2ZLKzr0mDkikWN3CV5GS63o=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.70 h1:OHnBZYEJF8CuLOH++G4XYL2lZ4yLH/kkKTRf6gqV5UE=
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

View File

@ -29,11 +29,3 @@ var (
func nocheck(_ io.ReadSeeker) (bool, string) { func nocheck(_ io.ReadSeeker) (bool, string) {
return true, "" return true, ""
} }
// todo: enable all in v1.1.0
// onebot 12 feature
const (
AcceptOneBot12HTTPEndPoint = false
AcceptOneBot12Message = false
)

View File

@ -39,6 +39,7 @@ var (
LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志 LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志
LogColorful bool // 是否启用日志颜色 LogColorful bool // 是否启用日志颜色
FastStart bool // 是否为快速启动 FastStart bool // 是否为快速启动
AllowTempSession bool // 是否允许发送临时会话信息
PostFormat string // 上报格式 string or array PostFormat string // 上报格式 string or array
Proxy string // 存储 proxy_rewrite,用于设置代理 Proxy string // 存储 proxy_rewrite,用于设置代理
@ -63,6 +64,7 @@ func Parse() {
flag.BoolVar(&LittleH, "h", false, "this Help") flag.BoolVar(&LittleH, "h", false, "this Help")
flag.StringVar(&LittleWD, "w", "", "cover the working directory") flag.StringVar(&LittleWD, "w", "", "cover the working directory")
d := flag.Bool("D", false, "debug mode") d := flag.Bool("D", false, "debug mode")
flag.BoolVar(&FastStart, "faststart", false, "skip waiting 5 seconds")
flag.Parse() flag.Parse()
if *d { if *d {
@ -85,6 +87,7 @@ func Init() {
SkipMimeScan = conf.Message.SkipMimeScan SkipMimeScan = conf.Message.SkipMimeScan
ReportSelfMessage = conf.Message.ReportSelfMessage ReportSelfMessage = conf.Message.ReportSelfMessage
UseSSOAddress = conf.Account.UseSSOAddress UseSSOAddress = conf.Account.UseSSOAddress
AllowTempSession = conf.Account.AllowTempSession
} }
{ // others { // others
Proxy = conf.Message.ProxyRewrite Proxy = conf.Message.ProxyRewrite

View File

@ -475,15 +475,7 @@ func (d *DB) Insert(chash *byte, data []byte) {
d.flushSuper() d.flushSuper()
} }
// Get look up item with the given key 'hash' in the database file. Length of the func (d *DB) readValue(off int64) []byte {
// item is stored in 'len'. Returns a pointer to the contents of the item.
// The returned pointer should be released with free() after use.
func (d *DB) Get(hash *byte) []byte {
off := d.lookup(d.top, hash)
if off == 0 {
return nil
}
d.fd.Seek(off, io.SeekStart) d.fd.Seek(off, io.SeekStart)
length, err := read32(d.fd) length, err := read32(d.fd)
if err != nil { if err != nil {
@ -497,6 +489,17 @@ func (d *DB) Get(hash *byte) []byte {
return data[:n] return data[:n]
} }
// Get look up item with the given key 'hash' in the database file. Length of the
// item is stored in 'len'. Returns a pointer to the contents of the item.
// The returned pointer should be released with free() after use.
func (d *DB) Get(hash *byte) []byte {
off := d.lookup(d.top, hash)
if off == 0 {
return nil
}
return d.readValue(off)
}
// Delete remove item with the given key 'hash' from the database file. // Delete remove item with the given key 'hash' from the database file.
func (d *DB) Delete(hash *byte) error { func (d *DB) Delete(hash *byte) error {
var h [hashSize]byte var h [hashSize]byte
@ -522,3 +525,29 @@ func (d *DB) Delete(hash *byte) error {
d.flushSuper() d.flushSuper()
return nil return nil
} }
// Foreach iterates over all items in the database file.
func (d *DB) Foreach(iter func(key [16]byte, value []byte)) {
if d.top != 0 {
top := d.get(d.top)
d.iterate(top, iter)
}
}
func (d *DB) iterate(table *table, iter func(key [16]byte, value []byte)) {
for i := 0; i < table.size; i++ {
item := table.items[i]
offset := item.offset
iter(item.hash, d.readValue(offset))
if item.child != 0 {
child := d.get(item.child)
d.iterate(child, iter)
}
}
item := table.items[table.size]
if item.child != 0 {
child := d.get(item.child)
d.iterate(child, iter)
}
}

View File

@ -5,6 +5,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/Mrs4s/MiraiGo/utils"
assert2 "github.com/stretchr/testify/assert" assert2 "github.com/stretchr/testify/assert"
) )
@ -26,36 +27,71 @@ func TestBtree(t *testing.T) {
f := tempfile(t) f := tempfile(t)
defer os.Remove(f) defer os.Remove(f)
bt, err := Create(f) bt, err := Create(f)
assert2.NoError(t, err) assert := assert2.New(t)
assert.NoError(err)
var tests = []string{ tests := []string{
"hello world", "hello world",
"123", "123",
"We are met on a great battle-field of that war.", "We are met on a great battle-field of that war.",
"Abraham Lincoln, November 19, 1863, Gettysburg, Pennsylvania", "Abraham Lincoln, November 19, 1863, Gettysburg, Pennsylvania",
} }
var sha = make([]*byte, len(tests)) sha := make([]*byte, len(tests))
for i, tt := range tests { for i, tt := range tests {
var hash = sha1.New() hash := sha1.New()
hash.Write([]byte(tt)) hash.Write([]byte(tt))
sha[i] = &hash.Sum(nil)[0] sha[i] = &hash.Sum(nil)[0]
bt.Insert(sha[i], []byte(tt)) bt.Insert(sha[i], []byte(tt))
} }
assert2.NoError(t, bt.Close()) assert.NoError(bt.Close())
bt, err = Open(f) bt, err = Open(f)
assert2.NoError(t, err) assert.NoError(err)
var ss []string
bt.Foreach(func(key [16]byte, value []byte) {
ss = append(ss, string(value))
})
assert.ElementsMatch(tests, ss)
for i, tt := range tests { for i, tt := range tests {
assert2.Equal(t, []byte(tt), bt.Get(sha[i])) assert.Equal([]byte(tt), bt.Get(sha[i]))
} }
for i := range tests { for i := range tests {
assert2.NoError(t, bt.Delete(sha[i])) assert.NoError(bt.Delete(sha[i]))
} }
for i := range tests { for i := range tests {
assert2.Equal(t, []byte(nil), bt.Get(sha[i])) assert.Equal([]byte(nil), bt.Get(sha[i]))
} }
assert.NoError(bt.Close())
}
func testForeach(t *testing.T, elemSize int) {
expected := make([]string, elemSize)
for i := 0; i < elemSize; i++ {
expected[i] = utils.RandomString(20)
}
f := tempfile(t)
defer os.Remove(f)
bt, err := Create(f)
assert2.NoError(t, err)
for _, v := range expected {
hash := sha1.New()
hash.Write([]byte(v))
bt.Insert(&hash.Sum(nil)[0], []byte(v))
}
var got []string
bt.Foreach(func(key [16]byte, value []byte) {
got = append(got, string(value))
})
assert2.ElementsMatch(t, expected, got)
assert2.NoError(t, bt.Close()) assert2.NoError(t, bt.Close())
} }
func TestDB_Foreach(t *testing.T) {
elemSizes := []int{0, 5, 100, 200}
for _, size := range elemSizes {
testForeach(t, size)
}
}

View File

@ -45,7 +45,7 @@ func resethash(sha1 *byte) {
// reading table // reading table
func read32(r io.Reader) (int32, error) { func read32(r io.Reader) (int32, error) {
var b = make([]byte, 4) b := make([]byte, 4)
_, err := r.Read(b) _, err := r.Read(b)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -12,11 +12,6 @@ import (
"github.com/Mrs4s/go-cqhttp/internal/btree" "github.com/Mrs4s/go-cqhttp/internal/btree"
) )
// todo(wdvxdr): always enable db-cache in v1.0.0
// EnableCacheDB 是否启用 btree db缓存图片等
var EnableCacheDB bool
// Media Cache DBs // Media Cache DBs
var ( var (
Image Cache Image Cache
@ -63,17 +58,15 @@ func (c *Cache) Delete(md5 []byte) {
// Init 初始化 Cache // Init 初始化 Cache
func Init() { func Init() {
node, ok := base.Database["cache"] node, ok := base.Database["cache"]
if !ok {
return
}
EnableCacheDB = true
var conf map[string]string var conf map[string]string
err := node.Decode(&conf) if ok {
if err != nil { err := node.Decode(&conf)
log.Fatalf("failed to read cache config: %v", err) if err != nil {
log.Fatalf("failed to read cache config: %v", err)
}
} }
var open = func(typ string, cache *Cache) { open := func(typ string, cache *Cache) {
file := conf[typ] file := conf[typ]
if file == "" { if file == "" {
file = fmt.Sprintf("data/%s.db", typ) file = fmt.Sprintf("data/%s.db", typ)

View File

@ -3,7 +3,6 @@ package param
import ( import (
"math" "math"
"reflect"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -94,37 +93,3 @@ func Base64DecodeString(s string) ([]byte, error) {
n, err := e.Decode(dst, utils.S2B(s)) n, err := e.Decode(dst, utils.S2B(s))
return dst[:n], err return dst[:n], err
} }
// SetAtDefault 在变量 variable 为默认值 defaultValue 的时候修改为 value
func SetAtDefault(variable, value, defaultValue interface{}) {
v := reflect.ValueOf(variable)
v2 := reflect.ValueOf(value)
if v.Kind() != reflect.Ptr || v.IsNil() {
return
}
v = v.Elem()
if v.Interface() != defaultValue {
return
}
if v.Kind() != v2.Kind() {
return
}
v.Set(v2)
}
// SetExcludeDefault 在目标值 value 不为默认值 defaultValue 时修改 variable 为 value
func SetExcludeDefault(variable, value, defaultValue interface{}) {
v := reflect.ValueOf(variable)
v2 := reflect.ValueOf(value)
if v.Kind() != reflect.Ptr || v.IsNil() {
return
}
v = v.Elem()
if reflect.Indirect(v2).Interface() != defaultValue {
return
}
if v.Kind() != v2.Kind() {
return
}
v.Set(v2)
}

406
main.go
View File

@ -1,33 +1,7 @@
package main package main
import ( import (
"crypto/aes" "github.com/Mrs4s/go-cqhttp/cmd/gocq"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"os"
"path"
"sync"
"time"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
para "github.com/fumiama/go-hide-param"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/term"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/global/terminal"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/cache"
"github.com/Mrs4s/go-cqhttp/internal/selfdiagnosis"
"github.com/Mrs4s/go-cqhttp/internal/selfupdate"
"github.com/Mrs4s/go-cqhttp/modules/servers"
"github.com/Mrs4s/go-cqhttp/server"
_ "github.com/Mrs4s/go-cqhttp/db/leveldb" // leveldb _ "github.com/Mrs4s/go-cqhttp/db/leveldb" // leveldb
_ "github.com/Mrs4s/go-cqhttp/modules/mime" // mime检查模块 _ "github.com/Mrs4s/go-cqhttp/modules/mime" // mime检查模块
@ -35,382 +9,6 @@ import (
_ "github.com/Mrs4s/go-cqhttp/modules/silk" // silk编码模块 _ "github.com/Mrs4s/go-cqhttp/modules/silk" // silk编码模块
) )
// 允许通过配置文件设置的状态列表
var allowStatus = [...]client.UserOnlineStatus{
client.StatusOnline, client.StatusAway, client.StatusInvisible, client.StatusBusy,
client.StatusListening, client.StatusConstellation, client.StatusWeather, client.StatusMeetSpring,
client.StatusTimi, client.StatusEatChicken, client.StatusLoving, client.StatusWangWang, client.StatusCookedRice,
client.StatusStudy, client.StatusStayUp, client.StatusPlayBall, client.StatusSignal, client.StatusStudyOnline,
client.StatusGaming, client.StatusVacationing, client.StatusWatchingTV, client.StatusFitness,
}
func main() { func main() {
base.Parse() gocq.Main()
if !base.FastStart && terminal.RunningByDoubleClick() {
err := terminal.NoMoreDoubleClick()
if err != nil {
log.Errorf("遇到错误: %v", err)
time.Sleep(time.Second * 5)
}
return
}
switch {
case base.LittleH:
base.Help()
case base.LittleD:
server.Daemon()
case base.LittleWD != "":
base.ResetWorkingDir()
}
base.Init()
rotateOptions := []rotatelogs.Option{
rotatelogs.WithRotationTime(time.Hour * 24),
}
rotateOptions = append(rotateOptions, rotatelogs.WithMaxAge(base.LogAging))
if base.LogForceNew {
rotateOptions = append(rotateOptions, rotatelogs.ForceNewFile())
}
w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotateOptions...)
if err != nil {
log.Errorf("rotatelogs init err: %v", err)
panic(err)
}
consoleFormatter := global.LogFormat{EnableColor: base.LogColorful}
fileFormatter := global.LogFormat{EnableColor: false}
log.AddHook(global.NewLocalHook(w, consoleFormatter, fileFormatter, global.GetLogLevel(base.LogLevel)...))
mkCacheDir := func(path string, _type string) {
if !global.PathExists(path) {
if err := os.MkdirAll(path, 0o755); err != nil {
log.Fatalf("创建%s缓存文件夹失败: %v", _type, err)
}
}
}
mkCacheDir(global.ImagePath, "图片")
mkCacheDir(global.VoicePath, "语音")
mkCacheDir(global.VideoPath, "视频")
mkCacheDir(global.CachePath, "发送图片")
mkCacheDir(path.Join(global.ImagePath, "guild-images"), "频道图片缓存")
cache.Init()
db.Init()
if err := db.Open(); err != nil {
log.Fatalf("打开数据库失败: %v", err)
}
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.SelfUpdate(arg[i+1])
} else {
selfupdate.SelfUpdate("")
}
case "key":
p := i + 1
if len(arg) > p {
byteKey = []byte(arg[p])
para.Hide(p)
}
case "faststart":
base.FastStart = true
}
}
}
if (base.Account.Uin == 0 || (base.Account.Password == "" && !base.Account.Encrypt)) && !global.PathExists("session.token") {
log.Warn("账号密码未配置, 将使用二维码登录.")
if !base.FastStart {
log.Warn("将在 5秒 后继续.")
time.Sleep(time.Second * 5)
}
}
log.Info("当前版本:", base.Version)
if base.Debug {
log.SetLevel(log.DebugLevel)
log.SetReportCaller(true)
log.Warnf("已开启Debug模式.")
log.Debugf("开发交流群: 192548878")
}
log.Info("用户交流群: 721829413")
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice()
_ = os.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0o644)
log.Info("已生成设备信息并保存到 device.json 文件.")
} else {
log.Info("将使用 device.json 内的设备信息运行Bot.")
if err := client.SystemDeviceInfo.ReadJson([]byte(global.ReadAllText("device.json"))); err != nil {
log.Fatalf("加载设备信息失败: %v", err)
}
}
if base.Account.Encrypt {
if !global.PathExists("password.encrypt") {
if base.Account.Password == "" {
log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.")
readLine()
os.Exit(0)
}
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
base.PasswordHash = md5.Sum([]byte(base.Account.Password))
_ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(base.PasswordHash[:], byteKey)), 0o644)
log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
} else {
if base.Account.Password != "" {
log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
}
if len(byteKey) == 0 {
log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
cancel := make(chan struct{}, 1)
state, _ := term.GetState(int(os.Stdin.Fd()))
go func() {
select {
case <-cancel:
return
case <-time.After(time.Second * 45):
log.Infof("解密key输入超时")
time.Sleep(3 * time.Second)
_ = term.Restore(int(os.Stdin.Fd()), state)
os.Exit(0)
}
}()
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
cancel <- struct{}{}
} else {
log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
}
encrypt, _ := os.ReadFile("password.encrypt")
ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
if err != nil {
log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
}
copy(base.PasswordHash[:], ph)
}
} else if len(base.Account.Password) > 0 {
base.PasswordHash = md5.Sum([]byte(base.Account.Password))
}
if !base.FastStart {
log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
time.Sleep(time.Second * 5)
}
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"
case client.QiDian:
return "企点"
}
return "未知"
}())
cli = newClient()
isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt
isTokenLogin := false
saveToken := func() {
base.AccountToken = cli.GenToken()
_ = os.WriteFile("session.token", base.AccountToken, 0o644)
}
if global.PathExists("session.token") {
token, err := os.ReadFile("session.token")
if err == nil {
if base.Account.Uin != 0 {
r := binary.NewReader(token)
cu := r.ReadInt64()
if cu != base.Account.Uin {
log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", base.Account.Uin, cu)
log.Warnf("1. 使用会话缓存继续.")
log.Warnf("2. 删除会话缓存并重启.")
log.Warnf("请选择: (5秒后自动选1)")
text := readLineTimeout(time.Second*5, "1")
if text == "2" {
_ = os.Remove("session.token")
log.Infof("缓存已删除.")
os.Exit(0)
}
}
}
if err = cli.TokenLogin(token); err != nil {
_ = os.Remove("session.token")
log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
time.Sleep(time.Second)
cli.Disconnect()
cli.Release()
cli = newClient()
} else {
isTokenLogin = true
}
}
}
if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} {
cli.Uin = base.Account.Uin
cli.PasswordMd5 = base.PasswordHash
}
if !isTokenLogin {
if !isQRCodeLogin {
if err := commonLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
} else {
if err := qrcodeLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
}
}
var times uint = 1 // 重试次数
var reLoginLock sync.Mutex
cli.OnDisconnected(func(q *client.QQClient, e *client.ClientDisconnectedEvent) {
reLoginLock.Lock()
defer reLoginLock.Unlock()
times = 1
if cli.Online {
return
}
log.Warnf("Bot已离线: %v", e.Message)
time.Sleep(time.Second * time.Duration(base.Reconnect.Delay))
for {
if base.Reconnect.Disabled {
log.Warnf("未启用自动重连, 将退出.")
os.Exit(1)
}
if times > base.Reconnect.MaxTimes && base.Reconnect.MaxTimes != 0 {
log.Fatalf("Bot重连次数超过限制, 停止")
}
times++
if base.Reconnect.Interval > 0 {
log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v/%v", base.Reconnect.Interval, times, base.Reconnect.MaxTimes)
time.Sleep(time.Second * time.Duration(base.Reconnect.Interval))
} else {
time.Sleep(time.Second)
}
if cli.Online {
log.Infof("登录已完成")
break
}
log.Warnf("尝试重连...")
err := cli.TokenLogin(base.AccountToken)
if err == nil {
saveToken()
return
}
log.Warnf("快速重连失败: %v", err)
if isQRCodeLogin {
log.Fatalf("快速重连失败, 扫码登录无法恢复会话.")
}
log.Warnf("快速重连失败, 尝试普通登录. 这可能是因为其他端强行T下线导致的.")
time.Sleep(time.Second)
if err := commonLogin(); err != nil {
log.Errorf("登录时发生致命错误: %v", err)
} else {
saveToken()
break
}
}
})
saveToken()
cli.AllowSlider = true
log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
log.Info("开始加载好友列表...")
global.Check(cli.ReloadFriendList(), true)
log.Infof("共加载 %v 个好友.", len(cli.FriendList))
log.Infof("开始加载群列表...")
global.Check(cli.ReloadGroupList(), true)
log.Infof("共加载 %v 个群.", len(cli.GroupList))
if uint(base.Account.Status) >= uint(len(allowStatus)) {
base.Account.Status = 0
}
cli.SetOnlineStatus(allowStatus[base.Account.Status])
servers.Run(coolq.NewQQBot(cli))
log.Info("资源初始化完成, 开始处理信息.")
log.Info("アトリは、高性能ですから!")
go selfupdate.CheckUpdate()
go func() {
time.Sleep(5 * time.Second)
go selfdiagnosis.NetworkDiagnosis(cli)
}()
<-global.SetupMainSignalHandler()
}
// 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
}
func newClient() *client.QQClient {
c := client.NewClientEmpty()
c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
if !base.UseSSOAddress {
log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
return false
}
log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
return true
})
if global.PathExists("address.txt") {
log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
addr := global.ReadAddrFile("address.txt")
if len(addr) > 0 {
c.SetCustomServer(addr)
}
log.Infof("读取到 %v 个自定义地址.", len(addr))
}
c.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)
}
})
return c
} }

View File

@ -18,6 +18,9 @@ func (c *Caller) call(action string, p Getter) global.MSG {
p0 := p.Get("context") p0 := p.Get("context")
p1 := p.Get("operation") p1 := p.Get("operation")
return c.bot.CQHandleQuickOperation(p0, p1) return c.bot.CQHandleQuickOperation(p0, p1)
case ".ocr_image", "ocr_image":
p0 := p.Get("image").String()
return c.bot.CQOcrImage(p0)
case "_get_model_show": case "_get_model_show":
p0 := p.Get("model").String() p0 := p.Get("model").String()
return c.bot.CQGetModelShow(p0) return c.bot.CQGetModelShow(p0)
@ -45,6 +48,13 @@ func (c *Caller) call(action string, p Getter) global.MSG {
p1 := p.Get("parent_id").String() p1 := p.Get("parent_id").String()
p2 := p.Get("name").String() p2 := p.Get("name").String()
return c.bot.CQGroupFileCreateFolder(p0, p1, p2) return c.bot.CQGroupFileCreateFolder(p0, p1, p2)
case "create_guild_role":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("name").String()
p2 := uint32(p.Get("color").Uint())
p3 := p.Get("independent").Bool()
p4 := p.Get("initial_users")
return c.bot.CQCreateGuildRole(p0, p1, p2, p3, p4)
case "delete_essence_msg": case "delete_essence_msg":
p0 := int32(p.Get("message_id").Int()) p0 := int32(p.Get("message_id").Int())
return c.bot.CQDeleteEssenceMessage(p0) return c.bot.CQDeleteEssenceMessage(p0)
@ -54,12 +64,16 @@ func (c *Caller) call(action string, p Getter) global.MSG {
case "delete_group_file": case "delete_group_file":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
p1 := p.Get("file_id").String() p1 := p.Get("file_id").String()
p2 := int32(p.Get("bus_id").Int()) p2 := int32(p.Get("[busid,bus_id].0").Int())
return c.bot.CQGroupFileDeleteFile(p0, p1, p2) return c.bot.CQGroupFileDeleteFile(p0, p1, p2)
case "delete_group_folder": case "delete_group_folder":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
p1 := p.Get("folder_id").String() p1 := p.Get("folder_id").String()
return c.bot.CQGroupFileDeleteFolder(p0, p1) return c.bot.CQGroupFileDeleteFolder(p0, p1)
case "delete_guild_role":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("role_id").Uint()
return c.bot.CQDeleteGuildRole(p0, p1)
case "delete_msg": case "delete_msg":
p0 := int32(p.Get("message_id").Int()) p0 := int32(p.Get("message_id").Int())
return c.bot.CQDeleteMessage(p0) return c.bot.CQDeleteMessage(p0)
@ -88,7 +102,7 @@ func (c *Caller) call(action string, p Getter) global.MSG {
case "get_group_file_url": case "get_group_file_url":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
p1 := p.Get("file_id").String() p1 := p.Get("file_id").String()
p2 := int32(p.Get("bus_id").Int()) p2 := int32(p.Get("[busid,bus_id].0").Int())
return c.bot.CQGetGroupFileURL(p0, p1, p2) return c.bot.CQGetGroupFileURL(p0, p1, p2)
case "get_group_files_by_folder": case "get_group_files_by_folder":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
@ -129,12 +143,24 @@ func (c *Caller) call(action string, p Getter) global.MSG {
return c.bot.CQGetGuildChannelList(p0, p1) return c.bot.CQGetGuildChannelList(p0, p1)
case "get_guild_list": case "get_guild_list":
return c.bot.CQGetGuildList() return c.bot.CQGetGuildList()
case "get_guild_members": case "get_guild_member_list":
p0 := p.Get("guild_id").Uint() p0 := p.Get("guild_id").Uint()
return c.bot.CQGetGuildMembers(p0) p1 := p.Get("next_token").String()
return c.bot.CQGetGuildMembers(p0, p1)
case "get_guild_member_profile":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("user_id").Uint()
return c.bot.CQGetGuildMemberProfile(p0, p1)
case "get_guild_meta_by_guest": case "get_guild_meta_by_guest":
p0 := p.Get("guild_id").Uint() p0 := p.Get("guild_id").Uint()
return c.bot.CQGetGuildMetaByGuest(p0) return c.bot.CQGetGuildMetaByGuest(p0)
case "get_guild_msg":
p0 := p.Get("message_id").String()
p1 := p.Get("no_cache").Bool()
return c.bot.CQGetGuildMessage(p0, p1)
case "get_guild_roles":
p0 := p.Get("guild_id").Uint()
return c.bot.CQGetGuildRoles(p0)
case "get_guild_service_profile": case "get_guild_service_profile":
return c.bot.CQGetGuildServiceProfile() return c.bot.CQGetGuildServiceProfile()
case "get_image": case "get_image":
@ -153,6 +179,10 @@ func (c *Caller) call(action string, p Getter) global.MSG {
case "get_stranger_info": case "get_stranger_info":
p0 := p.Get("user_id").Int() p0 := p.Get("user_id").Int()
return c.bot.CQGetStrangerInfo(p0) return c.bot.CQGetStrangerInfo(p0)
case "get_topic_channel_feeds":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("channel_id").Uint()
return c.bot.CQGetTopicChannelFeeds(p0, p1)
case "get_unidirectional_friend_list": case "get_unidirectional_friend_list":
return c.bot.CQGetUnidirectionalFriendList() return c.bot.CQGetUnidirectionalFriendList()
case "get_version_info": case "get_version_info":
@ -160,9 +190,6 @@ func (c *Caller) call(action string, p Getter) global.MSG {
case "mark_msg_as_read": case "mark_msg_as_read":
p0 := int32(p.Get("message_id").Int()) p0 := int32(p.Get("message_id").Int())
return c.bot.CQMarkMessageAsRead(p0) return c.bot.CQMarkMessageAsRead(p0)
case "ocr_image", ".ocr_image":
p0 := p.Get("image").String()
return c.bot.CQOcrImage(p0)
case "qidian_get_account_info": case "qidian_get_account_info":
return c.bot.CQGetQiDianAccountInfo() return c.bot.CQGetQiDianAccountInfo()
case "reload_event_filter": case "reload_event_filter":
@ -233,7 +260,7 @@ func (c *Caller) call(action string, p Getter) global.MSG {
p1 := p.Get("user_id").Int() p1 := p.Get("user_id").Int()
p2 := uint32(1800) p2 := uint32(1800)
if pt := p.Get("duration"); pt.Exists() { if pt := p.Get("duration"); pt.Exists() {
p2 = uint32(pt.Int()) p2 = uint32(pt.Uint())
} }
return c.bot.CQSetGroupBan(p0, p1, p2) return c.bot.CQSetGroupBan(p0, p1, p2)
case "set_group_card": case "set_group_card":
@ -262,7 +289,7 @@ func (c *Caller) call(action string, p Getter) global.MSG {
case "set_group_special_title": case "set_group_special_title":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
p1 := p.Get("user_id").Int() p1 := p.Get("user_id").Int()
p2 := p.Get("title").String() p2 := p.Get("special_title").String()
return c.bot.CQSetGroupSpecialTitle(p0, p1, p2) return c.bot.CQSetGroupSpecialTitle(p0, p1, p2)
case "set_group_whole_ban": case "set_group_whole_ban":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
@ -271,6 +298,19 @@ func (c *Caller) call(action string, p Getter) global.MSG {
p1 = pt.Bool() p1 = pt.Bool()
} }
return c.bot.CQSetGroupWholeBan(p0, p1) return c.bot.CQSetGroupWholeBan(p0, p1)
case "set_guild_member_role":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("set").Bool()
p2 := p.Get("role_id").Uint()
p3 := p.Get("users")
return c.bot.CQSetGuildMemberRole(p0, p1, p2, p3)
case "update_guild_role":
p0 := p.Get("guild_id").Uint()
p1 := p.Get("role_id").Uint()
p2 := p.Get("name").String()
p3 := uint32(p.Get("color").Uint())
p4 := p.Get("indepedent").Bool()
return c.bot.CQModifyRoleInGuild(p0, p1, p2, p3, p4)
case "upload_group_file": case "upload_group_file":
p0 := p.Get("group_id").Int() p0 := p.Get("group_id").Int()
p1 := p.Get("file").String() p1 := p.Get("file").String()

View File

@ -6,10 +6,9 @@ import (
_ "embed" // embed the default config file _ "embed" // embed the default config file
"fmt" "fmt"
"os" "os"
"strconv" "regexp"
"strings" "strings"
"sync"
"github.com/Mrs4s/go-cqhttp/internal/param"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -29,12 +28,13 @@ type Reconnect struct {
// Account 账号配置 // Account 账号配置
type Account struct { type Account struct {
Uin int64 `yaml:"uin"` Uin int64 `yaml:"uin"`
Password string `yaml:"password"` Password string `yaml:"password"`
Encrypt bool `yaml:"encrypt"` Encrypt bool `yaml:"encrypt"`
Status int `yaml:"status"` Status int `yaml:"status"`
ReLogin *Reconnect `yaml:"relogin"` ReLogin *Reconnect `yaml:"relogin"`
UseSSOAddress bool `yaml:"use-sso-address"` UseSSOAddress bool `yaml:"use-sso-address"`
AllowTempSession bool `yaml:"allow-temp-session"`
} }
// Config 总配置文件 // Config 总配置文件
@ -69,68 +69,10 @@ type Config struct {
Database map[string]yaml.Node `yaml:"database"` Database map[string]yaml.Node `yaml:"database"`
} }
// MiddleWares 通信中间件 // Server 的简介和初始配置
type MiddleWares struct { type Server struct {
AccessToken string `yaml:"access-token"` Brief string
Filter string `yaml:"filter"` Default string
RateLimit struct {
Enabled bool `yaml:"enabled"`
Frequency float64 `yaml:"frequency"`
Bucket int `yaml:"bucket"`
} `yaml:"rate-limit"`
}
// HTTPServer HTTP通信相关配置
type HTTPServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Timeout int32 `yaml:"timeout"`
LongPolling struct {
Enabled bool `yaml:"enabled"`
MaxQueueSize int `yaml:"max-queue-size"`
} `yaml:"long-polling"`
Post []struct {
URL string `yaml:"url"`
Secret string `yaml:"secret"`
}
MiddleWares `yaml:"middlewares"`
}
// PprofServer pprof性能分析服务器相关配置
type PprofServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
}
// WebsocketServer 正向WS相关配置
type WebsocketServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
MiddleWares `yaml:"middlewares"`
}
// WebsocketReverse 反向WS相关配置
type WebsocketReverse struct {
Disabled bool `yaml:"disabled"`
Universal string `yaml:"universal"`
API string `yaml:"api"`
Event string `yaml:"event"`
ReconnectInterval int `yaml:"reconnect-interval"`
MiddleWares `yaml:"middlewares"`
}
// LambdaServer 云函数配置
type LambdaServer struct {
Disabled bool `yaml:"disabled"`
Type string `yaml:"type"`
MiddleWares `yaml:"middlewares"`
} }
// LevelDBConfig leveldb 相关配置 // LevelDBConfig leveldb 相关配置
@ -147,127 +89,58 @@ type MongoDBConfig struct {
// Parse 从默认配置文件路径中获取 // Parse 从默认配置文件路径中获取
func Parse(path string) *Config { func Parse(path string) *Config {
fromEnv := os.Getenv("GCQ_UIN") != "" file, err := os.ReadFile(path)
file, err := os.Open(path)
config := &Config{} config := &Config{}
if err == nil { if err == nil {
defer func() { _ = file.Close() }() err = yaml.NewDecoder(strings.NewReader(expand(string(file), os.Getenv))).Decode(config)
if err = yaml.NewDecoder(file).Decode(config); err != nil && !fromEnv { if err != nil {
log.Fatal("配置文件不合法!", err) log.Fatal("配置文件不合法!", err)
} }
} else if !fromEnv { } else {
generateConfig() generateConfig()
os.Exit(0) os.Exit(0)
} }
if fromEnv {
// type convert tools
toInt64 := func(str string) int64 {
i, _ := strconv.ParseInt(str, 10, 64)
return i
}
// load config from environment variable
param.SetAtDefault(&config.Account.Uin, toInt64(os.Getenv("GCQ_UIN")), int64(0))
param.SetAtDefault(&config.Account.Password, os.Getenv("GCQ_PWD"), "")
param.SetAtDefault(&config.Account.Status, int32(toInt64(os.Getenv("GCQ_STATUS"))), int32(0))
param.SetAtDefault(&config.Account.ReLogin.Disabled, !param.EnsureBool(os.Getenv("GCQ_RELOGIN_DISABLED"), true), false)
param.SetAtDefault(&config.Account.ReLogin.Delay, uint(toInt64(os.Getenv("GCQ_RELOGIN_DELAY"))), uint(0))
param.SetAtDefault(&config.Account.ReLogin.MaxTimes, uint(toInt64(os.Getenv("GCQ_RELOGIN_MAX_TIMES"))), uint(0))
dbConf := &LevelDBConfig{Enable: param.EnsureBool(os.Getenv("GCQ_LEVELDB"), true)}
if config.Database == nil {
config.Database = make(map[string]yaml.Node)
}
config.Database["leveldb"] = func() yaml.Node {
n := &yaml.Node{}
_ = n.Encode(dbConf)
return *n
}()
accessTokenEnv := os.Getenv("GCQ_ACCESS_TOKEN")
if os.Getenv("GCQ_HTTP_PORT") != "" {
node := &yaml.Node{}
httpConf := &HTTPServer{
Host: "0.0.0.0",
Port: 5700,
MiddleWares: MiddleWares{
AccessToken: accessTokenEnv,
},
}
param.SetExcludeDefault(&httpConf.Disabled, param.EnsureBool(os.Getenv("GCQ_HTTP_DISABLE"), false), false)
param.SetExcludeDefault(&httpConf.Host, os.Getenv("GCQ_HTTP_HOST"), "")
param.SetExcludeDefault(&httpConf.Port, int(toInt64(os.Getenv("GCQ_HTTP_PORT"))), 0)
if os.Getenv("GCQ_HTTP_POST_URL") != "" {
httpConf.Post = append(httpConf.Post, struct {
URL string `yaml:"url"`
Secret string `yaml:"secret"`
}{os.Getenv("GCQ_HTTP_POST_URL"), os.Getenv("GCQ_HTTP_POST_SECRET")})
}
_ = node.Encode(httpConf)
config.Servers = append(config.Servers, map[string]yaml.Node{"http": *node})
}
if os.Getenv("GCQ_WS_PORT") != "" {
node := &yaml.Node{}
wsServerConf := &WebsocketServer{
Host: "0.0.0.0",
Port: 6700,
MiddleWares: MiddleWares{
AccessToken: accessTokenEnv,
},
}
param.SetExcludeDefault(&wsServerConf.Disabled, param.EnsureBool(os.Getenv("GCQ_WS_DISABLE"), false), false)
param.SetExcludeDefault(&wsServerConf.Host, os.Getenv("GCQ_WS_HOST"), "")
param.SetExcludeDefault(&wsServerConf.Port, int(toInt64(os.Getenv("GCQ_WS_PORT"))), 0)
_ = node.Encode(wsServerConf)
config.Servers = append(config.Servers, map[string]yaml.Node{"ws": *node})
}
if os.Getenv("GCQ_RWS_API") != "" || os.Getenv("GCQ_RWS_EVENT") != "" || os.Getenv("GCQ_RWS_UNIVERSAL") != "" {
node := &yaml.Node{}
rwsConf := &WebsocketReverse{
MiddleWares: MiddleWares{
AccessToken: accessTokenEnv,
},
}
param.SetExcludeDefault(&rwsConf.Disabled, param.EnsureBool(os.Getenv("GCQ_RWS_DISABLE"), false), false)
param.SetExcludeDefault(&rwsConf.API, os.Getenv("GCQ_RWS_API"), "")
param.SetExcludeDefault(&rwsConf.Event, os.Getenv("GCQ_RWS_EVENT"), "")
param.SetExcludeDefault(&rwsConf.Universal, os.Getenv("GCQ_RWS_UNIVERSAL"), "")
_ = node.Encode(rwsConf)
config.Servers = append(config.Servers, map[string]yaml.Node{"ws-reverse": *node})
}
}
return config return config
} }
var (
serverconfs []*Server
mu sync.Mutex
)
// AddServer 添加该服务的简介和默认配置
func AddServer(s *Server) {
mu.Lock()
serverconfs = append(serverconfs, s)
mu.Unlock()
}
// generateConfig 生成配置文件 // generateConfig 生成配置文件
func generateConfig() { func generateConfig() {
fmt.Println("未找到配置文件,正在为您生成配置文件中!") fmt.Println("未找到配置文件,正在为您生成配置文件中!")
sb := strings.Builder{} sb := strings.Builder{}
sb.WriteString(defaultConfig) sb.WriteString(defaultConfig)
fmt.Print(`请选择你需要的通信方式: hint := "请选择你需要的通信方式:"
> 1: HTTP通信 for i, s := range serverconfs {
> 2: 正向 Websocket 通信 hint += fmt.Sprintf("\n> %d: %s", i, s.Brief)
> 3: 反向 Websocket 通信 }
> 4: pprof 性能分析服务器 hint += `
> 5: 云函数服务 请输入你需要的编号(0-9),可输入多个,同一编号也可输入多个(如: 233)
请输入你需要的编号,可输入多个,同一编号也可输入多个(如: 233) 您的选择是:`
您的选择是:`) fmt.Print(hint)
input := bufio.NewReader(os.Stdin) input := bufio.NewReader(os.Stdin)
readString, err := input.ReadString('\n') readString, err := input.ReadString('\n')
if err != nil { if err != nil {
log.Fatal("输入不合法: ", err) log.Fatal("输入不合法: ", err)
} }
rmax := len(serverconfs)
if rmax > 10 {
rmax = 10
}
for _, r := range readString { for _, r := range readString {
switch r { r -= '0'
case '1': if r >= 0 && r < rune(rmax) {
sb.WriteString(httpDefault) sb.WriteString(serverconfs[r].Default)
case '2':
sb.WriteString(wsDefault)
case '3':
sb.WriteString(wsReverseDefault)
case '4':
sb.WriteString(pprofDefault)
case '5':
sb.WriteString(lambdaDefault)
} }
} }
_ = os.WriteFile("config.yml", []byte(sb.String()), 0o644) _ = os.WriteFile("config.yml", []byte(sb.String()), 0o644)
@ -275,69 +148,26 @@ func generateConfig() {
_, _ = input.ReadString('\n') _, _ = input.ReadString('\n')
} }
const httpDefault = ` # HTTP 通信设置 // expand 使用正则进行环境变量展开
- http: // os.ExpandEnv 字符 $ 无法逃逸
# 服务端监听地址 // https://github.com/golang/go/issues/43482
host: 127.0.0.1 func expand(s string, mapping func(string) string) string {
# 服务端监听端口 r := regexp.MustCompile(`\${([a-zA-Z_]+[a-zA-Z0-9_:/.]*)}`)
port: 5700 return r.ReplaceAllStringFunc(s, func(s string) string {
# 反向HTTP超时时间, 单位秒 s = strings.Trim(s, "${}")
# 最小值为5小于5将会忽略本项设置 // todo: use strings.Cut once go1.18 is released
timeout: 5 before, after, ok := cut(s, ":")
# 长轮询拓展 m := mapping(before)
long-polling: if ok && m == "" {
# 是否开启 return after
enabled: false }
# 消息队列大小0 表示不限制队列大小,谨慎使用 return m
max-queue-size: 2000 })
middlewares: }
<<: *default # 引用默认中间件
# 反向HTTP POST地址列表
post:
#- url: '' # 地址
# secret: '' # 密钥
#- url: 127.0.0.1:5701 # 地址
# secret: '' # 密钥
`
const lambdaDefault = ` # LambdaServer 配置 func cut(s, sep string) (before, after string, found bool) {
- lambda: if i := strings.Index(s, sep); i >= 0 {
type: scf # scf: 腾讯云函数 aws: aws Lambda return s[:i], s[i+len(sep):], true
middlewares: }
<<: *default # 引用默认中间件 return s, "", false
` }
const wsDefault = ` # 正向WS设置
- ws:
# 正向WS服务器监听地址
host: 127.0.0.1
# 正向WS服务器监听端口
port: 6700
middlewares:
<<: *default # 引用默认中间件
`
const wsReverseDefault = ` # 反向WS设置
- ws-reverse:
# 反向WS Universal 地址
# 注意 设置了此项地址后下面两项将会被忽略
universal: ws://your_websocket_universal.server
# 反向WS API 地址
api: ws://your_websocket_api.server
# 反向WS Event 地址
event: ws://your_websocket_event.server
# 重连间隔 单位毫秒
reconnect-interval: 3000
middlewares:
<<: *default # 引用默认中间件
`
const pprofDefault = ` # pprof 性能分析服务器, 一般情况下不需要启用.
# 如果遇到性能问题请上传报告给开发者处理
# 注意: pprof服务不支持中间件、不支持鉴权. 请不要开放到公网
- pprof:
# pprof服务器监听地址
host: 127.0.0.1
# pprof服务器监听端口
port: 7700
`

View File

@ -0,0 +1,48 @@
package config
import (
"strings"
"testing"
)
func Test_expand(t *testing.T) {
nullStringMapping := func(_ string) string {
return ""
}
tests := []struct {
src string
mapping func(string) string
expected string
}{
{
src: "foo: ${bar}",
mapping: strings.ToUpper,
expected: "foo: BAR",
},
{
src: "$123",
mapping: strings.ToUpper,
expected: "$123",
},
{
src: "foo: ${bar:123456}",
mapping: nullStringMapping,
expected: "foo: 123456",
},
{
src: "foo: ${bar:127.0.0.1:5700}",
mapping: nullStringMapping,
expected: "foo: 127.0.0.1:5700",
},
{
src: "foo: ${bar:ws//localhost:9999/ws}",
mapping: nullStringMapping,
expected: "foo: ws//localhost:9999/ws",
},
}
for i, tt := range tests {
if got := expand(tt.src, tt.mapping); got != tt.expected {
t.Errorf("testcase %d failed, expected %v but got %v", i, tt.expected, got)
}
}
}

View File

@ -13,6 +13,8 @@ account: # 账号相关
# 是否使用服务器下发的新地址进行重连 # 是否使用服务器下发的新地址进行重连
# 注意, 此设置可能导致在海外服务器上连接情况更差 # 注意, 此设置可能导致在海外服务器上连接情况更差
use-sso-address: true use-sso-address: true
# 是否允许发送临时会话消息
allow-temp-session: false
heartbeat: heartbeat:
# 心跳频率, 单位秒 # 心跳频率, 单位秒

View File

@ -16,9 +16,33 @@ import (
"github.com/Mrs4s/go-cqhttp/modules/servers" "github.com/Mrs4s/go-cqhttp/modules/servers"
) )
const pprofDefault = ` # pprof 性能分析服务器, 一般情况下不需要启用.
# 如果遇到性能问题请上传报告给开发者处理
# 注意: pprof服务不支持中间件、不支持鉴权. 请不要开放到公网
- pprof:
# pprof服务器监听地址
host: 127.0.0.1
# pprof服务器监听端口
port: 7700
`
// pprofServer pprof性能分析服务器相关配置
type pprofServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
}
func init() {
config.AddServer(&config.Server{
Brief: "pprof 性能分析服务器",
Default: pprofDefault,
})
}
// runPprof 启动 pprof 性能分析服务器 // runPprof 启动 pprof 性能分析服务器
func runPprof(_ *coolq.CQBot, node yaml.Node) { func runPprof(_ *coolq.CQBot, node yaml.Node) {
var conf config.PprofServer var conf pprofServer
switch err := node.Decode(&conf); { switch err := node.Decode(&conf); {
case err != nil: case err != nil:
log.Warn("读取pprof配置失败 :", err) log.Warn("读取pprof配置失败 :", err)

View File

@ -8,7 +8,10 @@ import (
"github.com/Mrs4s/go-cqhttp/internal/base" "github.com/Mrs4s/go-cqhttp/internal/base"
) )
var svr = make(map[string]func(*coolq.CQBot, yaml.Node)) var (
svr = make(map[string]func(*coolq.CQBot, yaml.Node))
nocfgsvr = make(map[string]func(*coolq.CQBot))
)
// Register 注册 Server // Register 注册 Server
func Register(name string, proc func(*coolq.CQBot, yaml.Node)) { func Register(name string, proc func(*coolq.CQBot, yaml.Node)) {
@ -19,6 +22,15 @@ func Register(name string, proc func(*coolq.CQBot, yaml.Node)) {
svr[name] = proc svr[name] = proc
} }
// RegisterCustom 注册无需 config 的自定义 Server
func RegisterCustom(name string, proc func(*coolq.CQBot)) {
_, ok := nocfgsvr[name]
if ok {
panic(name + " server has existed")
}
nocfgsvr[name] = proc
}
// Run 运行所有svr // Run 运行所有svr
func Run(bot *coolq.CQBot) { func Run(bot *coolq.CQBot) {
for _, l := range base.Servers { for _, l := range base.Servers {
@ -28,4 +40,7 @@ func Run(bot *coolq.CQBot) {
} }
} }
} }
for _, fn := range nocfgsvr {
go fn(bot)
}
} }

View File

@ -2,17 +2,16 @@ package server
import ( import (
"bytes" "bytes"
"context"
"crypto/hmac" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -24,12 +23,33 @@ import (
"github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/modules/api" "github.com/Mrs4s/go-cqhttp/modules/api"
"github.com/Mrs4s/go-cqhttp/modules/config" "github.com/Mrs4s/go-cqhttp/modules/config"
"github.com/Mrs4s/go-cqhttp/modules/filter" "github.com/Mrs4s/go-cqhttp/modules/filter"
) )
// HTTPServer HTTP通信相关配置
type HTTPServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Timeout int32 `yaml:"timeout"`
LongPolling struct {
Enabled bool `yaml:"enabled"`
MaxQueueSize int `yaml:"max-queue-size"`
} `yaml:"long-polling"`
Post []httpServerPost `yaml:"post"`
MiddleWares `yaml:"middlewares"`
}
type httpServerPost struct {
URL string `yaml:"url"`
Secret string `yaml:"secret"`
MaxRetries *uint64 `yaml:"max-retries"`
RetriesInterval *uint64 `yaml:"retries-interval"`
}
type httpServer struct { type httpServer struct {
HTTP *http.Server HTTP *http.Server
api *api.Caller api *api.Caller
@ -38,12 +58,14 @@ type httpServer struct {
// HTTPClient 反向HTTP上报客户端 // HTTPClient 反向HTTP上报客户端
type HTTPClient struct { type HTTPClient struct {
bot *coolq.CQBot bot *coolq.CQBot
secret string secret string
addr string addr string
filter string filter string
apiPort int apiPort int
timeout int32 timeout int32
MaxRetries uint64
RetriesInterval uint64
} }
type httpCtx struct { type httpCtx struct {
@ -52,11 +74,44 @@ type httpCtx struct {
postForm url.Values postForm url.Values
} }
func (h *httpCtx) Get(s string) gjson.Result { const httpDefault = `
j := h.json.Get(s) - http: # HTTP 通信设置
if j.Exists() { host: 127.0.0.1 # 服务端监听地址
return j port: 5700 # 服务端监听端口
timeout: 5 # 反向 HTTP 超时时间, 单位秒,<5 时将被忽略
long-polling: # 长轮询拓展
enabled: false # 是否开启
max-queue-size: 2000 # 消息队列大小0 表示不限制队列大小,谨慎使用
middlewares:
<<: *default # 引用默认中间件
post: # 反向HTTP POST地址列表
#- url: '' # 地址
# secret: '' # 密钥
# max-retries: 3 # 最大重试0 时禁用
# retries-interval: 1500 # 重试时间单位毫秒0 时立即
#- url: http://127.0.0.1:5701/ # 地址
# secret: '' # 密钥
# max-retries: 10 # 最大重试0 时禁用
# retries-interval: 1000 # 重试时间单位毫秒0 时立即
`
func init() {
config.AddServer(&config.Server{Brief: "HTTP通信", Default: httpDefault})
}
var joinQuery = regexp.MustCompile(`\[(.+?),(.+?)]\.0`)
func (h *httpCtx) get(s string, join bool) gjson.Result {
// support gjson advanced syntax:
// h.Get("[a,b].0") see usage in http_test.go
if join && joinQuery.MatchString(s) {
matched := joinQuery.FindStringSubmatch(s)
if r := h.get(matched[1], false); r.Exists() {
return r
}
return h.get(matched[2], false)
} }
validJSONParam := func(p string) bool { validJSONParam := func(p string) bool {
return (strings.HasPrefix(p, "{") || strings.HasPrefix(p, "[")) && gjson.Valid(p) return (strings.HasPrefix(p, "{") || strings.HasPrefix(p, "[")) && gjson.Valid(p)
} }
@ -79,6 +134,14 @@ func (h *httpCtx) Get(s string) gjson.Result {
return gjson.Result{} return gjson.Result{}
} }
func (h *httpCtx) Get(s string) gjson.Result {
j := h.json.Get(s)
if j.Exists() {
return j
}
return h.get(s, true)
}
func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var ctx httpCtx var ctx httpCtx
contentType := request.Header.Get("Content-Type") contentType := request.Header.Get("Content-Type")
@ -121,7 +184,7 @@ func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request
} }
var response global.MSG var response global.MSG
if base.AcceptOneBot12HTTPEndPoint && request.URL.Path == "/" { if request.URL.Path == "/" {
action := strings.TrimSuffix(ctx.Get("action").Str, "_async") action := strings.TrimSuffix(ctx.Get("action").Str, "_async")
log.Debugf("HTTPServer接收到API调用: %v", action) log.Debugf("HTTPServer接收到API调用: %v", action)
response = s.api.Call(action, ctx.Get("params")) response = s.api.Call(action, ctx.Get("params"))
@ -162,9 +225,16 @@ func checkAuth(req *http.Request, token string) int {
} }
} }
func puint64Operator(p *uint64, def uint64) uint64 {
if p == nil {
return def
}
return *p
}
// runHTTP 启动HTTP服务器与HTTP上报客户端 // runHTTP 启动HTTP服务器与HTTP上报客户端
func runHTTP(bot *coolq.CQBot, node yaml.Node) { func runHTTP(bot *coolq.CQBot, node yaml.Node) {
var conf config.HTTPServer var conf HTTPServer
switch err := node.Decode(&conf); { switch err := node.Decode(&conf); {
case err != nil: case err != nil:
log.Warn("读取http配置失败 :", err) log.Warn("读取http配置失败 :", err)
@ -205,12 +275,14 @@ client:
for _, c := range conf.Post { for _, c := range conf.Post {
if c.URL != "" { if c.URL != "" {
go HTTPClient{ go HTTPClient{
bot: bot, bot: bot,
secret: c.Secret, secret: c.Secret,
addr: c.URL, addr: c.URL,
apiPort: conf.Port, apiPort: conf.Port,
filter: conf.Filter, filter: conf.Filter,
timeout: conf.Timeout, timeout: conf.Timeout,
MaxRetries: puint64Operator(c.MaxRetries, 3),
RetriesInterval: puint64Operator(c.RetriesInterval, 1500),
}.Run() }.Run()
} }
} }
@ -236,42 +308,47 @@ func (c *HTTPClient) onBotPushEvent(e *coolq.Event) {
} }
client := http.Client{Timeout: time.Second * time.Duration(c.timeout)} client := http.Client{Timeout: time.Second * time.Duration(c.timeout)}
req, _ := http.NewRequest("POST", c.addr, bytes.NewReader(e.JSONBytes())) header := make(http.Header)
req.Header.Set("X-Self-ID", strconv.FormatInt(c.bot.Client.Uin, 10)) header.Set("X-Self-ID", strconv.FormatInt(c.bot.Client.Uin, 10))
req.Header.Set("User-Agent", "CQHttp/4.15.0") header.Set("User-Agent", "CQHttp/4.15.0")
header.Set("Content-Type", "application/json")
if c.secret != "" { if c.secret != "" {
mac := hmac.New(sha1.New, []byte(c.secret)) mac := hmac.New(sha1.New, []byte(c.secret))
_, _ = mac.Write(e.JSONBytes()) _, _ = mac.Write(e.JSONBytes())
req.Header.Set("X-Signature", "sha1="+hex.EncodeToString(mac.Sum(nil))) header.Set("X-Signature", "sha1="+hex.EncodeToString(mac.Sum(nil)))
} }
if c.apiPort != 0 { if c.apiPort != 0 {
req.Header.Set("X-API-Port", strconv.FormatInt(int64(c.apiPort), 10)) header.Set("X-API-Port", strconv.FormatInt(int64(c.apiPort), 10))
} }
var res *http.Response var res *http.Response
var err error for i := uint64(0); i <= c.MaxRetries; i++ {
const maxAttemptTimes = 5 // see https://stackoverflow.com/questions/31337891/net-http-http-contentlength-222-with-body-length-0
// we should create a new request for every single post trial
req, err := http.NewRequest("POST", c.addr, bytes.NewReader(e.JSONBytes()))
if err != nil {
log.Warnf("上报 Event 数据到 %v 时创建请求失败: %v", c.addr, err)
return
}
req.Header = header
for i := 0; i <= maxAttemptTimes; i++ {
res, err = client.Do(req) res, err = client.Do(req)
if err == nil { if res != nil {
//goland:noinspection GoDeferInLoop //goland:noinspection GoDeferInLoop
defer res.Body.Close() defer res.Body.Close()
}
if err == nil {
break break
} }
if i != maxAttemptTimes { if i < c.MaxRetries {
log.Warnf("上报 Event 数据到 %v 失败: %v 将进行第 %d 次重试", c.addr, err, i+1) log.Warnf("上报 Event 数据到 %v 失败: %v 将进行第 %d 次重试", c.addr, err, i+1)
} else {
log.Warnf("上报 Event 数据 %s 到 %v 失败: %v 停止上报:已达重试上线", e.JSONBytes(), c.addr, err)
return
} }
const maxWait = int64(time.Second * 3) time.Sleep(time.Millisecond * time.Duration(c.RetriesInterval))
const minWait = int64(time.Millisecond * 500)
wait := rand.Int63n(maxWait-minWait) + minWait
time.Sleep(time.Duration(wait))
} }
if err != nil {
log.Warnf("上报Event数据 %s 到 %v 失败: %v", e.JSONBytes(), c.addr, err)
return
}
log.Debugf("上报Event数据 %s 到 %v", e.JSONBytes(), c.addr) log.Debugf("上报Event数据 %s 到 %v", e.JSONBytes(), c.addr)
r, err := io.ReadAll(res.Body) r, err := io.ReadAll(res.Body)
@ -282,14 +359,3 @@ func (c *HTTPClient) onBotPushEvent(e *coolq.Event) {
c.bot.CQHandleQuickOperation(gjson.Parse(e.JSONString()), gjson.ParseBytes(r)) c.bot.CQHandleQuickOperation(gjson.Parse(e.JSONString()), gjson.ParseBytes(r))
} }
} }
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")
}

42
server/http_test.go Normal file
View File

@ -0,0 +1,42 @@
package server
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
func TestHttpCtx_Get(t *testing.T) {
cases := []struct {
ctx *httpCtx
key string
expected string
}{
{
ctx: &httpCtx{
json: gjson.Result{},
query: url.Values{
"sub_type": []string{"hello"},
"type": []string{"world"},
},
},
key: "[sub_type,type].0",
expected: "hello",
},
{
ctx: &httpCtx{
json: gjson.Result{},
query: url.Values{
"type": []string{"114514"},
},
},
key: "[sub_type,type].0",
expected: "114514",
},
}
for _, c := range cases {
assert.Equal(t, c.expected, c.ctx.Get(c.key).String())
}
}

View File

@ -13,6 +13,17 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
// MiddleWares 通信中间件
type MiddleWares struct {
AccessToken string `yaml:"access-token"`
Filter string `yaml:"filter"`
RateLimit struct {
Enabled bool `yaml:"enabled"`
Frequency float64 `yaml:"frequency"`
Bucket int `yaml:"bucket"`
} `yaml:"rate-limit"`
}
func rateLimit(frequency float64, bucketSize int) api.Handler { func rateLimit(frequency float64, bucketSize int) api.Handler {
limiter := rate.NewLimiter(rate.Limit(frequency), bucketSize) limiter := rate.NewLimiter(rate.Limit(frequency), bucketSize)
return func(_ string, _ api.Getter) global.MSG { return func(_ string, _ api.Getter) global.MSG {

View File

@ -81,7 +81,7 @@ var cli *lambdaClient
// runLambda type: [scf,aws] // runLambda type: [scf,aws]
func runLambda(bot *coolq.CQBot, node yaml.Node) { func runLambda(bot *coolq.CQBot, node yaml.Node) {
var conf config.LambdaServer var conf LambdaServer
switch err := node.Decode(&conf); { switch err := node.Decode(&conf); {
case err != nil: case err != nil:
log.Warn("读取lambda配置失败 :", err) log.Warn("读取lambda配置失败 :", err)
@ -155,6 +155,28 @@ type lambdaInvoke struct {
} `json:"requestContext"` } `json:"requestContext"`
} }
const lambdaDefault = ` # LambdaServer 配置
- lambda:
type: scf # scf: 腾讯云函数 aws: aws Lambda
middlewares:
<<: *default # 引用默认中间件
`
// LambdaServer 云函数配置
type LambdaServer struct {
Disabled bool `yaml:"disabled"`
Type string `yaml:"type"`
MiddleWares `yaml:"middlewares"`
}
func init() {
config.AddServer(&config.Server{
Brief: "云函数服务",
Default: lambdaDefault,
})
}
func (c *lambdaClient) next() *http.Request { func (c *lambdaClient) next() *http.Request {
r, err := http.NewRequest(http.MethodGet, c.nextURL, nil) r, err := http.NewRequest(http.MethodGet, c.nextURL, nil)
if err != nil { if err != nil {

View File

@ -2,7 +2,6 @@ package server
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -13,10 +12,10 @@ import (
"time" "time"
"github.com/Mrs4s/MiraiGo/utils" "github.com/Mrs4s/MiraiGo/utils"
"github.com/RomiChan/websocket"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"nhooyr.io/websocket"
"github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
@ -27,7 +26,7 @@ import (
type webSocketServer struct { type webSocketServer struct {
bot *coolq.CQBot bot *coolq.CQBot
conf *config.WebsocketServer conf *WebsocketServer
mu sync.Mutex mu sync.Mutex
eventConn []*wsConn eventConn []*wsConn
@ -39,25 +38,99 @@ type webSocketServer struct {
// websocketClient WebSocket客户端实例 // websocketClient WebSocket客户端实例
type websocketClient struct { type websocketClient struct {
bot *coolq.CQBot bot *coolq.CQBot
conf *config.WebsocketReverse mu sync.Mutex
mu sync.Mutex
universal *wsConn universal *wsConn
event *wsConn event *wsConn
token string
filter string token string
filter string
reconnectInterval time.Duration
limiter api.Handler
} }
type wsConn struct { type wsConn struct {
*websocket.Conn mu sync.Mutex
conn *websocket.Conn
apiCaller *api.Caller apiCaller *api.Caller
} }
func (c *wsConn) WriteText(b []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
_ = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 15))
return c.conn.WriteMessage(websocket.TextMessage, b)
}
func (c *wsConn) Close() error {
return c.conn.Close()
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
const wsDefault = ` # 正向WS设置
- ws:
# 正向WS服务器监听地址
host: 127.0.0.1
# 正向WS服务器监听端口
port: 6700
middlewares:
<<: *default # 引用默认中间件
`
const wsReverseDefault = ` # 反向WS设置
- ws-reverse:
# 反向WS Universal 地址
# 注意 设置了此项地址后下面两项将会被忽略
universal: ws://your_websocket_universal.server
# 反向WS API 地址
api: ws://your_websocket_api.server
# 反向WS Event 地址
event: ws://your_websocket_event.server
# 重连间隔 单位毫秒
reconnect-interval: 3000
middlewares:
<<: *default # 引用默认中间件
`
// WebsocketServer 正向WS相关配置
type WebsocketServer struct {
Disabled bool `yaml:"disabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
MiddleWares `yaml:"middlewares"`
}
// WebsocketReverse 反向WS相关配置
type WebsocketReverse struct {
Disabled bool `yaml:"disabled"`
Universal string `yaml:"universal"`
API string `yaml:"api"`
Event string `yaml:"event"`
ReconnectInterval int `yaml:"reconnect-interval"`
MiddleWares `yaml:"middlewares"`
}
func init() {
config.AddServer(&config.Server{
Brief: "正向 Websocket 通信",
Default: wsDefault,
})
config.AddServer(&config.Server{
Brief: "反向 Websocket 通信",
Default: wsReverseDefault,
})
}
// runWSServer 运行一个正向WS server // runWSServer 运行一个正向WS server
func runWSServer(b *coolq.CQBot, node yaml.Node) { func runWSServer(b *coolq.CQBot, node yaml.Node) {
var conf config.WebsocketServer var conf WebsocketServer
switch err := node.Decode(&conf); { switch err := node.Decode(&conf); {
case err != nil: case err != nil:
log.Warn("读取正向Websocket配置失败 :", err) log.Warn("读取正向Websocket配置失败 :", err)
@ -81,15 +154,13 @@ func runWSServer(b *coolq.CQBot, node yaml.Node) {
mux.HandleFunc("/event", s.event) mux.HandleFunc("/event", s.event)
mux.HandleFunc("/api", s.api) mux.HandleFunc("/api", s.api)
mux.HandleFunc("/", s.any) mux.HandleFunc("/", s.any)
go func() { log.Infof("CQ WebSocket 服务器已启动: %v", addr)
log.Infof("CQ WebSocket 服务器已启动: %v", addr) log.Fatal(http.ListenAndServe(addr, &mux))
log.Fatal(http.ListenAndServe(addr, &mux))
}()
} }
// runWSClient 运行一个反向向WS client // runWSClient 运行一个反向向WS client
func runWSClient(b *coolq.CQBot, node yaml.Node) { func runWSClient(b *coolq.CQBot, node yaml.Node) {
var conf config.WebsocketReverse var conf WebsocketReverse
switch err := node.Decode(&conf); { switch err := node.Decode(&conf); {
case err != nil: case err != nil:
log.Warn("读取反向Websocket配置失败 :", err) log.Warn("读取反向Websocket配置失败 :", err)
@ -100,26 +171,33 @@ func runWSClient(b *coolq.CQBot, node yaml.Node) {
c := &websocketClient{ c := &websocketClient{
bot: b, bot: b,
conf: &conf,
token: conf.AccessToken, token: conf.AccessToken,
filter: conf.Filter, filter: conf.Filter,
} }
filter.Add(c.filter) filter.Add(c.filter)
if c.conf.Universal != "" { if conf.ReconnectInterval != 0 {
c.connect("Universal", conf.Universal, &c.universal) c.reconnectInterval = time.Duration(conf.ReconnectInterval) * time.Millisecond
} else { }
if c.conf.API != "" { if conf.RateLimit.Enabled {
c.connect("API", conf.API, nil) c.limiter = rateLimit(conf.RateLimit.Frequency, conf.RateLimit.Bucket)
} }
if c.conf.Event != "" {
c.connect("Event", conf.Event, &c.event) if conf.Universal != "" {
} c.connect("Universal", conf.Universal, &c.universal)
c.bot.OnEventPush(c.onBotPushEvent("Universal", conf.Universal, &c.universal))
return // 连接到 Universal 后, 不再连接其他
}
if conf.API != "" {
c.connect("API", conf.API, nil)
}
if conf.Event != "" {
c.connect("Event", conf.Event, &c.event)
c.bot.OnEventPush(c.onBotPushEvent("Event", conf.Event, &c.event))
} }
c.bot.OnEventPush(c.onBotPushEvent)
} }
func (c *websocketClient) connect(typ, url string, conptr **wsConn) { func (c *websocketClient) connect(typ, url string, conptr **wsConn) {
log.Infof("开始尝试连接到反向WebSocket %s服务器: %v", typ, c.conf.API) log.Infof("开始尝试连接到反向WebSocket %s服务器: %v", typ, url)
header := http.Header{ header := http.Header{
"X-Client-Role": []string{typ}, "X-Client-Role": []string{typ},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)}, "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@ -128,11 +206,11 @@ func (c *websocketClient) connect(typ, url string, conptr **wsConn) {
if c.token != "" { if c.token != "" {
header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, _, err := websocket.Dial(context.Background(), url, &websocket.DialOptions{HTTPHeader: header}) // nolint conn, _, err := websocket.DefaultDialer.Dial(url, header) // nolint
if err != nil { if err != nil {
log.Warnf("连接到反向WebSocket %s服务器 %v 时出现错误: %v", typ, c.conf.API, err) log.Warnf("连接到反向WebSocket %s服务器 %v 时出现错误: %v", typ, url, err)
if c.conf.ReconnectInterval != 0 { if c.reconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReconnectInterval)) time.Sleep(c.reconnectInterval)
c.connect(typ, url, conptr) c.connect(typ, url, conptr)
} }
return return
@ -141,46 +219,50 @@ func (c *websocketClient) connect(typ, url string, conptr **wsConn) {
switch typ { switch typ {
case "Event", "Universal": case "Event", "Universal":
handshake := fmt.Sprintf(`{"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, c.bot.Client.Uin, time.Now().Unix()) handshake := fmt.Sprintf(`{"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, c.bot.Client.Uin, time.Now().Unix())
err = conn.Write(context.Background(), websocket.MessageText, []byte(handshake)) err = conn.WriteMessage(websocket.TextMessage, []byte(handshake))
if err != nil { if err != nil {
log.Warnf("反向WebSocket 握手时出现错误: %v", err) log.Warnf("反向WebSocket 握手时出现错误: %v", err)
} }
} }
log.Infof("已连接到反向WebSocket %s服务器 %v", typ, c.conf.API) log.Infof("已连接到反向WebSocket %s服务器 %v", typ, url)
wrappedConn := &wsConn{Conn: conn, apiCaller: api.NewCaller(c.bot)}
if c.conf.RateLimit.Enabled { var wrappedConn *wsConn
wrappedConn.apiCaller.Use(rateLimit(c.conf.RateLimit.Frequency, c.conf.RateLimit.Bucket)) if conptr != nil && *conptr != nil {
wrappedConn = *conptr
} else {
wrappedConn = new(wsConn)
if conptr != nil {
*conptr = wrappedConn
}
} }
if conptr != nil { wrappedConn.conn = conn
*conptr = wrappedConn wrappedConn.apiCaller = api.NewCaller(c.bot)
if c.limiter != nil {
wrappedConn.apiCaller.Use(c.limiter)
} }
switch typ { if typ != "Event" {
case "API": go c.listenAPI(typ, url, wrappedConn)
go c.listenAPI(wrappedConn, false)
case "Universal":
go c.listenAPI(wrappedConn, true)
} }
} }
func (c *websocketClient) listenAPI(conn *wsConn, u bool) { func (c *websocketClient) listenAPI(typ, url string, conn *wsConn) {
defer func() { _ = conn.Close(websocket.StatusNormalClosure, "") }() defer func() { _ = conn.Close() }()
conn.Conn.SetReadLimit(1024 * 1024 * 128)
for { for {
buffer := global.NewBuffer() buffer := global.NewBuffer()
t, reader, err := conn.Conn.Reader(context.Background()) t, reader, err := conn.conn.NextReader()
if err != nil { if err != nil {
log.Warnf("监听反向WS API时出现错误: %v", err) log.Warnf("监听反向WS %s时出现错误: %v", typ, err)
break break
} }
_, err = buffer.ReadFrom(reader) _, err = buffer.ReadFrom(reader)
if err != nil { if err != nil {
log.Warnf("监听反向WS API时出现错误: %v", err) log.Warnf("监听反向WS %s时出现错误: %v", typ, err)
break break
} }
if t == websocket.MessageText { if t == websocket.TextMessage {
go func(buffer *bytes.Buffer) { go func(buffer *bytes.Buffer) {
defer global.PutBuffer(buffer) defer global.PutBuffer(buffer)
conn.handleRequest(c.bot, buffer.Bytes()) conn.handleRequest(c.bot, buffer.Bytes())
@ -189,49 +271,35 @@ func (c *websocketClient) listenAPI(conn *wsConn, u bool) {
global.PutBuffer(buffer) global.PutBuffer(buffer)
} }
} }
if c.conf.ReconnectInterval != 0 { if c.reconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReconnectInterval)) time.Sleep(c.reconnectInterval)
if !u { if typ == "API" { // Universal 不重连,避免多次重连
go c.connect("API", c.conf.API, nil) go c.connect(typ, url, nil)
} }
} }
} }
func (c *websocketClient) onBotPushEvent(e *coolq.Event) { func (c *websocketClient) onBotPushEvent(typ, url string, conn **wsConn) func(e *coolq.Event) {
filter := filter.Find(c.filter) return func(e *coolq.Event) {
if filter != nil && !filter.Eval(gjson.Parse(e.JSONString())) { c.mu.Lock()
log.Debugf("上报Event %s 到 WS服务器 时被过滤.", e.JSONBytes()) defer c.mu.Unlock()
return
} flt := filter.Find(c.filter)
push := func(conn *wsConn, reconnect func()) { if flt != nil && !flt.Eval(gjson.Parse(e.JSONString())) {
log.Debugf("向WS服务器推送Event: %s", e.JSONBytes()) log.Debugf("上报Event %s 到 WS服务器 时被过滤.", e.JSONBytes())
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) return
defer cancel() }
if err := conn.Write(ctx, websocket.MessageText, e.JSONBytes()); err != nil {
log.Warnf("向WS服务器推送 Event 时出现错误: %v", err) log.Debugf("向反向WS %s服务器推送Event: %s", typ, e.JSONBytes())
_ = conn.Close(websocket.StatusNormalClosure, "") if err := (*conn).WriteText(e.JSONBytes()); err != nil {
if c.conf.ReconnectInterval != 0 { log.Warnf("向反向WS %s服务器推送 Event 时出现错误: %v", typ, err)
time.Sleep(time.Millisecond * time.Duration(c.conf.ReconnectInterval)) _ = (*conn).Close()
reconnect() if c.reconnectInterval != 0 {
time.Sleep(c.reconnectInterval)
c.connect(typ, url, conn)
} }
} }
} }
connect := func(typ, url string, conptr **wsConn) func() {
return func() {
c.connect(typ, url, conptr)
}
}
c.mu.Lock()
defer c.mu.Unlock()
if c.event != nil {
push(c.event, connect("Event", c.conf.Event, &c.event))
}
if c.universal != nil {
push(c.universal, connect("Universal", c.conf.Universal, &c.universal))
}
} }
func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) { func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) {
@ -241,23 +309,22 @@ func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status) w.WriteHeader(status)
return return
} }
opts := &websocket.AcceptOptions{InsecureSkipVerify: true}
c, err := websocket.Accept(w, r, opts) c, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Warnf("处理 WebSocket 请求时出现错误: %v", err) log.Warnf("处理 WebSocket 请求时出现错误: %v", err)
return return
} }
err = c.Write(context.Background(), websocket.MessageText, []byte(s.handshake))
err = c.WriteMessage(websocket.TextMessage, []byte(s.handshake))
if err != nil { if err != nil {
log.Warnf("WebSocket 握手时出现错误: %v", err) log.Warnf("WebSocket 握手时出现错误: %v", err)
_ = c.Close(websocket.StatusNormalClosure, "") _ = c.Close()
return return
} }
log.Infof("接受 WebSocket 连接: %v (/event)", r.RemoteAddr) log.Infof("接受 WebSocket 连接: %v (/event)", r.RemoteAddr)
conn := &wsConn{conn: c, apiCaller: api.NewCaller(s.bot)}
conn := &wsConn{Conn: c, apiCaller: api.NewCaller(s.bot)}
s.mu.Lock() s.mu.Lock()
s.eventConn = append(s.eventConn, conn) s.eventConn = append(s.eventConn, conn)
s.mu.Unlock() s.mu.Unlock()
@ -270,18 +337,19 @@ func (s *webSocketServer) api(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status) w.WriteHeader(status)
return return
} }
opts := &websocket.AcceptOptions{InsecureSkipVerify: true}
c, err := websocket.Accept(w, r, opts) c, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Warnf("处理 WebSocket 请求时出现错误: %v", err) log.Warnf("处理 WebSocket 请求时出现错误: %v", err)
return return
} }
log.Infof("接受 WebSocket 连接: %v (/api)", r.RemoteAddr) log.Infof("接受 WebSocket 连接: %v (/api)", r.RemoteAddr)
conn := &wsConn{Conn: c, apiCaller: api.NewCaller(s.bot)} conn := &wsConn{conn: c, apiCaller: api.NewCaller(s.bot)}
if s.conf.RateLimit.Enabled { if s.conf.RateLimit.Enabled {
conn.apiCaller.Use(rateLimit(s.conf.RateLimit.Frequency, s.conf.RateLimit.Bucket)) conn.apiCaller.Use(rateLimit(s.conf.RateLimit.Frequency, s.conf.RateLimit.Bucket))
} }
go s.listenAPI(conn) s.listenAPI(conn)
} }
func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) { func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
@ -291,20 +359,22 @@ func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status) w.WriteHeader(status)
return return
} }
opts := &websocket.AcceptOptions{InsecureSkipVerify: true}
c, err := websocket.Accept(w, r, opts) c, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Warnf("处理 WebSocket 请求时出现错误: %v", err) log.Warnf("处理 WebSocket 请求时出现错误: %v", err)
return return
} }
err = c.Write(context.Background(), websocket.MessageText, []byte(s.handshake))
err = c.WriteMessage(websocket.TextMessage, []byte(s.handshake))
if err != nil { if err != nil {
log.Warnf("WebSocket 握手时出现错误: %v", err) log.Warnf("WebSocket 握手时出现错误: %v", err)
_ = c.Close(websocket.StatusNormalClosure, "") _ = c.Close()
return return
} }
log.Infof("接受 WebSocket 连接: %v (/)", r.RemoteAddr) log.Infof("接受 WebSocket 连接: %v (/)", r.RemoteAddr)
conn := &wsConn{Conn: c, apiCaller: api.NewCaller(s.bot)} conn := &wsConn{conn: c, apiCaller: api.NewCaller(s.bot)}
if s.conf.RateLimit.Enabled { if s.conf.RateLimit.Enabled {
conn.apiCaller.Use(rateLimit(s.conf.RateLimit.Frequency, s.conf.RateLimit.Bucket)) conn.apiCaller.Use(rateLimit(s.conf.RateLimit.Frequency, s.conf.RateLimit.Bucket))
} }
@ -315,11 +385,10 @@ func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
} }
func (s *webSocketServer) listenAPI(c *wsConn) { func (s *webSocketServer) listenAPI(c *wsConn) {
defer func() { _ = c.Close(websocket.StatusNormalClosure, "") }() defer func() { _ = c.Close() }()
c.Conn.SetReadLimit(1024 * 1024 * 128)
for { for {
buffer := global.NewBuffer() buffer := global.NewBuffer()
t, reader, err := c.Reader(context.Background()) t, reader, err := c.conn.NextReader()
if err != nil { if err != nil {
break break
} }
@ -328,7 +397,7 @@ func (s *webSocketServer) listenAPI(c *wsConn) {
break break
} }
if t == websocket.MessageText { if t == websocket.TextMessage {
go func(buffer *bytes.Buffer) { go func(buffer *bytes.Buffer) {
defer global.PutBuffer(buffer) defer global.PutBuffer(buffer)
c.handleRequest(s.bot, buffer.Bytes()) c.handleRequest(s.bot, buffer.Bytes())
@ -343,7 +412,7 @@ func (c *wsConn) handleRequest(_ *coolq.CQBot, payload []byte) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Printf("处置WS命令时发生无法恢复的异常%v\n%s", err, debug.Stack()) log.Printf("处置WS命令时发生无法恢复的异常%v\n%s", err, debug.Stack())
_ = c.Close(websocket.StatusInternalError, fmt.Sprint(err)) _ = c.Close()
} }
}() }()
j := gjson.Parse(utils.B2S(payload)) j := gjson.Parse(utils.B2S(payload))
@ -353,14 +422,18 @@ func (c *wsConn) handleRequest(_ *coolq.CQBot, payload []byte) {
if j.Get("echo").Exists() { if j.Get("echo").Exists() {
ret["echo"] = j.Get("echo").Value() ret["echo"] = j.Get("echo").Value()
} }
writer, _ := c.Writer(context.Background(), websocket.MessageText)
c.mu.Lock()
defer c.mu.Unlock()
_ = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 15))
writer, _ := c.conn.NextWriter(websocket.TextMessage)
_ = json.NewEncoder(writer).Encode(ret) _ = json.NewEncoder(writer).Encode(ret)
_ = writer.Close() _ = writer.Close()
} }
func (s *webSocketServer) onBotPushEvent(e *coolq.Event) { func (s *webSocketServer) onBotPushEvent(e *coolq.Event) {
filter := filter.Find(s.filter) flt := filter.Find(s.filter)
if filter != nil && !filter.Eval(gjson.Parse(e.JSONString())) { if flt != nil && !flt.Eval(gjson.Parse(e.JSONString())) {
log.Debugf("上报Event %s 到 WS客户端 时被过滤.", e.JSONBytes()) log.Debugf("上报Event %s 到 WS客户端 时被过滤.", e.JSONBytes())
return return
} }
@ -372,8 +445,8 @@ func (s *webSocketServer) onBotPushEvent(e *coolq.Event) {
for i := 0; i < len(s.eventConn); i++ { for i := 0; i < len(s.eventConn); i++ {
conn := s.eventConn[i] conn := s.eventConn[i]
log.Debugf("向WS客户端推送Event: %s", e.JSONBytes()) log.Debugf("向WS客户端推送Event: %s", e.JSONBytes())
if err := conn.Write(context.Background(), websocket.MessageText, e.JSONBytes()); err != nil { if err := conn.WriteText(e.JSONBytes()); err != nil {
_ = conn.Close(websocket.StatusNormalClosure, "") _ = conn.Close()
conn = nil conn = nil
continue continue
} }