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

Compare commits

..

1 Commits

Author SHA1 Message Date
0c7c790386 use goreleaser 2021-03-26 17:33:22 +08:00
131 changed files with 5698 additions and 13766 deletions

44
.github/ISSUE_TEMPLATE/bug--.md vendored Normal file
View File

@ -0,0 +1,44 @@
---
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版本:
运行环境:
连接方式:
使用协议:
**bug内容**
<!-- 请在这里详细描述bug的内容 -->
**复现方法**
<!-- 请在这里分步骤的描述如何复现这个bug -->

View File

@ -1,157 +0,0 @@
name: 回报错误
description: 在使用 go-cqhttp 的过程中遇到了错误
title: '[Bug]: '
labels: [ "bug?" ]
body:
# User's README and agreement
- type: markdown
attributes:
value: |
## 感谢您愿意填写错误回报!
## 以下是一些注意事项,请务必阅读让我们能够更容易处理
### ❗ | 确定没有相同问题的ISSUE已被提出. (教程: https://forums.go-cqhttp.org/t/topic/141)
### 🌎| 请准确填写环境信息
### ❔ | 打开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://forums.go-cqhttp.org/t/topic/141)"
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

@ -1,79 +0,0 @@
name: Build And Push Docker Image
on:
push:
branches:
- 'master'
- 'dev'
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v3
- name: Set time zone
uses: szenius/set-timezone@v1.1
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
# # 如果有 dockerhub 账户可以在github的secrets中配置下面两个然后取消下面注释的这几行并在meta步骤的images增加一行 ${{ github.repository }}
# - name: Login to DockerHub
# uses: docker/login-action@v1
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,12 +1,12 @@
name: CI
on: [push, pull_request,workflow_dispatch]
on: [push, pull_request]
env:
BINARY_PREFIX: "go-cqhttp_"
BINARY_SUFFIX: ""
COMMIT_ID: "${{ github.sha }}"
PR_PROMPT: "::warning:: Build artifact will not be uploaded due to the workflow is trigged by pull request."
LD_FLAGS: "-w -s"
jobs:
build:
@ -22,14 +22,15 @@ jobs:
goarch: arm
- goos: darwin
goarch: "386"
- goos: windows
goarch: arm64
fail-fast: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Setup Go environment
uses: actions/setup-go@v3
uses: actions/setup-go@v2.1.3
with:
cache: true
go-version: '1.20'
go-version: 1.16
- name: Build binary file
env:
GOOS: ${{ matrix.goos }}
@ -38,12 +39,11 @@ jobs:
run: |
if [ $GOOS = "windows" ]; then export BINARY_SUFFIX="$BINARY_SUFFIX.exe"; 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 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" .
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
if: ${{ !github.head_ref }}
with:
name: ${{ matrix.goos }}_${{ matrix.goarch }}

View File

@ -1,41 +1,24 @@
name: Lint
on: [push,pull_request,workflow_dispatch]
on: [push]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go environment
uses: actions/setup-go@v3
with:
go-version: '1.20'
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v2
with:
version: latest
- name: Tests
run: |
go test $(go list ./...)
- name: Commit back
if: ${{ github.repository_owner == 'Mrs4s' && !github.event.pull_request }}
continue-on-error: true
run: |
git config --local user.name 'github-actions[bot]'
git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com'
git config --local user.name 'Github Actions'
git config --local user.email 'github-actions@users.noreply.github.com'
git add --all
git commit -m "ci(chore): Fix stylings"
git push
- name: Suggester
if: ${{ github.event.pull_request }}
uses: reviewdog/action-suggester@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tool_name: golangci-lint

12
.github/workflows/issuebot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
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

@ -1,4 +1,4 @@
name: release
name: build
on:
push:
@ -9,33 +9,22 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git version
git clone https://github.com/Mrs4s/go-cqhttp.git /home/runner/work/go-cqhttp/go-cqhttp
- name: Set up Go
uses: actions/setup-go@v3
-
name: Checkout
uses: actions/checkout@v2.3.4
with:
go-version: '1.20'
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.16.2'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --clean
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#- name: Checkout Dist
# uses: actions/checkout@v2
# with:
# repository: 'gocq/dist'
# ref: master
# ssh-key: ${{ secrets.SSH_KEY }}
# path: upstream/dist
#- name: Update Dist
# run: |
# chmod +x scripts/upload_dist.sh
# ./scripts/upload_dist.sh

20
.github/workflows/suggester.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Lint
on: [pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
- name: Suggester
uses: reviewdog/action-suggester@v1
with:
tool_name: golangci-lint

16
.gitignore vendored
View File

@ -1,21 +1,7 @@
vendor/
.idea
.vscode
config.hjson
config.yml
session.token
device.json
codec/
data/
logs/
internal/btree/*.lock
internal/btree/*.db
# binary builds
go-cqhttp
*.exe
# macos
.DS_Store
# windwos rc
*.syso

View File

@ -22,24 +22,35 @@ linters:
fast: false
enable:
- bodyclose
- durationcheck
- gofmt
- goimports
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- exportloopref
- exhaustive
- bidichk
#- funlen
#- goconst
- gocritic
#- gocyclo
- gofmt
- goimports
- goprintffuncname
#- gosec
- gosimple
- govet
- ineffassign
#- nolintlint
- misspell
- nolintlint
- rowserrcheck
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- usestdlibvars
- unparam
- unused
- varcheck
- whitespace
- prealloc
- predeclared
@ -47,21 +58,31 @@ linters:
- revive
- forbidigo
- makezero
#- interfacer
# don't enable:
# - scopelint
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - interfacer
# - maligned
# - nestif
# - testpackage
# - wsl
run:
# default concurrency is a available CPU number.
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
deadline: 5m
issues-exit-code: 1
skip-dirs:
- db
- cmd/api-generator
- internal/encryption
tests: true
tests: false
# output configuration options
output:
format: "colored-line-number"
format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
uniq-by-line: true
@ -69,6 +90,3 @@ output:
issues:
# Fix found issues (if it's supported by the linter)
fix: true
exclude-use-default: false
exclude:
- "Error return value of .((os.)?std(out|err)..*|.*Close|.*Seek|.*Flush|os.Remove(All)?|.*print(f|ln)?|os.(Un)?Setenv). is not check"

View File

@ -3,64 +3,40 @@ env:
before:
hooks:
- go mod tidy
- go install github.com/tc-hib/go-winres@latest
- go generate winres/init.go
- go-winres make
release:
draft: true
discussion_category_name: General
builds:
- id: nowin
env:
- env:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- linux
- darwin
- windows
goarch:
- '386'
- 386
- amd64
- arm
- arm64
goarm:
- '7'
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: '386'
mod_timestamp: "{{ .CommitTimestamp }}"
goarch: 386
- goos: windows
goarch: arm64
mod_timestamp: '{{ .CommitTimestamp }}'
flags:
- -trimpath
ldflags:
- -s -w -X github.com/Mrs4s/go-cqhttp/internal/base.Version=v{{.Version}}
- id: win
env:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- windows
goarch:
- '386'
- amd64
- arm
- arm64
goarm:
- '7'
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags:
- -s -w -X github.com/Mrs4s/go-cqhttp/internal/base.Version=v{{.Version}}
- -s -w -X github.com/Mrs4s/go-cqhttp/coolq.Version=v{{.Version}}
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
name_template: '{{ .ProjectName }}_checksums.txt'
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- '^docs:'
- '^test:'
- fix typo
- Merge pull request
- Merge branch
@ -68,27 +44,7 @@ changelog:
- go mod tidy
archives:
- id: binary
builds:
- win
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
format: binary
- id: nowin
builds:
- nowin
- win
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
- name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
format_overrides:
- goos: windows
format: zip
nfpms:
- license: AGPL 3.0
homepage: https://go-cqhttp.org
file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
formats:
- deb
- rpm
maintainer: Mrs4s

View File

@ -1,4 +1,4 @@
FROM golang:1.20-alpine AS builder
FROM golang:1.15.5-alpine AS builder
RUN go env -w GO111MODULE=auto \
&& go env -w CGO_ENABLED=0 \
@ -14,31 +14,9 @@ RUN set -ex \
FROM alpine:latest
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh && \
apk add --no-cache --update \
ffmpeg \
coreutils \
shadow \
su-exec && \
rm -rf /var/cache/apk/* && \
mkdir -p /app && \
mkdir -p /data && \
mkdir -p /config && \
useradd -d /config -s /bin/sh abc && \
chown -R abc /config && \
chown -R abc /data
ENV TZ="Asia/Shanghai"
ENV UID=99
ENV GID=100
ENV UMASK=002
COPY --from=builder /build/cqhttp /app/
COPY --from=builder /build/cqhttp /usr/bin/cqhttp
RUN chmod +x /usr/bin/cqhttp
WORKDIR /data
VOLUME [ "/data" ]
ENTRYPOINT [ "/docker-entrypoint.sh" ]
ENTRYPOINT [ "/usr/bin/cqhttp" ]

182
README.md
View File

@ -1,7 +1,5 @@
<p align="center">
<a href="https://ishkong.github.io/go-cqhttp-docs/">
<img src="winres/icon.png" width="200" height="200" alt="go-cqhttp">
</a>
<a href="https://ishkong.github.io/go-cqhttp-docs/"><img src="https://i.loli.net/2020/12/20/qSLMDWxiocRQgu6.jpg" width="200" height="200" alt="go-cqhttp"></a>
</p>
<div align="center">
@ -44,14 +42,14 @@ _✨ 基于 [Mirai](https://github.com/mamoe/mirai) 以及 [MiraiGo](https://git
## 兼容性
go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大多数内容,并在其基础上做了一些扩展,详情请看 go-cqhttp 的文档
go-cqhttp兼容[OneBot-v11](https://github.com/howmanybots/onebot/tree/master/v11/specs)绝大多数内容并在其基础上做了一些扩展详情请看go-cqhttp的文档
### 接口
- [x] HTTP API
- [x] 反向 HTTP POST
- [x] 正向 WebSocket
- [x] 反向 WebSocket
- [x] 正向 Websocket
- [x] 反向 Websocket
### 拓展支持
@ -68,9 +66,9 @@ go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大
### 实现
<details>
<summary>已实现 CQ </summary>
<summary>已实现CQ码</summary>
#### 符合 OneBot 标准的 CQ 码
#### 符合 Onebot 标准的 CQ 码
| CQ 码 | 功能 |
| ------------ | --------------------------- |
@ -86,43 +84,45 @@ go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大
| [CQ:xml] | [XML 消息] |
| [CQ:json] | [JSON 消息] |
[qq 表情]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#qq-%E8%A1%A8%E6%83%85
[语音]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E8%AF%AD%E9%9F%B3
[短视频]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E7%9F%AD%E8%A7%86%E9%A2%91
[@某人]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E6%9F%90%E4%BA%BA
[链接分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%93%BE%E6%8E%A5%E5%88%86%E4%BA%AB
[音乐分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%9F%B3%E4%B9%90%E5%88%86%E4%BA%AB-
[音乐自定义分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%9F%B3%E4%B9%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%86%E4%BA%AB-
[回复]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%9B%9E%E5%A4%8D
[合并转发]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-
[合并转发节点]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E8%8A%82%E7%82%B9-
[xml 消息]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#xml-%E6%B6%88%E6%81%AF
[json 消息]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#json-%E6%B6%88%E6%81%AF
[qq 表情]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#qq-%E8%A1%A8%E6%83%85
[语音]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E8%AF%AD%E9%9F%B3
[短视频]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E7%9F%AD%E8%A7%86%E9%A2%91
[@某人]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E6%9F%90%E4%BA%BA
[链接分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%93%BE%E6%8E%A5%E5%88%86%E4%BA%AB
[音乐分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%9F%B3%E4%B9%90%E5%88%86%E4%BA%AB-
[音乐自定义分享]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E9%9F%B3%E4%B9%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%86%E4%BA%AB-
[回复]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%9B%9E%E5%A4%8D
[合并转发]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-
[合并转发节点]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E8%8A%82%E7%82%B9-
[xml 消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#xml-%E6%B6%88%E6%81%AF
[json 消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#json-%E6%B6%88%E6%81%AF
#### 拓展 CQ 码及与 OneBot 标准有略微差异的 CQ 码
#### 拓展 CQ 码及与 Onebot 标准有略微差异的 CQ 码
| 拓展 CQ 码 | 功能 |
| -------------- | --------------------------------- |
| [CQ:image] | [图片] |
| [CQ:redbag] | [红包] |
| [CQ:poke] | [戳一戳] |
| [CQ:gift] | [礼物] |
| [CQ:node] | [合并转发消息节点] |
| [CQ:cardimage] | [一种 xml 的图片消息(装逼大图)] |
| [CQ:tts] | [文本转语音] |
[图片]: https://docs.go-cqhttp.org/cqcode/#%E5%9B%BE%E7%89%87
[红包]: https://docs.go-cqhttp.org/cqcode/#%E7%BA%A2%E5%8C%85
[戳一戳]: https://docs.go-cqhttp.org/cqcode/#%E6%88%B3%E4%B8%80%E6%88%B3
[合并转发消息节点]: https://docs.go-cqhttp.org/cqcode/#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9
[一种 xml 的图片消息(装逼大图)]: https://docs.go-cqhttp.org/cqcode/#cardimage
[文本转语音]: https://docs.go-cqhttp.org/cqcode/#%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3
[图片]: docs/cqhttp.md#%E5%9B%BE%E7%89%87
[红包]: docs/cqhttp.md#%E7%BA%A2%E5%8C%85
[戳一戳]: docs/cqhttp.md#%E6%88%B3%E4%B8%80%E6%88%B3
[礼物]: docs/cqhttp.md#%E7%A4%BC%E7%89%A9
[合并转发消息节点]: docs/cqhttp.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9
[一种 xml 的图片消息(装逼大图)]: docs/cqhttp.md#cardimage-%E4%B8%80%E7%A7%8Dxml%E7%9A%84%E5%9B%BE%E7%89%87%E6%B6%88%E6%81%AF%E8%A3%85%E9%80%BC%E5%A4%A7%E5%9B%BE
[文本转语音]: docs/cqhttp.md#%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3
</details>
<details>
<summary>已实现 API</summary>
<summary>已实现API</summary>
#### 符合 OneBot 标准的 API
#### 符合 Onebot 标准的 API
| API | 功能 |
| ------------------------ | ---------------------- |
@ -154,35 +154,35 @@ go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大
| /set_restart | [重启 go-cqhttp] |
| /.handle_quick_operation | [对事件执行快速操作] |
[发送私聊消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[发送群消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
[发送消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
[撤回信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
[群组踢人]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
[群组单人禁言]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
[群组全员禁言]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
[群组设置管理员]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
[设置群名片(群备注)]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
[设置群名]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
[退出群组]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
[设置群组专属头衔]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
[处理加好友请求]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[处理加群请求/邀请]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
[获取登录号信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
[获取陌生人信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
[获取好友列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
[获取群信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
[获取群列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
[获取群成员信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
[获取群成员列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
[获取群荣誉信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
[检查是否可以发送图片]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
[检查是否可以发送语音]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
[获取版本信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
[重启 go-cqhttp]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
[对事件执行快速操作]: https://github.com/botuniverse/onebot-11/blob/master/api/hidden.md#handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
[发送私聊消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[发送群消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
[发送消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
[撤回信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
[群组踢人]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
[群组单人禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
[群组全员禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
[群组设置管理员]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
[设置群名片(群备注)]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
[设置群名]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
[退出群组]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
[设置群组专属头衔]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
[处理加好友请求]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[处理加群请求/邀请]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
[获取登录号信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
[获取陌生人信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
[获取好友列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
[获取群信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
[获取群列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
[获取群成员信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
[获取群成员列表]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
[获取群荣誉信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
[检查是否可以发送图片]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
[检查是否可以发送语音]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
[获取版本信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
[重启 go-cqhttp]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
[对事件执行快速操作]: https://github.com/howmanybots/onebot/blob/master/v11/specs/api/hidden.md#handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
#### 拓展 API 及与 OneBot 标准有略微差异的 API
#### 拓展 API 及与 Onebot 标准有略微差异的 API
| 拓展 API | 功能 |
| --------------------------- | ---------------------- |
@ -200,26 +200,26 @@ go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大
| /get_group_file_url | [获取群文件资源链接] |
| /get_status | [获取状态] |
[设置群头像]: https://docs.go-cqhttp.org/api/#%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%A4%B4%E5%83%8F
[获取图片信息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87%E4%BF%A1%E6%81%AF
[获取消息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
[获取合并转发内容]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E5%86%85%E5%AE%B9
[发送合并转发(群)]: 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
[获取中文分词]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D-%E9%9A%90%E8%97%8F-api
[图片 ocr]: https://docs.go-cqhttp.org/api/#%E5%9B%BE%E7%89%87-ocr
[获取群系统消息]: 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%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF
[获取群根目录文件列表]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%A0%B9%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群子目录文件列表]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%AD%90%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群文件资源链接]: 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%8A%B6%E6%80%81
[设置群头像]: docs/cqhttp.md#%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%A4%B4%E5%83%8F
[获取图片信息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87%E4%BF%A1%E6%81%AF
[获取消息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
[获取合并转发内容]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E5%86%85%E5%AE%B9
[发送合并转发(群)]: docs/cqhttp.md#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E7%BE%A4
[获取中文分词]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D
[图片 ocr]: docs/cqhttp.md#%E5%9B%BE%E7%89%87ocr
[获取群系统消息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF
[获取群文件系统信息]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF
[获取群根目录文件列表]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%A0%B9%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群子目录文件列表]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%AD%90%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群文件资源链接]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E8%B5%84%E6%BA%90%E9%93%BE%E6%8E%A5
[获取状态]: docs/cqhttp.md#%E8%8E%B7%E5%8F%96%E7%8A%B6%E6%80%81
</details>
<details>
<summary>已实现 Event</summary>
<summary>已实现Event</summary>
#### 符合 OneBot 标准的 Event部分 Event 比 OneBot 标准多上报几个字段,不影响使用)
#### 符合 Onebot 标准的 Event部分 Event 比 Onebot 标准多上报几个字段,不影响使用)
| 事件类型 | Event |
| -------- | ---------------- |
@ -239,35 +239,33 @@ go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大
| 请求事件 | [加好友请求] |
| 请求事件 | [加群请求/邀请] |
[私聊信息]: https://github.com/botuniverse/onebot-11/blob/master/event/message.md#%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[群消息]: https://github.com/botuniverse/onebot-11/blob/master/event/message.md#%E7%BE%A4%E6%B6%88%E6%81%AF
[群文件上传]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0
[群管理员变动]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%AE%A1%E7%90%86%E5%91%98%E5%8F%98%E5%8A%A8
[群成员减少]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%87%8F%E5%B0%91
[群成员增加]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%A2%9E%E5%8A%A0
[群禁言]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%A6%81%E8%A8%80
[好友添加]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B7%BB%E5%8A%A0
[群消息撤回]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[好友消息撤回]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[群内戳一戳]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
[群红包运气王]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%BA%A2%E5%8C%85%E8%BF%90%E6%B0%94%E7%8E%8B
[群成员荣誉变更]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E8%8D%A3%E8%AA%89%E5%8F%98%E6%9B%B4
[加好友请求]: https://github.com/botuniverse/onebot-11/blob/master/event/request.md#%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[加群请求/邀请]: https://github.com/botuniverse/onebot-11/blob/master/event/request.md#%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
[私聊信息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[群消息]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#%E7%BE%A4%E6%B6%88%E6%81%AF
[群文件上传]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0
[群管理员变动]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%AE%A1%E7%90%86%E5%91%98%E5%8F%98%E5%8A%A8
[群成员减少]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%87%8F%E5%B0%91
[群成员增加]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%A2%9E%E5%8A%A0
[群禁言]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%A6%81%E8%A8%80
[好友添加]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B7%BB%E5%8A%A0
[群消息撤回]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[好友消息撤回]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[群内戳一戳]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
[群红包运气王]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E7%BA%A2%E5%8C%85%E8%BF%90%E6%B0%94%E7%8E%8B
[群成员荣誉变更]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E8%8D%A3%E8%AA%89%E5%8F%98%E6%9B%B4
[加好友请求]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md#%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[加群请求/邀请]: https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md#%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
#### 拓展 Event
| 事件类型 | 拓展 Event |
| -------- | ---------------- |
| 通知事件 | [好友戳一戳] |
| 通知事件 | [群内戳一戳] |
| 通知事件 | [群成员名片更新] |
| 通知事件 | [接收到离线文件] |
[好友戳一戳]: https://docs.go-cqhttp.org/event/#%E5%A5%BD%E5%8F%8B%E6%88%B3%E4%B8%80%E6%88%B3
[群内戳一戳]: https://docs.go-cqhttp.org/event/#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
[群成员名片更新]: https://docs.go-cqhttp.org/event/#%E7%BE%A4%E6%88%90%E5%91%98%E5%90%8D%E7%89%87%E6%9B%B4%E6%96%B0
[接收到离线文件]: https://docs.go-cqhttp.org/event/#%E6%8E%A5%E6%94%B6%E5%88%B0%E7%A6%BB%E7%BA%BF%E6%96%87%E4%BB%B6
[好友戳一戳]: docs/cqhttp.md#%E5%A5%BD%E5%8F%8B%E6%88%B3%E4%B8%80%E6%88%B3
[群成员名片更新]: docs/cqhttp.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%90%8D%E7%89%87%E6%9B%B4%E6%96%B0
[接收到离线文件]: docs/cqhttp.md#%E6%8E%A5%E6%94%B6%E5%88%B0%E7%A6%BB%E7%BA%BF%E6%96%87%E4%BB%B6
</details>

View File

@ -1,355 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"io"
"os"
"reflect"
"sort"
"strconv"
"strings"
)
var supported = flag.Bool("supported", false, "genRouter supported.go")
var output = flag.String("o", "", "output file")
var pkg = flag.String("pkg", "", "package name")
var src = flag.String("path", "", "source file")
type Param struct {
Name string
Type string
Default string
}
type Router struct {
Func string
Path []string
PathV11 []string // v11 only
PathV12 []string // v12 only
Params []Param
}
type generator struct {
out io.Writer
}
const (
PathAll = 0
PathV11 = 11
PathV12 = 12
)
func (g *generator) WriteString(s string) {
io.WriteString(g.out, s)
}
func (g *generator) writef(format string, a ...any) {
fmt.Fprintf(g.out, format, a...)
}
func (g *generator) header() {
g.WriteString("// Code generated by cmd/api-generator. DO NOT EDIT.\n\n")
g.writef("package %s\n\n", *pkg)
}
func (g *generator) genRouter(routers []Router) {
g.WriteString("import (\n\n")
g.WriteString("\"github.com/Mrs4s/go-cqhttp/coolq\"\n")
g.WriteString("\"github.com/Mrs4s/go-cqhttp/global\"\n")
g.WriteString("\"github.com/Mrs4s/go-cqhttp/pkg/onebot\"\n")
g.WriteString(")\n\n")
g.WriteString(`func (c *Caller) call(action string, spec *onebot.Spec, p Getter) global.MSG {`)
genVer := func(path int) {
g.writef(`if spec.Version == %d {
switch action {
`, path)
for _, router := range routers {
g.router(router, path)
}
g.WriteString("}}\n")
}
genVer(PathV11)
genVer(PathV12)
// generic path
g.WriteString("switch action {\n")
for _, router := range routers {
g.router(router, PathAll)
}
g.WriteString("}\n")
g.WriteString("return coolq.Failed(404, \"API_NOT_FOUND\", \"API不存在\")}")
}
func (g *generator) router(router Router, pathVersion int) {
path := router.Path
if pathVersion == PathV11 {
path = router.PathV11
}
if pathVersion == PathV12 {
path = router.PathV12
}
if len(path) == 0 {
return
}
g.WriteString(`case `)
for i, p := range path {
if i != 0 {
g.WriteString(`, `)
}
g.WriteString(strconv.Quote(p))
}
g.WriteString(":\n")
for i, p := range router.Params {
if p.Type == "*onebot.Spec" {
continue
}
if p.Default == "" {
v := "p.Get(" + strconv.Quote(p.Name) + ")"
g.writef("p%d := %s\n", i, conv(v, p.Type))
} else {
g.writef("p%d := %s\n", i, p.Default)
g.writef("if pt := p.Get(%s); pt.Exists() {\n", strconv.Quote(p.Name))
g.writef("p%d = %s\n}\n", i, conv("pt", p.Type))
}
}
g.WriteString("\t\treturn c.bot." + router.Func + "(")
for i, p := range router.Params {
if i != 0 {
g.WriteString(", ")
}
if p.Type == "*onebot.Spec" {
g.WriteString("spec")
continue
}
g.writef("p%d", i)
}
g.WriteString(")\n")
}
func conv(v, t string) string {
switch t {
default:
panic("unsupported type: " + t)
case "gjson.Result", "*onebot.Spec":
return v
case "int64":
return v + ".Int()"
case "bool":
return v + ".Bool()"
case "string":
return v + ".String()"
case "int32", "int":
return t + "(" + v + ".Int())"
case "uint64":
return v + ".Uint()"
case "uint32":
return "uint32(" + v + ".Uint())"
case "uint16":
return "uint16(" + v + ".Uint())"
}
}
func main() {
var routers []Router
flag.Parse()
fset := token.NewFileSet()
for _, s := range strings.Split(*src, ",") {
file, err := parser.ParseFile(fset, s, nil, parser.ParseComments)
if err != nil {
panic(err)
}
for _, decl := range file.Decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
if !decl.Name.IsExported() || decl.Recv == nil ||
typeName(decl.Recv.List[0].Type) != "*CQBot" {
continue
}
router := Router{Func: decl.Name.Name}
// compute params
for _, p := range decl.Type.Params.List {
typ := typeName(p.Type)
for _, name := range p.Names {
router.Params = append(router.Params, Param{Name: snakecase(name.Name), Type: typ})
}
}
for _, comment := range decl.Doc.List {
annotation, args := match(comment.Text)
switch annotation {
case "route":
for _, route := range strings.Split(args, ",") {
router.Path = append(router.Path, unquote(route))
}
case "route11":
for _, route := range strings.Split(args, ",") {
router.PathV11 = append(router.PathV11, unquote(route))
}
case "route12":
for _, route := range strings.Split(args, ",") {
router.PathV12 = append(router.PathV12, unquote(route))
}
case "default":
for name, value := range parseMap(args, "=") {
for i, p := range router.Params {
if p.Name == name {
router.Params[i].Default = convDefault(value, p.Type)
}
}
}
case "rename":
for name, value := range parseMap(args, "->") {
for i, p := range router.Params {
if p.Name == name {
router.Params[i].Name = value
}
}
}
}
sort.Slice(router.Path, func(i, j int) bool {
return router.Path[i] < router.Path[j]
})
sort.Slice(router.PathV11, func(i, j int) bool {
return router.PathV11[i] < router.PathV11[j]
})
sort.Slice(router.PathV12, func(i, j int) bool {
return router.PathV12[i] < router.PathV12[j]
})
}
if router.Path != nil || router.PathV11 != nil || router.PathV12 != nil {
routers = append(routers, router)
} else {
println(decl.Name.Name)
}
}
}
}
sort.Slice(routers, func(i, j int) bool {
path := func(r Router) string {
if r.Path != nil {
return r.Path[0]
}
if r.PathV11 != nil {
return r.PathV11[0]
}
if r.PathV12 != nil {
return r.PathV12[0]
}
return ""
}
return path(routers[i]) < path(routers[j])
})
out := new(bytes.Buffer)
g := &generator{out: out}
g.header()
if *supported {
g.genSupported(routers)
} else {
g.genRouter(routers)
}
source, err := format.Source(out.Bytes())
if err != nil {
panic(err)
}
err = os.WriteFile(*output, source, 0o644)
if err != nil {
panic(err)
}
}
func unquote(s string) string {
switch s[0] {
case '"':
s, _ = strconv.Unquote(s)
case '`':
s = strings.Trim(s, "`")
}
return s
}
func parseMap(input string, sep string) map[string]string {
out := make(map[string]string)
for _, arg := range strings.Split(input, ",") {
k, v, ok := strings.Cut(arg, sep)
if !ok {
out[k] = "true"
}
k = strings.TrimSpace(k)
v = unquote(strings.TrimSpace(v))
out[k] = v
}
return out
}
func match(text string) (string, string) {
text = strings.TrimPrefix(text, "//")
text = strings.TrimSpace(text)
if !strings.HasPrefix(text, "@") || !strings.HasSuffix(text, ")") {
return "", ""
}
text = strings.Trim(text, "@)")
cmd, args, ok := strings.Cut(text, "(")
if !ok {
return "", ""
}
return cmd, unquote(args)
}
// some abbreviations need translation before transforming ro snake case
var replacer = strings.NewReplacer("ID", "Id")
func snakecase(s string) string {
s = replacer.Replace(s)
t := make([]byte, 0, 32)
for i := 0; i < len(s); i++ {
c := s[i]
if ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
t = append(t, c)
} else {
t = append(t, '_')
t = append(t, c^0x20)
}
}
return string(t)
}
func convDefault(s string, t string) string {
switch t {
case "bool":
if s == "true" {
return s
}
case "uint32":
if s != "0" {
return t + "(" + s + ")"
}
default:
panic("unhandled default value type:" + t)
}
return ""
}
func typeName(x ast.Node) string {
switch x := x.(type) {
case *ast.Ident:
return x.Name
case *ast.SelectorExpr:
return typeName(x.X) + "." + x.Sel.Name
case *ast.StarExpr:
return "*" + typeName(x.X)
default:
panic("unhandled type: " + reflect.TypeOf(x).String())
}
}

View File

@ -1,44 +0,0 @@
package main
import "html/template"
func (g *generator) genSupported(routers []Router) {
var v11, v12 []string // for onebot v12 get_supported_actions
for _, router := range routers {
if len(router.PathV11) > 0 {
v11 = append(v11, router.PathV11...)
}
if len(router.PathV11) > 0 {
v12 = append(v12, router.PathV12...)
}
if len(router.Path) > 0 {
v11 = append(v11, router.Path...)
v12 = append(v12, router.Path...)
}
}
type S struct {
V11 []string
V12 []string
}
tmpl, err := template.New("").Parse(supportedTemplete)
if err != nil {
panic(err)
}
err = tmpl.Execute(g.out, &S{V11: v11, V12: v12})
if err != nil {
panic(err)
}
}
const supportedTemplete = `
var supportedV11 = []string{
{{range .V11}} "{{.}}",
{{end}}
}
var supportedV12 = []string{
{{range .V12}} "{{.}}",
{{end}}
}`

View File

@ -1,300 +0,0 @@
package gocq
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"image"
"image/png"
"net/http"
"os"
"strings"
"time"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/Mrs4s/MiraiGo/wrapper"
"github.com/Mrs4s/go-cqhttp/internal/encryption"
_ "github.com/Mrs4s/go-cqhttp/internal/encryption/t544"
"github.com/mattn/go-colorable"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"gopkg.ilharper.com/x/isatty"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/download"
)
var console = bufio.NewReader(os.Stdin)
func init() {
wrapper.DandelionEnergy = energy
}
func readLine() (str string) {
str, _ = console.ReadString('\n')
str = strings.TrimSpace(str)
return
}
func readLineTimeout(t time.Duration) {
r := make(chan string)
go func() {
select {
case r <- readLine():
case <-time.After(t):
}
}()
select {
case <-r:
case <-time.After(t):
}
}
func readIfTTY(de string) (str string) {
if isatty.Isatty(os.Stdin.Fd()) {
return readLine()
}
log.Warnf("未检测到输入终端,自动选择%s.", de)
return de
}
var cli *client.QQClient
var device *client.DeviceInfo
// ErrSMSRequestError SMS请求出错
var ErrSMSRequestError = errors.New("sms request error")
func commonLogin() error {
res, err := cli.Login()
if err != nil {
return err
}
return loginResponseProcessor(res)
}
func printQRCode(imgData []byte) {
const (
black = "\033[48;5;0m \033[0m"
white = "\033[48;5;7m \033[0m"
)
img, err := png.Decode(bytes.NewReader(imgData))
if err != nil {
log.Panic(err)
}
data := img.(*image.Gray).Pix
bound := img.Bounds().Max.X
buf := make([]byte, 0, (bound*4+1)*(bound))
i := 0
for y := 0; y < bound; y++ {
i = y * bound
for x := 0; x < bound; x++ {
if data[i] != 255 {
buf = append(buf, white...)
} else {
buf = append(buf, black...)
}
i++
}
buf = append(buf, '\n')
}
_, _ = colorable.NewColorableStdout().Write(buf)
}
func qrcodeLogin() error {
rsp, err := cli.FetchQRCodeCustomSize(1, 2, 1)
if err != nil {
return err
}
_ = os.WriteFile("qrcode.png", rsp.ImageData, 0o644)
defer func() { _ = os.Remove("qrcode.png") }()
if cli.Uin != 0 {
log.Infof("请使用账号 %v 登录手机QQ扫描二维码 (qrcode.png) : ", cli.Uin)
} else {
log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
}
time.Sleep(time.Second)
printQRCode(rsp.ImageData)
s, err := cli.QueryQRCodeStatus(rsp.Sig)
if err != nil {
return err
}
prevState := s.State
for {
time.Sleep(time.Second)
s, _ = cli.QueryQRCodeStatus(rsp.Sig)
if s == nil {
continue
}
if prevState == s.State {
continue
}
prevState = s.State
switch s.State {
case client.QRCodeCanceled:
log.Fatalf("扫码被用户取消.")
case client.QRCodeTimeout:
log.Fatalf("二维码过期")
case client.QRCodeWaitingForConfirm:
log.Infof("扫码成功, 请在手机端确认登录.")
case client.QRCodeConfirmed:
res, err := cli.QRCodeLogin(s.LoginInfo)
if err != nil {
return err
}
return loginResponseProcessor(res)
case client.QRCodeImageFetch, client.QRCodeWaitingForScan:
// ignore
}
}
}
func loginResponseProcessor(res *client.LoginResponse) error {
var err error
for {
if err != nil {
return err
}
if res.Success {
return nil
}
var text string
switch res.Error {
case client.SliderNeededError:
log.Warnf("登录需要滑条验证码, 请验证后重试.")
ticket := getTicket(res.VerifyUrl)
if ticket == "" {
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
}
res, err = cli.SubmitTicket(ticket)
continue
case client.NeedCaptcha:
log.Warnf("登录需要验证码.")
_ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644)
log.Warnf("请输入验证码 (captcha.jpg) (Enter 提交)")
text = readLine()
global.DelFile("captcha.jpg")
res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
continue
case client.SMSNeededError:
log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
readLine()
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
case client.SMSOrVerifyNeededError:
log.Warnf("账号已开启设备锁,请选择验证方式:")
log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
log.Warnf("2. 使用手机QQ扫码验证.")
log.Warn("请输入(1 - 2)")
text = readIfTTY("2")
if strings.Contains(text, "1") {
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
}
fallthrough
case client.UnsafeDeviceError:
log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyUrl)
log.Infof("按 Enter 或等待 5s 后继续....")
readLineTimeout(time.Second * 5)
os.Exit(0)
case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
msg := res.ErrorMessage
log.Warnf("登录失败: %v Code: %v", msg, res.Code)
if res.Code == 235 {
log.Warnf("请删除 device.json 后重试.")
}
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
}
}
}
func getTicket(u string) string {
log.Warnf("请选择提交滑块ticket方式:")
log.Warnf("1. 自动提交")
log.Warnf("2. 手动抓取提交")
log.Warn("请输入(1 - 2)")
text := readLine()
id := utils.RandomString(8)
auto := !strings.Contains(text, "2")
if auto {
u = strings.ReplaceAll(u, "https://ssl.captcha.qq.com/template/wireless_mqq_captcha.html?", fmt.Sprintf("https://captcha.go-cqhttp.org/captcha?id=%v&", id))
}
log.Warnf("请前往该地址验证 -> %v ", u)
if !auto {
log.Warn("请输入ticket (Enter 提交)")
return readLine()
}
for count := 120; count > 0; count-- {
str := fetchCaptcha(id)
if str != "" {
return str
}
time.Sleep(time.Second)
}
log.Warnf("验证超时")
return ""
}
func fetchCaptcha(id string) string {
g, err := download.Request{URL: "https://captcha.go-cqhttp.org/captcha/ticket?id=" + id}.JSON()
if err != nil {
log.Debugf("获取 Ticket 时出现错误: %v", err)
return ""
}
if g.Get("ticket").Exists() {
return g.Get("ticket").String()
}
return ""
}
func energy(uin uint64, id string, appVersion string, salt []byte) ([]byte, error) {
if localSigner, ok := encryption.T544Signer[appVersion]; ok {
log.Debugf("use local T544Signer v%s", appVersion)
result := localSigner(time.Now().UnixMicro(), salt)
log.Debugf("t544 sign result: %x", result)
return result, nil
}
log.Debugf("fallback to remote T544Signer v%s", appVersion)
signServer := "https://captcha.go-cqhttp.org/sdk/dandelion/energy"
if base.SignServerOverwrite != "" {
signServer = base.SignServerOverwrite
}
response, err := download.Request{
Method: http.MethodPost,
URL: signServer,
Header: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Body: bytes.NewReader([]byte(fmt.Sprintf("uin=%v&id=%s&salt=%s&version=%s", uin, id, hex.EncodeToString(salt), appVersion))),
}.Bytes()
if err != nil {
log.Errorf("获取T544时出现问题: %v", err)
return nil, err
}
sign, err := hex.DecodeString(gjson.GetBytes(response, "result").String())
if err != nil || len(sign) == 0 {
log.Errorf("获取T544时出现问题: %v", err)
return nil, err
}
log.Debugf("t544 sign result: %x", sign)
return sign, nil
}

View File

@ -1,500 +0,0 @@
// 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"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/term"
"github.com/Mrs4s/go-cqhttp/internal/download"
"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,
}
// InitBase 解析参数并检测
//
// 如果在 windows 下双击打开了程序,程序将在此函数释出脚本后终止;
// 如果传入 -h 参数,程序将打印帮助后终止;
// 如果传入 -d 参数,程序将在启动 daemon 后终止。
func InitBase() {
base.Parse()
if !base.FastStart && terminal.RunningByDoubleClick() {
err := terminal.NoMoreDoubleClick()
if err != nil {
log.Errorf("遇到错误: %v", err)
time.Sleep(time.Second * 5)
}
os.Exit(0)
}
switch {
case base.LittleH:
base.Help()
case base.LittleD:
server.Daemon()
}
if base.LittleWD != "" {
err := os.Chdir(base.LittleWD)
if err != nil {
log.Fatalf("重置工作目录时出现错误: %v", err)
}
}
base.Init()
}
// PrepareData 准备 log, 缓存, 数据库, 必须在 InitBase 之后执行
func PrepareData() {
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"), "频道图片缓存")
mkCacheDir(global.VersionsPath, "版本缓存")
cache.Init()
db.Init()
if err := db.Open(); err != nil {
log.Fatalf("打开数据库失败: %v", err)
}
}
// LoginInteract 登录交互, 可能需要键盘输入, 必须在 InitBase, PrepareData 之后执行
func LoginInteract() {
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.Warnf("已开启Debug模式.")
}
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
device = client.GenRandomDevice()
_ = os.WriteFile("device.json", device.ToJson(), 0o644)
log.Info("已生成设备信息并保存到 device.json 文件.")
} else {
log.Info("将使用 device.json 内的设备信息运行Bot.")
device = new(client.DeviceInfo)
if err := device.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", device.Protocol.Version())
cli = newClient()
cli.UseDevice(device)
isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt
isTokenLogin := false
if isQRCodeLogin && cli.Device().Protocol != 2 {
log.Warn("当前协议不支持二维码登录, 请配置账号密码登录.")
os.Exit(0)
}
// 加载本地版本信息, 一般是在上次登录时保存的
versionFile := path.Join(global.VersionsPath, fmt.Sprint(int(cli.Device().Protocol))+".json")
if global.PathExists(versionFile) {
b, err := os.ReadFile(versionFile)
if err == nil {
_ = cli.Device().Protocol.Version().UpdateFromJson(b)
}
log.Infof("从文件 %s 读取协议版本 %v.", versionFile, cli.Device().Protocol.Version())
}
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("请选择:")
text := readIfTTY("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()
cli.UseDevice(device)
} else {
isTokenLogin = true
}
}
}
if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} {
cli.Uin = base.Account.Uin
cli.PasswordMd5 = base.PasswordHash
}
if !base.FastStart {
log.Infof("正在检查协议更新...")
currentVersionName := device.Protocol.Version().SortVersionName
remoteVersion, err := getRemoteLatestProtocolVersion(int(device.Protocol.Version().Protocol))
if err == nil {
remoteVersionName := gjson.GetBytes(remoteVersion, "sort_version_name").String()
if remoteVersionName != currentVersionName {
switch {
case !base.UpdateProtocol:
log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
log.Infof("如果登录时出现版本过低错误, 可尝试使用 -update-protocol 参数启动")
case !isTokenLogin:
_ = device.Protocol.Version().UpdateFromJson(remoteVersion)
log.Infof("协议版本已更新: %s -> %s", currentVersionName, remoteVersionName)
default:
log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
log.Infof("由于使用了会话缓存, 无法自动更新协议, 请删除缓存后重试")
}
}
} else if err.Error() != "remote version unavailable" {
log.Warnf("检查协议更新失败: %v", err)
}
}
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.DisconnectedEvent.Subscribe(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("アトリは、高性能ですから!")
}
// WaitSignal 在新线程检查更新和网络并等待信号, 必须在 InitBase, PrepareData, LoginInteract 之后执行
//
// - 直接返回: os.Interrupt, syscall.SIGTERM
// - dump stack: syscall.SIGQUIT, syscall.SIGUSR1
func WaitSignal() {
go func() {
selfupdate.CheckUpdate()
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.UseFragmentMessage = base.ForceFragmented
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.SetLogger(protocolLogger{})
return c
}
var remoteVersions = map[int]string{
1: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone.json",
6: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad.json",
}
func getRemoteLatestProtocolVersion(protocolType int) ([]byte, error) {
url, ok := remoteVersions[protocolType]
if !ok {
return nil, errors.New("remote version unavailable")
}
response, err := download.Request{URL: url}.Bytes()
if err != nil {
return download.Request{URL: "https://ghproxy.com/" + url}.Bytes()
}
return response, nil
}
type protocolLogger struct{}
const fromProtocol = "Protocol -> "
func (p protocolLogger) Info(format string, arg ...any) {
log.Infof(fromProtocol+format, arg...)
}
func (p protocolLogger) Warning(format string, arg ...any) {
log.Warnf(fromProtocol+format, arg...)
}
func (p protocolLogger) Debug(format string, arg ...any) {
log.Debugf(fromProtocol+format, arg...)
}
func (p protocolLogger) Error(format string, arg ...any) {
log.Errorf(fromProtocol+format, arg...)
}
func (p protocolLogger) Dump(data []byte, format string, arg ...any) {
if !global.PathExists(global.DumpsPath) {
_ = os.MkdirAll(global.DumpsPath, 0o755)
}
dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix()))
message := fmt.Sprintf(format, arg...)
log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile)
_ = os.WriteFile(dumpFile, data, 0o644)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +0,0 @@
package coolq
import (
"runtime"
"github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
// CQGetVersion 获取版本信息 OneBotV12
//
// https://git.io/JtwUs
// @route12(get_version)
func (bot *CQBot) CQGetVersion() global.MSG {
return OK(global.MSG{
"impl": "go_cqhttp",
"platform": "qq",
"version": base.Version,
"onebot_version": 12,
"runtime_version": runtime.Version(),
"runtime_os": runtime.GOOS,
})
}
// CQSendMessageV12 发送消息
//
// @route12(send_message)
// @rename(m->message)
func (bot *CQBot) CQSendMessageV12(groupID, userID, detailType string, m gjson.Result) global.MSG {
// TODO: implement
return OK(nil)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,222 +0,0 @@
package coolq
import (
"strconv"
"strings"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/MiraiGo/topic"
log "github.com/sirupsen/logrus"
"github.com/Mrs4s/go-cqhttp/global"
)
func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) global.MSG {
sex := "unknown"
if m.Gender == 1 { // unknown = 0xff
sex = "female"
} else if m.Gender == 0 {
sex = "male"
}
role := "member"
switch m.Permission { // nolint:exhaustive
case client.Owner:
role = "owner"
case client.Administrator:
role = "admin"
}
return global.MSG{
"group_id": groupID,
"user_id": m.Uin,
"nickname": m.Nickname,
"card": m.CardName,
"sex": sex,
"age": 0,
"area": "",
"join_time": m.JoinTime,
"last_sent_time": m.LastSpeakTime,
"shut_up_timestamp": m.ShutUpTimestamp,
"level": strconv.FormatInt(int64(m.Level), 10),
"role": role,
"unfriendly": false,
"title": m.SpecialTitle,
"title_expire_time": 0,
"card_changeable": false,
}
}
func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) {
for _, mem := range m {
r = append(r, global.MSG{
"tiny_id": fU64(mem.TinyId),
"title": mem.Title,
"nickname": mem.Nickname,
"role_id": fU64(mem.Role),
"role_name": mem.RoleName,
})
}
return
}
func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) *event {
source := message.Source{
SourceType: message.SourceGroup,
PrimaryID: m.GroupCode,
}
cqm := toStringMessage(m.Elements, source)
typ := "message/group/normal"
if m.Sender.Uin == bot.Client.Uin {
typ = "message_sent/group/normal"
}
gm := global.MSG{
"anonymous": nil,
"font": 0,
"group_id": m.GroupCode,
"message": ToFormattedMessage(m.Elements, source),
"message_seq": m.Id,
"raw_message": cqm,
"sender": global.MSG{
"age": 0,
"area": "",
"level": "",
"sex": "unknown",
"user_id": m.Sender.Uin,
},
"user_id": m.Sender.Uin,
}
if m.Sender.IsAnonymous() {
gm["anonymous"] = global.MSG{
"flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
"id": m.Sender.Uin,
"name": m.Sender.AnonymousInfo.AnonymousNick,
}
gm["sender"].(global.MSG)["nickname"] = "匿名消息"
gm["sub_type"] = "anonymous"
} else {
group := bot.Client.FindGroup(m.GroupCode)
mem := group.FindMember(m.Sender.Uin)
if mem == nil {
log.Warnf("获取 %v 成员信息失败,尝试刷新成员列表", m.Sender.Uin)
t, err := bot.Client.GetGroupMembers(group)
if err != nil {
log.Warnf("刷新群 %v 成员列表失败: %v", group.Uin, err)
return nil
}
group.Members = t
mem = group.FindMember(m.Sender.Uin)
if mem == nil {
return nil
}
}
ms := gm["sender"].(global.MSG)
role := "member"
switch mem.Permission { // nolint:exhaustive
case client.Owner:
role = "owner"
case client.Administrator:
role = "admin"
}
ms["role"] = role
ms["nickname"] = mem.Nickname
ms["card"] = mem.CardName
ms["title"] = mem.SpecialTitle
}
ev := bot.event(typ, gm)
ev.Time = int64(m.Time)
return ev
}
func convertChannelInfo(c *client.ChannelInfo) global.MSG {
slowModes := make([]global.MSG, 0, len(c.Meta.SlowModes))
for _, mode := range c.Meta.SlowModes {
slowModes = append(slowModes, global.MSG{
"slow_mode_key": mode.SlowModeKey,
"slow_mode_text": mode.SlowModeText,
"speak_frequency": mode.SpeakFrequency,
"slow_mode_circle": mode.SlowModeCircle,
})
}
return global.MSG{
"channel_id": fU64(c.ChannelId),
"channel_type": c.ChannelType,
"channel_name": c.ChannelName,
"owner_guild_id": fU64(c.Meta.GuildId),
"creator_tiny_id": fU64(c.Meta.CreatorTinyId),
"create_time": c.Meta.CreateTime,
"current_slow_mode": c.Meta.CurrentSlowMode,
"talk_permission": c.Meta.TalkPermission,
"visible_type": c.Meta.VisibleType,
"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 toStringMessage(m []message.IMessageElement, source message.Source) string {
elems := toElements(m, source)
var sb strings.Builder
for _, elem := range elems {
elem.WriteCQCodeTo(&sb)
}
return sb.String()
}
func fU64(v uint64) string {
return strconv.FormatUint(v, 10)
}

File diff suppressed because it is too large Load Diff

25
coolq/cqcode_test.go Normal file
View File

@ -0,0 +1,25 @@
package coolq
import (
"fmt"
"testing"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/go-cqhttp/global"
)
var bot = NewQQBot(client.NewClient(1, ""), global.DefaultConfig())
func TestCQBot_ConvertStringMessage(t *testing.T) {
for _, v := range bot.ConvertStringMessage(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`, false) {
fmt.Println(v)
}
}
var bench = `asdfqwerqwerqwer[CQ:face,id=115,text=111]asdfasdfasdfasdfasdfasdfasd[CQ:face,id=217]] [CQ:text,text=123] [`
func BenchmarkCQBot_ConvertStringMessage(b *testing.B) {
for i := 0; i < b.N; i++ {
bot.ConvertStringMessage(bench, false)
}
}

View File

@ -2,166 +2,128 @@ package coolq
import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"path"
"strconv"
"strings"
"time"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
log "github.com/sirupsen/logrus"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/cache"
"github.com/Mrs4s/go-cqhttp/internal/download"
)
var format = "string"
// SetMessageFormat 设置消息上报格式默认为string
func SetMessageFormat(f string) {
format = f
}
// ToFormattedMessage 将给定[]message.IMessageElement转换为通过coolq.SetMessageFormat所定义的消息上报格式
func ToFormattedMessage(e []message.IMessageElement, source message.Source) (r any) {
if base.PostFormat == "string" {
r = toStringMessage(e, source)
} else if base.PostFormat == "array" {
r = toElements(e, source)
func ToFormattedMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r interface{}) {
if format == "string" {
r = ToStringMessage(e, id, isRaw...)
} else if format == "array" {
r = ToArrayMessage(e, id, isRaw...)
}
return
}
type event struct {
PostType string
DetailType string
SubType string
Time int64
SelfID int64
Others global.MSG
}
func (ev *event) MarshalJSON() ([]byte, error) {
buf := global.NewBuffer()
defer global.PutBuffer(buf)
detail := ""
switch ev.PostType {
case "message", "message_sent":
detail = "message_type"
case "notice":
detail = "notice_type"
case "request":
detail = "request_type"
case "meta_event":
detail = "meta_event_type"
default:
panic("unknown post type: " + ev.PostType)
func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) {
bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, m.Sender.Uin, true)
if !m.Sender.IsFriend {
bot.oneWayMsgCache.Store(m.Sender.Uin, "")
}
fmt.Fprintf(buf, `{"post_type":"%s","%s":"%s","time":%d,"self_id":%d`, ev.PostType, detail, ev.DetailType, ev.Time, ev.SelfID)
if ev.SubType != "" {
fmt.Fprintf(buf, `,"sub_type":"%s"`, ev.SubType)
id := m.Id
if bot.db != nil {
id = bot.InsertPrivateMessage(m)
}
for k, v := range ev.Others {
v, err := json.Marshal(v)
if err != nil {
log.Warnf("marshal message payload error: %v", err)
return nil, err
}
fmt.Fprintf(buf, `,"%s":%s`, k, v)
}
buf.WriteByte('}')
return append([]byte(nil), buf.Bytes()...), nil
}
func (bot *CQBot) privateMessageEvent(_ *client.QQClient, m *message.PrivateMessage) {
bot.checkMedia(m.Elements, m.Sender.Uin)
source := message.Source{
SourceType: message.SourcePrivate,
PrimaryID: m.Sender.Uin,
}
cqm := toStringMessage(m.Elements, source)
id := bot.InsertPrivateMessage(m)
log.Infof("收到好友 %v(%v) 的消息: %v (%v)", m.Sender.DisplayName(), m.Sender.Uin, cqm, id)
typ := "message/private/friend"
if m.Sender.Uin == bot.Client.Uin {
typ = "message_sent/private/friend"
}
fm := global.MSG{
"message_id": id,
"user_id": m.Sender.Uin,
"target_id": m.Target,
"message": ToFormattedMessage(m.Elements, source),
"raw_message": cqm,
"font": 0,
"sender": global.MSG{
fm := MSG{
"post_type": "message",
"message_type": "private",
"sub_type": "friend",
"message_id": id,
"user_id": m.Sender.Uin,
"target_id": m.Target,
"message": ToFormattedMessage(m.Elements, m.Sender.Uin, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
"time": time.Now().Unix(),
"sender": MSG{
"user_id": m.Sender.Uin,
"nickname": m.Sender.Nickname,
"sex": "unknown",
"age": 0,
},
}
bot.dispatchEvent(typ, fm)
bot.dispatchEventMessage(fm)
}
func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) {
bot.checkMedia(m.Elements, m.GroupCode)
bot.checkMedia(m.Elements)
for _, elem := range m.Elements {
if file, ok := elem.(*message.GroupFileElement); ok {
log.Infof("群 %v(%v) 内 %v(%v) 上传了文件: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, file.Name)
bot.dispatchEvent("notice/group_upload", global.MSG{
"group_id": m.GroupCode,
"user_id": m.Sender.Uin,
"file": global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "group_upload",
"group_id": m.GroupCode,
"user_id": m.Sender.Uin,
"file": MSG{
"id": file.Path,
"name": file.Name,
"size": file.Size,
"busid": file.Busid,
"url": c.GetGroupFileUrl(m.GroupCode, file.Path, file.Busid),
},
"self_id": c.Uin,
"time": time.Now().Unix(),
})
return
}
}
source := message.Source{
SourceType: message.SourceGroup,
PrimaryID: m.GroupCode,
cqm := ToStringMessage(m.Elements, m.GroupCode, true)
id := m.Id
if bot.db != nil {
id = bot.InsertGroupMessage(m)
}
cqm := toStringMessage(m.Elements, source)
id := bot.InsertGroupMessage(m)
log.Infof("收到群 %v(%v) 内 %v(%v) 的消息: %v (%v)", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm, id)
gm := bot.formatGroupMessage(m)
if gm == nil {
return
}
gm.Others["message_id"] = id
bot.dispatch(gm)
gm["message_id"] = id
bot.dispatchEventMessage(gm)
}
func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEvent) {
m := e.Message
bot.checkMedia(m.Elements, m.Sender.Uin)
source := message.Source{
SourceType: message.SourcePrivate,
PrimaryID: e.Session.Sender,
}
cqm := toStringMessage(m.Elements, source)
if base.AllowTempSession {
bot.tempSessionCache.Store(m.Sender.Uin, e.Session)
}
func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, m.Sender.Uin, true)
bot.tempMsgCache.Store(m.Sender.Uin, m.GroupCode)
id := m.Id
// todo(Mrs4s)
// if bot.db != nil { // nolint
// id = bot.InsertTempMessage(m.Sender.Uin, m)
// }
if bot.db != nil {
id = bot.InsertTempMessage(m.Sender.Uin, m)
}
log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm)
tm := global.MSG{
"temp_source": e.Session.Source,
"message_id": id,
"user_id": m.Sender.Uin,
"message": ToFormattedMessage(m.Elements, source),
"raw_message": cqm,
"font": 0,
"sender": global.MSG{
tm := MSG{
"post_type": "message",
"message_type": "private",
"sub_type": "group",
"message_id": id,
"user_id": m.Sender.Uin,
"message": ToFormattedMessage(m.Elements, m.Sender.Uin, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
"time": time.Now().Unix(),
"sender": MSG{
"user_id": m.Sender.Uin,
"group_id": m.GroupCode,
"nickname": m.Sender.Nickname,
@ -169,155 +131,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEven
"age": 0,
},
}
bot.dispatchEvent("message/private/group", tm)
}
func (bot *CQBot) guildChannelMessageEvent(c *client.QQClient, m *message.GuildChannelMessage) {
bot.checkMedia(m.Elements, int64(m.Sender.TinyId))
guild := c.GuildService.FindGuild(m.GuildId)
if guild == nil {
return
}
channel := guild.FindChannel(m.ChannelId)
source := message.Source{
SourceType: message.SourceGuildChannel,
PrimaryID: int64(m.GuildId),
SecondaryID: int64(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))
id := bot.InsertGuildChannelMessage(m)
ev := bot.event("message/guild/channel", global.MSG{
"guild_id": fU64(m.GuildId),
"channel_id": fU64(m.ChannelId),
"message_id": id,
"user_id": fU64(m.Sender.TinyId),
"message": ToFormattedMessage(m.Elements, source), // todo: 增加对频道消息 Reply 的支持
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"sender": global.MSG{
"user_id": m.Sender.TinyId,
"tiny_id": fU64(m.Sender.TinyId),
"nickname": m.Sender.Nickname,
},
})
ev.Time = m.Time
bot.dispatch(ev)
}
func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *client.GuildMessageReactionsUpdatedEvent) {
guild := c.GuildService.FindGuild(e.GuildId)
if guild == nil {
return
}
msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, message.SourceGuildChannel)
str := fmt.Sprintf("频道 %v(%v) 消息 %v 表情贴片已更新: ", guild.GuildName, guild.GuildId, msgID)
currentReactions := make([]global.MSG, len(e.CurrentReactions))
for i, r := range e.CurrentReactions {
str += fmt.Sprintf("%v*%v ", r.Face.Name, r.Count)
currentReactions[i] = global.MSG{
"emoji_id": r.EmojiId,
"emoji_index": r.Face.Index,
"emoji_type": r.EmojiType,
"emoji_name": r.Face.Name,
"count": r.Count,
"clicked": r.Clicked,
}
}
if len(e.CurrentReactions) == 0 {
str += "无任何表情"
}
log.Infof(str)
bot.dispatchEvent("notice/message_reactions_updated", global.MSG{
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelId),
"message_id": msgID,
"operator_id": fU64(e.OperatorId),
"current_reactions": currentReactions,
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"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, message.SourceGuildChannel)
log.Infof("用户 %v(%v) 撤回了频道 %v(%v) 子频道 %v(%v) 的消息 %v", operator.Nickname, operator.TinyId, guild.GuildName, guild.GuildId, channel.ChannelName, channel.ChannelId, msgID)
bot.dispatchEvent("notice/guild_channel_recall", global.MSG{
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelId),
"operator_id": fU64(e.OperatorId),
"message_id": msgID,
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId,
})
}
func (bot *CQBot) guildChannelUpdatedEvent(c *client.QQClient, e *client.GuildChannelUpdatedEvent) {
guild := c.GuildService.FindGuild(e.GuildId)
if guild == nil {
return
}
log.Infof("频道 %v(%v) 子频道 %v(%v) 信息已更新", guild.GuildName, guild.GuildId, e.NewChannelInfo.ChannelName, e.NewChannelInfo.ChannelId)
bot.dispatchEvent("notice/channel_updated", global.MSG{
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelId),
"operator_id": fU64(e.OperatorId),
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId,
"old_info": convertChannelInfo(e.OldChannelInfo),
"new_info": convertChannelInfo(e.NewChannelInfo),
})
}
func (bot *CQBot) guildChannelCreatedEvent(c *client.QQClient, e *client.GuildChannelOperationEvent) {
guild := c.GuildService.FindGuild(e.GuildId)
if guild == nil {
return
}
member, _ := c.GuildService.FetchGuildMemberProfileInfo(e.GuildId, e.OperatorId)
if member == nil {
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)
bot.dispatchEvent("notice/channel_created", global.MSG{
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelInfo.ChannelId),
"operator_id": fU64(e.OperatorId),
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId,
"channel_info": convertChannelInfo(e.ChannelInfo),
})
}
func (bot *CQBot) guildChannelDestroyedEvent(c *client.QQClient, e *client.GuildChannelOperationEvent) {
guild := c.GuildService.FindGuild(e.GuildId)
if guild == nil {
return
}
member, _ := c.GuildService.FetchGuildMemberProfileInfo(e.GuildId, e.OperatorId)
if member == nil {
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)
bot.dispatchEvent("notice/channel_destroyed", global.MSG{
"guild_id": fU64(e.GuildId),
"channel_id": fU64(e.ChannelInfo.ChannelId),
"operator_id": fU64(e.OperatorId),
"self_tiny_id": fU64(bot.Client.GuildService.TinyId),
"user_id": e.OperatorId,
"channel_info": convertChannelInfo(e.ChannelInfo),
})
bot.dispatchEventMessage(tm)
}
func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent) {
@ -339,32 +153,40 @@ func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent)
formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin)))
}
}
typ := "notice/group_ban/ban"
if e.Time == 0 {
typ = "notice/group_ban/lift_ban"
}
bot.dispatchEvent(typ, global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"duration": e.Time,
"group_id": e.GroupCode,
"notice_type": "group_ban",
"operator_id": e.OperatorUin,
"self_id": c.Uin,
"user_id": e.TargetUin,
"time": time.Now().Unix(),
"sub_type": func() string {
if e.Time == 0 {
return "lift_ban"
}
return "ban"
}(),
})
}
func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMessageRecalledEvent) {
g := c.FindGroup(e.GroupCode)
gid := db.ToGlobalID(e.GroupCode, e.MessageId)
gid := toGlobalID(e.GroupCode, e.MessageId)
log.Infof("群 %v 内 %v 撤回了 %v 的消息: %v.",
formatGroupName(g), formatMemberName(g.FindMember(e.OperatorUin)), formatMemberName(g.FindMember(e.AuthorUin)), gid)
ev := bot.event("notice/group_recall", global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": e.GroupCode,
"notice_type": "group_recall",
"self_id": c.Uin,
"user_id": e.AuthorUin,
"operator_id": e.OperatorUin,
"time": e.Time,
"message_id": gid,
})
ev.Time = int64(e.Time)
bot.dispatch(ev)
}
func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
@ -374,27 +196,42 @@ func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
sender := group.FindMember(notify.Sender)
receiver := group.FindMember(notify.Receiver)
log.Infof("群 %v 内 %v 戳了戳 %v", formatGroupName(group), formatMemberName(sender), formatMemberName(receiver))
bot.dispatchEvent("notice/notify/poke", global.MSG{
"group_id": group.Code,
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.Receiver,
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": group.Code,
"notice_type": "notify",
"sub_type": "poke",
"self_id": c.Uin,
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.Receiver,
"time": time.Now().Unix(),
})
case *client.GroupRedBagLuckyKingNotifyEvent:
sender := group.FindMember(notify.Sender)
luckyKing := group.FindMember(notify.LuckyKing)
log.Infof("群 %v 内 %v 的红包被抢完, %v 是运气王", formatGroupName(group), formatMemberName(sender), formatMemberName(luckyKing))
bot.dispatchEvent("notice/notify/lucky_king", global.MSG{
"group_id": group.Code,
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.LuckyKing,
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": group.Code,
"notice_type": "notify",
"sub_type": "lucky_king",
"self_id": c.Uin,
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.LuckyKing,
"time": time.Now().Unix(),
})
case *client.MemberHonorChangedNotifyEvent:
log.Info(notify.Content())
bot.dispatchEvent("notice/notify/honor", global.MSG{
"group_id": group.Code,
"user_id": notify.Uin,
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": group.Code,
"notice_type": "notify",
"sub_type": "honor",
"self_id": c.Uin,
"user_id": notify.Uin,
"time": time.Now().Unix(),
"honor_type": func() string {
switch notify.Honor {
case client.Talkative:
@ -418,44 +255,36 @@ func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotifyEvent) {
friend := c.FindFriend(e.From())
if notify, ok := e.(*client.FriendPokeNotifyEvent); ok {
if notify.Receiver == notify.Sender {
log.Infof("好友 %v 戳了戳自己.", friend.Nickname)
} else {
log.Infof("好友 %v 戳了戳你.", friend.Nickname)
}
bot.dispatchEvent("notice/notify/poke", global.MSG{
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.Receiver,
log.Infof("好友 %v 戳了戳你.", friend.Nickname)
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "notify",
"sub_type": "poke",
"self_id": c.Uin,
"user_id": notify.Sender,
"sender_id": notify.Sender,
"target_id": notify.Receiver,
"time": time.Now().Unix(),
})
}
}
func (bot *CQBot) memberTitleUpdatedEvent(c *client.QQClient, e *client.MemberSpecialTitleUpdatedEvent) {
group := c.FindGroup(e.GroupCode)
mem := group.FindMember(e.Uin)
log.Infof("群 %v(%v) 内成员 %v(%v) 获得了新的头衔: %v", group.Name, group.Code, mem.DisplayName(), mem.Uin, e.NewTitle)
bot.dispatchEvent("notice/notify/title", global.MSG{
"group_id": group.Code,
"user_id": e.Uin,
"title": e.NewTitle,
})
}
func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageRecalledEvent) {
f := c.FindFriend(e.FriendUin)
gid := db.ToGlobalID(e.FriendUin, e.MessageId)
gid := toGlobalID(e.FriendUin, e.MessageId)
if f != nil {
log.Infof("好友 %v(%v) 撤回了消息: %v", f.Nickname, f.Uin, gid)
} else {
log.Infof("好友 %v 撤回了消息: %v", e.FriendUin, gid)
}
ev := bot.event("notice/friend_recall", global.MSG{
"user_id": e.FriendUin,
"message_id": gid,
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "friend_recall",
"self_id": c.Uin,
"user_id": e.FriendUin,
"time": e.Time,
"message_id": gid,
})
ev.Time = e.Time
bot.dispatch(ev)
}
func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.OfflineFileEvent) {
@ -464,22 +293,23 @@ func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.OfflineFileEven
return
}
log.Infof("好友 %v(%v) 发送了离线文件 %v", f.Nickname, f.Uin, e.FileName)
bot.dispatchEvent("notice/offline_file", global.MSG{
"user_id": e.Sender,
"file": global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "offline_file",
"user_id": e.Sender,
"file": MSG{
"name": e.FileName,
"size": e.FileSize,
"url": e.DownloadUrl,
},
"self_id": c.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) joinGroupEvent(c *client.QQClient, group *client.GroupInfo) {
if group == nil {
return
}
log.Infof("Bot进入了群 %v.", formatGroupName(group))
bot.dispatch(bot.groupIncrease(group.Code, 0, c.Uin))
bot.dispatchEventMessage(bot.groupIncrease(group.Code, 0, c.Uin))
}
func (bot *CQBot) leaveGroupEvent(c *client.QQClient, e *client.GroupLeaveEvent) {
@ -488,84 +318,111 @@ func (bot *CQBot) leaveGroupEvent(c *client.QQClient, e *client.GroupLeaveEvent)
} else {
log.Infof("Bot退出了群 %v.", formatGroupName(e.Group))
}
bot.dispatch(bot.groupDecrease(e.Group.Code, c.Uin, e.Operator))
bot.dispatchEventMessage(bot.groupDecrease(e.Group.Code, c.Uin, e.Operator))
}
func (bot *CQBot) memberPermissionChangedEvent(c *client.QQClient, e *client.MemberPermissionChangedEvent) {
st := "unset"
if e.NewPermission == client.Administrator {
st = "set"
}
bot.dispatchEvent("notice/group_admin/"+st, global.MSG{
"group_id": e.Group.Code,
"user_id": e.Member.Uin,
st := func() string {
if e.NewPermission == client.Administrator {
return "set"
}
return "unset"
}()
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "group_admin",
"sub_type": st,
"group_id": e.Group.Code,
"user_id": e.Member.Uin,
"time": time.Now().Unix(),
"self_id": c.Uin,
})
}
func (bot *CQBot) memberCardUpdatedEvent(c *client.QQClient, e *client.MemberCardUpdatedEvent) {
log.Infof("群 %v 的 %v 更新了名片 %v -> %v", formatGroupName(e.Group), formatMemberName(e.Member), e.OldCard, e.Member.CardName)
bot.dispatchEvent("notice/group_card", global.MSG{
"group_id": e.Group.Code,
"user_id": e.Member.Uin,
"card_new": e.Member.CardName,
"card_old": e.OldCard,
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "group_card",
"group_id": e.Group.Code,
"user_id": e.Member.Uin,
"card_new": e.Member.CardName,
"card_old": e.OldCard,
"time": time.Now().Unix(),
"self_id": c.Uin,
})
}
func (bot *CQBot) memberJoinEvent(_ *client.QQClient, e *client.MemberJoinGroupEvent) {
func (bot *CQBot) memberJoinEvent(c *client.QQClient, e *client.MemberJoinGroupEvent) {
log.Infof("新成员 %v 进入了群 %v.", formatMemberName(e.Member), formatGroupName(e.Group))
bot.dispatch(bot.groupIncrease(e.Group.Code, 0, e.Member.Uin))
bot.dispatchEventMessage(bot.groupIncrease(e.Group.Code, 0, e.Member.Uin))
}
func (bot *CQBot) memberLeaveEvent(_ *client.QQClient, e *client.MemberLeaveGroupEvent) {
func (bot *CQBot) memberLeaveEvent(c *client.QQClient, e *client.MemberLeaveGroupEvent) {
if e.Operator != nil {
log.Infof("成员 %v 被 %v T出了群 %v.", formatMemberName(e.Member), formatMemberName(e.Operator), formatGroupName(e.Group))
} else {
log.Infof("成员 %v 离开了群 %v.", formatMemberName(e.Member), formatGroupName(e.Group))
}
bot.dispatch(bot.groupDecrease(e.Group.Code, e.Member.Uin, e.Operator))
bot.dispatchEventMessage(bot.groupDecrease(e.Group.Code, e.Member.Uin, e.Operator))
}
func (bot *CQBot) friendRequestEvent(c *client.QQClient, e *client.NewFriendRequest) {
log.Infof("收到来自 %v(%v) 的好友请求: %v", e.RequesterNick, e.RequesterUin, e.Message)
flag := strconv.FormatInt(e.RequestId, 10)
bot.friendReqCache.Store(flag, e)
bot.dispatchEvent("request/friend", global.MSG{
"user_id": e.RequesterUin,
"comment": e.Message,
"flag": flag,
bot.dispatchEventMessage(MSG{
"post_type": "request",
"request_type": "friend",
"user_id": e.RequesterUin,
"comment": e.Message,
"flag": flag,
"time": time.Now().Unix(),
"self_id": c.Uin,
})
}
func (bot *CQBot) friendAddedEvent(c *client.QQClient, e *client.NewFriendEvent) {
log.Infof("添加了新好友: %v(%v)", e.Friend.Nickname, e.Friend.Uin)
bot.tempSessionCache.Delete(e.Friend.Uin)
bot.dispatchEvent("notice/friend_add", global.MSG{
"user_id": e.Friend.Uin,
bot.tempMsgCache.Delete(e.Friend.Uin)
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "friend_add",
"self_id": c.Uin,
"user_id": e.Friend.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) {
log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin)
flag := strconv.FormatInt(e.RequestId, 10)
bot.dispatchEvent("request/group/invite", global.MSG{
"group_id": e.GroupCode,
"user_id": e.InvitorUin,
"invitor_id": 0,
"comment": "",
"flag": flag,
bot.dispatchEventMessage(MSG{
"post_type": "request",
"request_type": "group",
"sub_type": "invite",
"group_id": e.GroupCode,
"user_id": e.InvitorUin,
"comment": "",
"flag": flag,
"time": time.Now().Unix(),
"self_id": c.Uin,
})
}
func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) {
log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupCode, e.RequesterNick, e.RequesterUin)
flag := strconv.FormatInt(e.RequestId, 10)
bot.dispatchEvent("request/group/add", global.MSG{
"group_id": e.GroupCode,
"user_id": e.RequesterUin,
"invitor_id": e.ActionUin,
"comment": e.Message,
"flag": flag,
bot.dispatchEventMessage(MSG{
"post_type": "request",
"request_type": "group",
"sub_type": "add",
"group_id": e.GroupCode,
"user_id": e.RequesterUin,
"comment": e.Message,
"flag": flag,
"time": time.Now().Unix(),
"self_id": c.Uin,
})
}
@ -575,19 +432,23 @@ func (bot *CQBot) otherClientStatusChangedEvent(c *client.QQClient, e *client.Ot
} else {
log.Infof("Bot 账号在客户端 %v (%v) 登出.", e.Client.DeviceName, e.Client.DeviceKind)
}
bot.dispatchEvent("notice/client_status", global.MSG{
"online": e.Online,
"client": global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "client_status",
"online": e.Online,
"client": MSG{
"app_id": e.Client.AppId,
"device_name": e.Client.DeviceName,
"device_kind": e.Client.DeviceKind,
},
"self_id": c.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent) {
g := c.FindGroup(e.GroupCode)
gid := db.ToGlobalID(e.GroupCode, e.MessageID)
gid := toGlobalID(e.GroupCode, e.MessageID)
if e.OperationType == 1 {
log.Infof(
"群 %v 内 %v 将 %v 的消息(%v)设为了精华消息.",
@ -608,112 +469,142 @@ func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent
if e.OperatorUin == bot.Client.Uin {
return
}
subtype := "delete"
if e.OperationType == 1 {
subtype = "add"
}
bot.dispatchEvent("notice/essence/"+subtype, global.MSG{
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": e.GroupCode,
"notice_type": "essence",
"sub_type": func() string {
if e.OperationType == 1 {
return "add"
}
return "delete"
}(),
"self_id": c.Uin,
"sender_id": e.SenderUin,
"operator_id": e.OperatorUin,
"time": time.Now().Unix(),
"message_id": gid,
})
}
func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) *event {
return bot.event("notice/group_increase/approve", global.MSG{
func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) MSG {
return MSG{
"post_type": "notice",
"notice_type": "group_increase",
"group_id": groupCode,
"operator_id": operatorUin,
"self_id": bot.Client.Uin,
"sub_type": "approve",
"time": time.Now().Unix(),
"user_id": userUin,
})
}
}
func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.GroupMemberInfo) *event {
op := userUin
if operator != nil {
op = operator.Uin
}
subtype := "leave"
if operator != nil {
if userUin == bot.Client.Uin {
subtype = "kick_me"
} else {
subtype = "kick"
}
}
return bot.event("notice/group_decrease/"+subtype, global.MSG{
func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.GroupMemberInfo) MSG {
return MSG{
"post_type": "notice",
"notice_type": "group_decrease",
"group_id": groupCode,
"operator_id": op,
"user_id": userUin,
})
"operator_id": func() int64 {
if operator != nil {
return operator.Uin
}
return userUin
}(),
"self_id": bot.Client.Uin,
"sub_type": func() string {
if operator != nil {
if userUin == bot.Client.Uin {
return "kick_me"
}
return "kick"
}
return "leave"
}(),
"time": time.Now().Unix(),
"user_id": userUin,
}
}
func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {
func (bot *CQBot) checkMedia(e []message.IMessageElement) {
for _, elem := range e {
switch i := elem.(type) {
case *message.GroupImageElement:
if i.Flash && sourceID != 0 {
u, err := bot.Client.GetGroupImageDownloadUrl(i.FileId, sourceID, i.Md5)
if err != nil {
log.Warnf("获取闪照地址时出现错误: %v", err)
} else {
i.Url = u
}
}
data := binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.ImageId)
w.WriteString(i.Url)
})
cache.Image.Insert(i.Md5, data)
case *message.GuildImageElement:
data := binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.DownloadIndex)
w.WriteString(i.Url)
})
case *message.ImageElement:
filename := hex.EncodeToString(i.Md5) + ".image"
cache.Image.Insert(i.Md5, data)
if i.Url != "" && !global.PathExists(path.Join(global.ImagePath, "guild-images", filename)) {
r := download.Request{URL: i.Url}
if err := r.WriteToFile(path.Join(global.ImagePath, "guild-images", filename)); err != nil {
log.Warnf("下载频道图片时出现错误: %v", err)
}
if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
w.WriteString(i.Url)
}), 0644)
}
i.Filename = filename
case *message.GroupImageElement:
filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(filename)
w.WriteString(i.Url)
}), 0644)
}
case *message.FriendImageElement:
data := binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.ImageId)
w.WriteString(i.Url)
})
cache.Image.Insert(i.Md5, data)
filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(0)) // 发送时会调用url, 大概没事
w.WriteString(filename)
w.WriteString(i.Url)
}), 0644)
}
case *message.GroupFlashImgElement:
filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
w.WriteString("")
}), 0644)
}
i.Filename = filename
case *message.FriendFlashImgElement:
filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
w.WriteString("")
}), 0644)
}
i.Filename = filename
case *message.VoiceElement:
// todo: don't download original file?
i.Name = strings.ReplaceAll(i.Name, "{", "")
i.Name = strings.ReplaceAll(i.Name, "}", "")
if !global.PathExists(path.Join(global.VoicePath, i.Name)) {
err := download.Request{URL: i.Url}.WriteToFile(path.Join(global.VoicePath, i.Name))
b, err := global.GetBytes(i.Url)
if err != nil {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue
}
_ = ioutil.WriteFile(path.Join(global.VoicePath, i.Name), b, 0644)
}
case *message.ShortVideoElement:
data := binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.Write(i.ThumbMd5)
w.WriteUInt32(uint32(i.Size))
w.WriteUInt32(uint32(i.ThumbSize))
w.WriteString(i.Name)
w.Write(i.Uuid)
})
filename := hex.EncodeToString(i.Md5) + ".video"
cache.Video.Insert(i.Md5, data)
if !global.PathExists(path.Join(global.VideoPath, filename)) {
_ = ioutil.WriteFile(path.Join(global.VideoPath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.Write(i.ThumbMd5)
w.WriteUInt32(uint32(i.Size))
w.WriteUInt32(uint32(i.ThumbSize))
w.WriteString(i.Name)
w.Write(i.Uuid)
}), 0644)
}
i.Name = filename
i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5)
}

View File

@ -1,53 +0,0 @@
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

@ -1,116 +0,0 @@
package db
import (
"fmt"
"hash/crc32"
"github.com/Mrs4s/go-cqhttp/global"
)
type (
// Database 数据库操作接口定义
Database interface {
// Open 初始化数据库
Open() error
// GetMessageByGlobalID 通过 GlobalID 来获取消息
GetMessageByGlobalID(int32) (StoredMessage, error)
// GetGroupMessageByGlobalID 通过 GlobalID 来获取群消息
GetGroupMessageByGlobalID(int32) (*StoredGroupMessage, error)
// GetPrivateMessageByGlobalID 通过 GlobalID 来获取私聊消息
GetPrivateMessageByGlobalID(int32) (*StoredPrivateMessage, error)
// GetGuildChannelMessageByID 通过 ID 来获取频道消息
GetGuildChannelMessageByID(string) (*StoredGuildChannelMessage, error)
// InsertGroupMessage 向数据库写入新的群消息
InsertGroupMessage(*StoredGroupMessage) error
// InsertPrivateMessage 向数据库写入新的私聊消息
InsertPrivateMessage(*StoredPrivateMessage) error
// InsertGuildChannelMessage 向数据库写入新的频道消息
InsertGuildChannelMessage(*StoredGuildChannelMessage) error
}
StoredMessage interface {
GetID() string
GetType() string
GetGlobalID() int32
GetAttribute() *StoredMessageAttribute
GetContent() []global.MSG
}
// StoredGroupMessage 持久化群消息
StoredGroupMessage struct {
ID string `bson:"_id" yaml:"-"`
GlobalID int32 `bson:"globalId" yaml:"-"`
Attribute *StoredMessageAttribute `bson:"attribute" yaml:"-"`
SubType string `bson:"subType" yaml:"-"`
QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
GroupCode int64 `bson:"groupCode" yaml:"-"`
AnonymousID string `bson:"anonymousId" yaml:"-"`
Content []global.MSG `bson:"content" yaml:"content"`
}
// StoredPrivateMessage 持久化私聊消息
StoredPrivateMessage struct {
ID string `bson:"_id" yaml:"-"`
GlobalID int32 `bson:"globalId" yaml:"-"`
Attribute *StoredMessageAttribute `bson:"attribute" yaml:"-"`
SubType string `bson:"subType" yaml:"-"`
QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
SessionUin int64 `bson:"sessionUin" yaml:"-"`
TargetUin int64 `bson:"targetUin" yaml:"-"`
Content []global.MSG `bson:"content" yaml:"content"`
}
// StoredGuildChannelMessage 持久化频道消息
StoredGuildChannelMessage struct {
ID string `bson:"_id" yaml:"-"`
Attribute *StoredGuildMessageAttribute `bson:"attribute" yaml:"-"`
GuildID uint64 `bson:"guildId" yaml:"-"`
ChannelID uint64 `bson:"channelId" yaml:"-"`
QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
Content []global.MSG `bson:"content" yaml:"content"`
}
// StoredMessageAttribute 持久化消息属性
StoredMessageAttribute struct {
MessageSeq int32 `bson:"messageSeq" yaml:"-"`
InternalID int32 `bson:"internalId" yaml:"-"`
SenderUin int64 `bson:"senderUin" yaml:"-"`
SenderName string `bson:"senderName" yaml:"-"`
Timestamp int64 `bson:"timestamp" yaml:"-"`
}
// StoredGuildMessageAttribute 持久化频道消息属性
StoredGuildMessageAttribute struct {
MessageSeq uint64 `bson:"messageSeq" yaml:"-"`
InternalID uint64 `bson:"internalId" yaml:"-"`
SenderTinyID uint64 `bson:"senderTinyId" yaml:"-"`
SenderName string `bson:"senderName" yaml:"-"`
Timestamp int64 `bson:"timestamp" yaml:"-"`
}
// QuotedInfo 引用回复
QuotedInfo struct {
PrevID string `bson:"prevId" yaml:"-"`
PrevGlobalID int32 `bson:"prevGlobalId" yaml:"-"`
QuotedContent []global.MSG `bson:"quotedContent" yaml:"quoted_content"`
}
)
// ToGlobalID 构建`code`-`msgID`的字符串并返回其CRC32 Checksum的值
func ToGlobalID(code int64, msgID int32) int32 {
return int32(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d-%d", code, msgID))))
}
func (m *StoredGroupMessage) GetID() string { return m.ID }
func (m *StoredGroupMessage) GetType() string { return "group" }
func (m *StoredGroupMessage) GetGlobalID() int32 { return m.GlobalID }
func (m *StoredGroupMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute }
func (m *StoredGroupMessage) GetContent() []global.MSG { return m.Content }
func (m *StoredPrivateMessage) GetID() string { return m.ID }
func (m *StoredPrivateMessage) GetType() string { return "private" }
func (m *StoredPrivateMessage) GetGlobalID() int32 { return m.GlobalID }
func (m *StoredPrivateMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute }
func (m *StoredPrivateMessage) GetContent() []global.MSG { return m.Content }

View File

@ -1,25 +0,0 @@
package leveldb
const dataVersion = 1
const (
group = 0x0
private = 0x1
guildChannel = 0x2
)
type coder byte
const (
coderNil coder = iota
coderInt
coderUint
coderInt32
coderUint32
coderInt64
coderUint64
coderString
coderMSG // global.MSG
coderArrayMSG // []global.MSG
coderStruct // struct{}
)

View File

@ -1,140 +0,0 @@
package leveldb
import (
"path"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/pkg/errors"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
"gopkg.in/yaml.v3"
"github.com/Mrs4s/go-cqhttp/db"
)
type database struct {
db *leveldb.DB
}
// config leveldb 相关配置
type config struct {
Enable bool `yaml:"enable"`
}
func init() {
db.Register("leveldb", func(node yaml.Node) db.Database {
conf := new(config)
_ = node.Decode(conf)
if !conf.Enable {
return nil
}
return &database{}
})
}
func (ldb *database) Open() error {
p := path.Join("data", "leveldb-v3")
d, err := leveldb.OpenFile(p, &opt.Options{
WriteBuffer: 32 * opt.KiB,
})
if err != nil {
return errors.Wrap(err, "open leveldb error")
}
ldb.db = d
return nil
}
func (ldb *database) GetMessageByGlobalID(id int32) (_ db.StoredMessage, err error) {
v, err := ldb.db.Get(binary.ToBytes(id), nil)
if err != nil || len(v) == 0 {
return nil, errors.Wrap(err, "get value error")
}
defer func() {
if r := recover(); r != nil {
err = errors.Errorf("%v", r)
}
}()
r, err := newReader(utils.B2S(v))
if err != nil {
return nil, err
}
switch r.uvarint() {
case group:
return r.readStoredGroupMessage(), nil
case private:
return r.readStoredPrivateMessage(), nil
default:
return nil, errors.New("unknown message flag")
}
}
func (ldb *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) {
i, err := ldb.GetMessageByGlobalID(id)
if err != nil {
return nil, err
}
g, ok := i.(*db.StoredGroupMessage)
if !ok {
return nil, errors.New("message type error")
}
return g, nil
}
func (ldb *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) {
i, err := ldb.GetMessageByGlobalID(id)
if err != nil {
return nil, err
}
p, ok := i.(*db.StoredPrivateMessage)
if !ok {
return nil, errors.New("message type error")
}
return p, nil
}
func (ldb *database) 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")
}
defer func() {
if r := recover(); r != nil {
err = errors.Errorf("%v", r)
}
}()
r, err := newReader(utils.B2S(v))
if err != nil {
return nil, err
}
switch r.uvarint() {
case guildChannel:
return r.readStoredGuildChannelMessage(), nil
default:
return nil, errors.New("unknown message flag")
}
}
func (ldb *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
w := newWriter()
w.uvarint(group)
w.writeStoredGroupMessage(msg)
err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil)
return errors.Wrap(err, "put data error")
}
func (ldb *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
w := newWriter()
w.uvarint(private)
w.writeStoredPrivateMessage(msg)
err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil)
return errors.Wrap(err, "put data error")
}
func (ldb *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
w := newWriter()
w.uvarint(guildChannel)
w.writeStoredGuildChannelMessage(msg)
err := ldb.db.Put(utils.S2B(msg.ID), w.bytes(), nil)
return errors.Wrap(err, "put data error")
}

View File

@ -1,131 +0,0 @@
package leveldb
import (
"encoding/binary"
"io"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/Mrs4s/go-cqhttp/global"
)
type intReader struct {
data string
*strings.Reader
}
func newIntReader(s string) intReader {
return intReader{
data: s,
Reader: strings.NewReader(s),
}
}
func (r *intReader) varint() int64 {
i, _ := binary.ReadVarint(r)
return i
}
func (r *intReader) uvarint() uint64 {
i, _ := binary.ReadUvarint(r)
return i
}
// reader implements the index read.
// data format is the same as the writer's
type reader struct {
data intReader
strings intReader
stringIndex map[uint64]string
}
func (r *reader) coder() coder { o, _ := r.data.ReadByte(); return coder(o) }
func (r *reader) varint() int64 { return r.data.varint() }
func (r *reader) uvarint() uint64 { return r.data.uvarint() }
func (r *reader) int32() int32 { return int32(r.varint()) }
func (r *reader) int64() int64 { return r.varint() }
func (r *reader) uint64() uint64 { return r.uvarint() }
// func (r *reader) uint32() uint32 { return uint32(r.uvarint()) }
// func (r *reader) int() int { return int(r.varint()) }
// func (r *reader) uint() uint { return uint(r.uvarint()) }
func (r *reader) string() string {
off := r.data.uvarint()
if s, ok := r.stringIndex[off]; ok {
return s
}
_, _ = r.strings.Seek(int64(off), io.SeekStart)
l := int64(r.strings.uvarint())
whence, _ := r.strings.Seek(0, io.SeekCurrent)
s := r.strings.data[whence : whence+l]
r.stringIndex[off] = s
return s
}
func (r *reader) msg() global.MSG {
length := r.uvarint()
msg := make(global.MSG, length)
for i := uint64(0); i < length; i++ {
s := r.string()
msg[s] = r.obj()
}
return msg
}
func (r *reader) arrayMsg() []global.MSG {
length := r.uvarint()
msgs := make([]global.MSG, length)
for i := range msgs {
msgs[i] = r.msg()
}
return msgs
}
func (r *reader) obj() any {
switch coder := r.coder(); coder {
case coderNil:
return nil
case coderInt:
return int(r.varint())
case coderUint:
return uint(r.uvarint())
case coderInt32:
return int32(r.varint())
case coderUint32:
return uint32(r.uvarint())
case coderInt64:
return r.varint()
case coderUint64:
return r.uvarint()
case coderString:
return r.string()
case coderMSG:
return r.msg()
case coderArrayMSG:
return r.arrayMsg()
default:
panic("db/leveldb: invalid coder " + strconv.Itoa(int(coder)))
}
}
func newReader(data string) (*reader, error) {
in := newIntReader(data)
v := in.uvarint()
if v != dataVersion {
return nil, errors.Errorf("db/leveldb: invalid data version %d", v)
}
sl := int64(in.uvarint())
dl := int64(in.uvarint())
whence, _ := in.Seek(0, io.SeekCurrent)
sData := data[whence : whence+sl]
dData := data[whence+sl : whence+sl+dl]
r := reader{
data: newIntReader(dData),
strings: newIntReader(sData),
stringIndex: make(map[uint64]string),
}
return &r, nil
}

View File

@ -1,175 +0,0 @@
package leveldb
import "github.com/Mrs4s/go-cqhttp/db"
func (w *writer) writeStoredGroupMessage(x *db.StoredGroupMessage) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.string(x.ID)
w.int32(x.GlobalID)
w.writeStoredMessageAttribute(x.Attribute)
w.string(x.SubType)
w.writeQuotedInfo(x.QuotedInfo)
w.int64(x.GroupCode)
w.string(x.AnonymousID)
w.arrayMsg(x.Content)
}
func (r *reader) readStoredGroupMessage() *db.StoredGroupMessage {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.StoredGroupMessage{}
x.ID = r.string()
x.GlobalID = r.int32()
x.Attribute = r.readStoredMessageAttribute()
x.SubType = r.string()
x.QuotedInfo = r.readQuotedInfo()
x.GroupCode = r.int64()
x.AnonymousID = r.string()
x.Content = r.arrayMsg()
return x
}
func (w *writer) writeStoredPrivateMessage(x *db.StoredPrivateMessage) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.string(x.ID)
w.int32(x.GlobalID)
w.writeStoredMessageAttribute(x.Attribute)
w.string(x.SubType)
w.writeQuotedInfo(x.QuotedInfo)
w.int64(x.SessionUin)
w.int64(x.TargetUin)
w.arrayMsg(x.Content)
}
func (r *reader) readStoredPrivateMessage() *db.StoredPrivateMessage {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.StoredPrivateMessage{}
x.ID = r.string()
x.GlobalID = r.int32()
x.Attribute = r.readStoredMessageAttribute()
x.SubType = r.string()
x.QuotedInfo = r.readQuotedInfo()
x.SessionUin = r.int64()
x.TargetUin = r.int64()
x.Content = r.arrayMsg()
return x
}
func (w *writer) writeStoredGuildChannelMessage(x *db.StoredGuildChannelMessage) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.string(x.ID)
w.writeStoredGuildMessageAttribute(x.Attribute)
w.uint64(x.GuildID)
w.uint64(x.ChannelID)
w.writeQuotedInfo(x.QuotedInfo)
w.arrayMsg(x.Content)
}
func (r *reader) readStoredGuildChannelMessage() *db.StoredGuildChannelMessage {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.StoredGuildChannelMessage{}
x.ID = r.string()
x.Attribute = r.readStoredGuildMessageAttribute()
x.GuildID = r.uint64()
x.ChannelID = r.uint64()
x.QuotedInfo = r.readQuotedInfo()
x.Content = r.arrayMsg()
return x
}
func (w *writer) writeStoredMessageAttribute(x *db.StoredMessageAttribute) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.int32(x.MessageSeq)
w.int32(x.InternalID)
w.int64(x.SenderUin)
w.string(x.SenderName)
w.int64(x.Timestamp)
}
func (r *reader) readStoredMessageAttribute() *db.StoredMessageAttribute {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.StoredMessageAttribute{}
x.MessageSeq = r.int32()
x.InternalID = r.int32()
x.SenderUin = r.int64()
x.SenderName = r.string()
x.Timestamp = r.int64()
return x
}
func (w *writer) writeStoredGuildMessageAttribute(x *db.StoredGuildMessageAttribute) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.uint64(x.MessageSeq)
w.uint64(x.InternalID)
w.uint64(x.SenderTinyID)
w.string(x.SenderName)
w.int64(x.Timestamp)
}
func (r *reader) readStoredGuildMessageAttribute() *db.StoredGuildMessageAttribute {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.StoredGuildMessageAttribute{}
x.MessageSeq = r.uint64()
x.InternalID = r.uint64()
x.SenderTinyID = r.uint64()
x.SenderName = r.string()
x.Timestamp = r.int64()
return x
}
func (w *writer) writeQuotedInfo(x *db.QuotedInfo) {
if x == nil {
w.nil()
return
}
w.coder(coderStruct)
w.string(x.PrevID)
w.int32(x.PrevGlobalID)
w.arrayMsg(x.QuotedContent)
}
func (r *reader) readQuotedInfo() *db.QuotedInfo {
coder := r.coder()
if coder == coderNil {
return nil
}
x := &db.QuotedInfo{}
x.PrevID = r.string()
x.PrevGlobalID = r.int32()
x.QuotedContent = r.arrayMsg()
return x
}

View File

@ -1,143 +0,0 @@
package leveldb
import (
"bytes"
"github.com/Mrs4s/go-cqhttp/global"
)
type intWriter struct {
bytes.Buffer
}
func (w *intWriter) varint(x int64) {
w.uvarint(uint64(x)<<1 ^ uint64(x>>63))
}
func (w *intWriter) uvarint(x uint64) {
for x >= 0x80 {
w.WriteByte(byte(x) | 0x80)
x >>= 7
}
w.WriteByte(byte(x))
}
// writer implements the index write.
//
// data format(use uvarint to encode integers):
//
// - version
// - string data length
// - index data length
// - string data
// - index data
//
// for string data part, each string is encoded as:
//
// - string length
// - string
//
// for index data part, each object value is encoded as:
//
// - coder
// - value
//
// * coder is the identifier of value's type.
// * specially for string, it's value is the offset in string data part.
type writer struct {
data intWriter
strings intWriter
stringIndex map[string]uint64
}
func newWriter() *writer {
return &writer{
stringIndex: make(map[string]uint64),
}
}
func (w *writer) coder(o coder) { w.data.WriteByte(byte(o)) }
func (w *writer) varint(x int64) { w.data.varint(x) }
func (w *writer) uvarint(x uint64) { w.data.uvarint(x) }
func (w *writer) nil() { w.coder(coderNil) }
func (w *writer) int(i int) { w.varint(int64(i)) }
func (w *writer) uint(i uint) { w.uvarint(uint64(i)) }
func (w *writer) int32(i int32) { w.varint(int64(i)) }
func (w *writer) uint32(i uint32) { w.uvarint(uint64(i)) }
func (w *writer) int64(i int64) { w.varint(i) }
func (w *writer) uint64(i uint64) { w.uvarint(i) }
func (w *writer) string(s string) {
off, ok := w.stringIndex[s]
if !ok {
// not found write to string data part
// | string length | string |
off = uint64(w.strings.Len())
w.strings.uvarint(uint64(len(s)))
_, _ = w.strings.WriteString(s)
w.stringIndex[s] = off
}
// write offset to index data part
w.uvarint(off)
}
func (w *writer) msg(m global.MSG) {
w.uvarint(uint64(len(m)))
for s, obj := range m {
w.string(s)
w.obj(obj)
}
}
func (w *writer) arrayMsg(a []global.MSG) {
w.uvarint(uint64(len(a)))
for _, v := range a {
w.msg(v)
}
}
func (w *writer) obj(o any) {
switch x := o.(type) {
case nil:
w.nil()
case int:
w.coder(coderInt)
w.int(x)
case int32:
w.coder(coderInt32)
w.int32(x)
case int64:
w.coder(coderInt64)
w.int64(x)
case uint:
w.coder(coderUint)
w.uint(x)
case uint32:
w.coder(coderUint32)
w.uint32(x)
case uint64:
w.coder(coderUint64)
w.uint64(x)
case string:
w.coder(coderString)
w.string(x)
case global.MSG:
w.coder(coderMSG)
w.msg(x)
case []global.MSG:
w.coder(coderArrayMSG)
w.arrayMsg(x)
default:
panic("unsupported type")
}
}
func (w *writer) bytes() []byte {
var out intWriter
out.uvarint(dataVersion)
out.uvarint(uint64(w.strings.Len()))
out.uvarint(uint64(w.data.Len()))
_, _ = w.strings.WriteTo(&out)
_, _ = w.data.WriteTo(&out)
return out.Bytes()
}

View File

@ -1,107 +0,0 @@
package mongodb
import (
"context"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"gopkg.in/yaml.v3"
"github.com/Mrs4s/go-cqhttp/db"
)
type database struct {
uri string
db string
mongo *mongo.Database
}
// config mongodb 相关配置
type config struct {
Enable bool `yaml:"enable"`
URI string `yaml:"uri"`
Database string `yaml:"database"`
}
const (
MongoGroupMessageCollection = "group-messages"
MongoPrivateMessageCollection = "private-messages"
MongoGuildChannelMessageCollection = "guild-channel-messages"
)
func init() {
db.Register("database", func(node yaml.Node) db.Database {
conf := new(config)
_ = node.Decode(conf)
if conf.Database == "" {
conf.Database = "gocq-database"
}
if !conf.Enable {
return nil
}
return &database{uri: conf.URI, db: conf.Database}
})
}
func (m *database) Open() error {
cli, err := mongo.Connect(context.Background(), options.Client().ApplyURI(m.uri))
if err != nil {
return errors.Wrap(err, "open mongo connection error")
}
m.mongo = cli.Database(m.db)
return nil
}
func (m *database) GetMessageByGlobalID(id int32) (db.StoredMessage, error) {
if r, err := m.GetGroupMessageByGlobalID(id); err == nil {
return r, nil
}
return m.GetPrivateMessageByGlobalID(id)
}
func (m *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) {
coll := m.mongo.Collection(MongoGroupMessageCollection)
var ret db.StoredGroupMessage
if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil {
return nil, errors.Wrap(err, "query error")
}
return &ret, nil
}
func (m *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) {
coll := m.mongo.Collection(MongoPrivateMessageCollection)
var ret db.StoredPrivateMessage
if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil {
return nil, errors.Wrap(err, "query error")
}
return &ret, nil
}
func (m *database) 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 *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
coll := m.mongo.Collection(MongoGroupMessageCollection)
_, 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 *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
coll := m.mongo.Collection(MongoPrivateMessageCollection)
_, 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 *database) 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")
}

View File

@ -1,105 +0,0 @@
package db
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
// backends 多数据库支持, 后端支持
// 写入会对所有 Backend 进行写入
// 读取只会读取第一个库
var backends []Database
// drivers 多数据库启动
var drivers = make(map[string]func(node yaml.Node) Database)
// DatabaseDisabledError 没有可用的db
var DatabaseDisabledError = errors.New("database disabled")
// Register 添加数据库后端
func Register(name string, init func(yaml.Node) Database) {
if _, ok := drivers[name]; ok {
panic("database driver conflict: " + name)
}
drivers[name] = init
}
// Init 加载所有后端配置文件
func Init() {
backends = make([]Database, 0, len(drivers))
for name, init := range drivers {
if n, ok := base.Database[name]; ok {
db := init(n)
if db != nil {
backends = append(backends, db)
}
}
}
}
func Open() error {
for _, b := range backends {
if err := b.Open(); err != nil {
return errors.Wrap(err, "open backend error")
}
}
base.Database = nil
return nil
}
func GetMessageByGlobalID(id int32) (StoredMessage, error) {
if len(backends) == 0 {
return nil, DatabaseDisabledError
}
return backends[0].GetMessageByGlobalID(id)
}
func GetGroupMessageByGlobalID(id int32) (*StoredGroupMessage, error) {
if len(backends) == 0 {
return nil, DatabaseDisabledError
}
return backends[0].GetGroupMessageByGlobalID(id)
}
func GetPrivateMessageByGlobalID(id int32) (*StoredPrivateMessage, error) {
if len(backends) == 0 {
return nil, DatabaseDisabledError
}
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 {
for _, b := range backends {
if err := b.InsertGroupMessage(m); err != nil {
return errors.Wrap(err, "insert message to backend error")
}
}
return nil
}
func InsertPrivateMessage(m *StoredPrivateMessage) error {
for _, b := range backends {
if err := b.InsertPrivateMessage(m); err != nil {
return errors.Wrap(err, "insert message to backend error")
}
}
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

@ -1,84 +0,0 @@
package sqlite3
const (
Sqlite3GroupMessageTableName = "grpmsg"
Sqlite3MessageAttributeTableName = "msgattr"
Sqlite3GuildMessageAttributeTableName = "gmsgattr"
Sqlite3QuotedInfoTableName = "quoinf"
Sqlite3PrivateMessageTableName = "privmsg"
Sqlite3GuildChannelMessageTableName = "guildmsg"
Sqlite3UinInfoTableName = "uininf"
Sqlite3TinyInfoTableName = "tinyinf"
)
// StoredMessageAttribute 持久化消息属性
type StoredMessageAttribute struct {
ID int64 // ID is the crc64 of 字段s below
MessageSeq int32
InternalID int32
SenderUin int64 // SenderUin is fk to UinInfo
Timestamp int64
}
// StoredGuildMessageAttribute 持久化频道消息属性
type StoredGuildMessageAttribute struct {
ID int64 // ID is the crc64 of 字段s below
MessageSeq int64
InternalID int64
SenderTinyID int64 // SenderTinyID is fk to TinyInfo
Timestamp int64
}
// QuotedInfo 引用回复
type QuotedInfo struct {
ID int64 // ID is the crc64 of 字段s below
PrevID string
PrevGlobalID int32
QuotedContent string // QuotedContent is json of original content
}
// UinInfo QQ 与 昵称
type UinInfo struct {
Uin int64
Name string
}
// TinyInfo Tiny 与 昵称
type TinyInfo struct {
ID int64
Name string
}
// StoredGroupMessage 持久化群消息
type StoredGroupMessage struct {
GlobalID int32
ID string
AttributeID int64
SubType string
QuotedInfoID int64
GroupCode int64
AnonymousID string
Content string // Content is json of original content
}
// StoredPrivateMessage 持久化私聊消息
type StoredPrivateMessage struct {
GlobalID int32
ID string
AttributeID int64
SubType string
QuotedInfoID int64
SessionUin int64
TargetUin int64
Content string // Content is json of original content
}
// StoredGuildChannelMessage 持久化频道消息
type StoredGuildChannelMessage struct {
ID string
AttributeID int64
GuildID int64
ChannelID int64
QuotedInfoID int64
Content string // Content is json of original content
}

View File

@ -1,538 +0,0 @@
package sqlite3
import (
"encoding/base64"
"hash/crc64"
"os"
"path"
"strconv"
"sync"
"time"
sql "github.com/FloatTech/sqlite"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/Mrs4s/go-cqhttp/db"
)
type database struct {
sync.RWMutex
db *sql.Sqlite
ttl time.Duration
}
// config mongodb 相关配置
type config struct {
Enable bool `yaml:"enable"`
CacheTTL string `yaml:"cachettl"`
}
func init() {
sql.DriverName = "sqlite"
db.Register("sqlite3", func(node yaml.Node) db.Database {
conf := new(config)
_ = node.Decode(conf)
if !conf.Enable {
return nil
}
duration, err := time.ParseDuration(conf.CacheTTL)
if err != nil {
log.Fatalf("illegal ttl config: %v", err)
}
return &database{db: new(sql.Sqlite), ttl: duration}
})
}
func (s *database) Open() error {
s.db.DBPath = path.Join("data", "sqlite3")
_ = os.MkdirAll(s.db.DBPath, 0755)
s.db.DBPath += "/msg.db"
err := s.db.Open(s.ttl)
if err != nil {
return errors.Wrap(err, "open sqlite3 error")
}
_, err = s.db.DB.Exec("PRAGMA foreign_keys = ON;")
if err != nil {
return errors.Wrap(err, "enable foreign_keys error")
}
err = s.db.Create(Sqlite3UinInfoTableName, &UinInfo{})
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Insert(Sqlite3UinInfoTableName, &UinInfo{Name: "null"})
if err != nil {
return errors.Wrap(err, "insert into sqlite3 table "+Sqlite3UinInfoTableName+" error")
}
err = s.db.Create(Sqlite3TinyInfoTableName, &TinyInfo{})
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Insert(Sqlite3TinyInfoTableName, &TinyInfo{Name: "null"})
if err != nil {
return errors.Wrap(err, "insert into sqlite3 table "+Sqlite3TinyInfoTableName+" error")
}
err = s.db.Create(Sqlite3MessageAttributeTableName, &StoredMessageAttribute{},
"FOREIGN KEY(SenderUin) REFERENCES "+Sqlite3UinInfoTableName+"(Uin)",
)
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Insert(Sqlite3MessageAttributeTableName, &StoredMessageAttribute{})
if err != nil {
return errors.Wrap(err, "insert into sqlite3 table "+Sqlite3MessageAttributeTableName+" error")
}
err = s.db.Create(Sqlite3GuildMessageAttributeTableName, &StoredGuildMessageAttribute{},
"FOREIGN KEY(SenderTinyID) REFERENCES "+Sqlite3TinyInfoTableName+"(ID)",
)
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Insert(Sqlite3GuildMessageAttributeTableName, &StoredGuildMessageAttribute{})
if err != nil {
return errors.Wrap(err, "insert into sqlite3 table "+Sqlite3GuildMessageAttributeTableName+" error")
}
err = s.db.Create(Sqlite3QuotedInfoTableName, &QuotedInfo{})
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Insert(Sqlite3QuotedInfoTableName, &QuotedInfo{QuotedContent: "null"})
if err != nil {
return errors.Wrap(err, "insert into sqlite3 table "+Sqlite3QuotedInfoTableName+" error")
}
err = s.db.Create(Sqlite3GroupMessageTableName, &StoredGroupMessage{},
"FOREIGN KEY(AttributeID) REFERENCES "+Sqlite3MessageAttributeTableName+"(ID)",
"FOREIGN KEY(QuotedInfoID) REFERENCES "+Sqlite3QuotedInfoTableName+"(ID)",
)
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Create(Sqlite3PrivateMessageTableName, &StoredPrivateMessage{},
"FOREIGN KEY(AttributeID) REFERENCES "+Sqlite3MessageAttributeTableName+"(ID)",
"FOREIGN KEY(QuotedInfoID) REFERENCES "+Sqlite3QuotedInfoTableName+"(ID)",
)
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
err = s.db.Create(Sqlite3GuildChannelMessageTableName, &StoredGuildChannelMessage{},
"FOREIGN KEY(AttributeID) REFERENCES "+Sqlite3MessageAttributeTableName+"(ID)",
"FOREIGN KEY(QuotedInfoID) REFERENCES "+Sqlite3QuotedInfoTableName+"(ID)",
)
if err != nil {
return errors.Wrap(err, "create sqlite3 table error")
}
return nil
}
func (s *database) GetMessageByGlobalID(id int32) (db.StoredMessage, error) {
if r, err := s.GetGroupMessageByGlobalID(id); err == nil {
return r, nil
}
return s.GetPrivateMessageByGlobalID(id)
}
func (s *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) {
var ret db.StoredGroupMessage
var grpmsg StoredGroupMessage
s.RLock()
err := s.db.Find(Sqlite3GroupMessageTableName, &grpmsg, "WHERE GlobalID="+strconv.Itoa(int(id)))
s.RUnlock()
if err != nil {
return nil, errors.Wrap(err, "query error")
}
ret.ID = grpmsg.ID
ret.GlobalID = grpmsg.GlobalID
ret.SubType = grpmsg.SubType
ret.GroupCode = grpmsg.GroupCode
ret.AnonymousID = grpmsg.AnonymousID
_ = yaml.Unmarshal(utils.S2B(grpmsg.Content), &ret)
if grpmsg.AttributeID != 0 {
var attr StoredMessageAttribute
s.RLock()
err = s.db.Find(Sqlite3MessageAttributeTableName, &attr, "WHERE ID="+strconv.FormatInt(grpmsg.AttributeID, 10))
s.RUnlock()
if err == nil {
var uin UinInfo
s.RLock()
err = s.db.Find(Sqlite3UinInfoTableName, &uin, "WHERE Uin="+strconv.FormatInt(attr.SenderUin, 10))
s.RUnlock()
if err == nil {
ret.Attribute = &db.StoredMessageAttribute{
MessageSeq: attr.MessageSeq,
InternalID: attr.InternalID,
SenderUin: attr.SenderUin,
SenderName: uin.Name,
Timestamp: attr.Timestamp,
}
}
}
}
if grpmsg.QuotedInfoID != 0 {
var quoinf QuotedInfo
s.RLock()
err = s.db.Find(Sqlite3QuotedInfoTableName, &quoinf, "WHERE ID="+strconv.FormatInt(grpmsg.QuotedInfoID, 10))
s.RUnlock()
if err == nil {
ret.QuotedInfo = &db.QuotedInfo{
PrevID: quoinf.PrevID,
PrevGlobalID: quoinf.PrevGlobalID,
}
_ = yaml.Unmarshal(utils.S2B(quoinf.QuotedContent), &ret.QuotedInfo)
}
}
return &ret, nil
}
func (s *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) {
var ret db.StoredPrivateMessage
var privmsg StoredPrivateMessage
s.RLock()
err := s.db.Find(Sqlite3PrivateMessageTableName, &privmsg, "WHERE GlobalID="+strconv.Itoa(int(id)))
s.RUnlock()
if err != nil {
return nil, errors.Wrap(err, "query error")
}
ret.ID = privmsg.ID
ret.GlobalID = privmsg.GlobalID
ret.SubType = privmsg.SubType
ret.SessionUin = privmsg.SessionUin
ret.TargetUin = privmsg.TargetUin
_ = yaml.Unmarshal(utils.S2B(privmsg.Content), &ret)
if privmsg.AttributeID != 0 {
var attr StoredMessageAttribute
s.RLock()
err = s.db.Find(Sqlite3MessageAttributeTableName, &attr, "WHERE ID="+strconv.FormatInt(privmsg.AttributeID, 10))
s.RUnlock()
if err == nil {
var uin UinInfo
s.RLock()
err = s.db.Find(Sqlite3UinInfoTableName, &uin, "WHERE Uin="+strconv.FormatInt(attr.SenderUin, 10))
s.RUnlock()
if err == nil {
ret.Attribute = &db.StoredMessageAttribute{
MessageSeq: attr.MessageSeq,
InternalID: attr.InternalID,
SenderUin: attr.SenderUin,
SenderName: uin.Name,
Timestamp: attr.Timestamp,
}
}
}
}
if privmsg.QuotedInfoID != 0 {
var quoinf QuotedInfo
s.RLock()
err = s.db.Find(Sqlite3QuotedInfoTableName, &quoinf, "WHERE ID="+strconv.FormatInt(privmsg.QuotedInfoID, 10))
s.RUnlock()
if err == nil {
ret.QuotedInfo = &db.QuotedInfo{
PrevID: quoinf.PrevID,
PrevGlobalID: quoinf.PrevGlobalID,
}
_ = yaml.Unmarshal(utils.S2B(quoinf.QuotedContent), &ret.QuotedInfo)
}
}
return &ret, nil
}
func (s *database) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) {
b, err := base64.StdEncoding.DecodeString(id)
if err != nil {
return nil, errors.Wrap(err, "query invalid id error")
}
if len(b) < 25 {
return nil, errors.New("query invalid id error: content too short")
}
var ret db.StoredGuildChannelMessage
var guildmsg StoredGuildChannelMessage
s.RLock()
err = s.db.Find(Sqlite3GuildChannelMessageTableName, &guildmsg, "WHERE ID='"+id+"'")
s.RUnlock()
if err != nil {
return nil, errors.Wrap(err, "query error")
}
ret.ID = guildmsg.ID
ret.GuildID = uint64(guildmsg.GuildID)
ret.ChannelID = uint64(guildmsg.ChannelID)
_ = yaml.Unmarshal(utils.S2B(guildmsg.Content), &ret)
if guildmsg.AttributeID != 0 {
var attr StoredGuildMessageAttribute
s.RLock()
err = s.db.Find(Sqlite3GuildMessageAttributeTableName, &attr, "WHERE ID="+strconv.FormatInt(guildmsg.AttributeID, 10))
s.RUnlock()
if err == nil {
var tiny TinyInfo
s.RLock()
err = s.db.Find(Sqlite3TinyInfoTableName, &tiny, "WHERE ID="+strconv.FormatInt(attr.SenderTinyID, 10))
s.RUnlock()
if err == nil {
ret.Attribute = &db.StoredGuildMessageAttribute{
MessageSeq: uint64(attr.MessageSeq),
InternalID: uint64(attr.InternalID),
SenderTinyID: uint64(attr.SenderTinyID),
SenderName: tiny.Name,
Timestamp: attr.Timestamp,
}
}
}
}
if guildmsg.QuotedInfoID != 0 {
var quoinf QuotedInfo
s.RLock()
err = s.db.Find(Sqlite3QuotedInfoTableName, &quoinf, "WHERE ID="+strconv.FormatInt(guildmsg.QuotedInfoID, 10))
s.RUnlock()
if err == nil {
ret.QuotedInfo = &db.QuotedInfo{
PrevID: quoinf.PrevID,
PrevGlobalID: quoinf.PrevGlobalID,
}
_ = yaml.Unmarshal(utils.S2B(quoinf.QuotedContent), &ret.QuotedInfo)
}
}
return &ret, nil
}
func (s *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
grpmsg := &StoredGroupMessage{
GlobalID: msg.GlobalID,
ID: msg.ID,
SubType: msg.SubType,
GroupCode: msg.GroupCode,
AnonymousID: msg.AnonymousID,
}
h := crc64.New(crc64.MakeTable(crc64.ISO))
if msg.Attribute != nil {
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.Attribute.MessageSeq))
w.WriteUInt32(uint32(msg.Attribute.InternalID))
w.WriteUInt64(uint64(msg.Attribute.SenderUin))
w.WriteUInt64(uint64(msg.Attribute.Timestamp))
}))
h.Write(utils.S2B(msg.Attribute.SenderName))
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err := s.db.Insert(Sqlite3UinInfoTableName, &UinInfo{
Uin: msg.Attribute.SenderUin,
Name: msg.Attribute.SenderName,
})
if err == nil {
err = s.db.Insert(Sqlite3MessageAttributeTableName, &StoredMessageAttribute{
ID: id,
MessageSeq: msg.Attribute.MessageSeq,
InternalID: msg.Attribute.InternalID,
SenderUin: msg.Attribute.SenderUin,
Timestamp: msg.Attribute.Timestamp,
})
}
s.Unlock()
if err == nil {
grpmsg.AttributeID = id
}
h.Reset()
}
if msg.QuotedInfo != nil {
h.Write(utils.S2B(msg.QuotedInfo.PrevID))
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.QuotedInfo.PrevGlobalID))
}))
content, err := yaml.Marshal(&msg.QuotedInfo)
if err != nil {
return errors.Wrap(err, "insert marshal QuotedContent error")
}
h.Write(content)
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err = s.db.Insert(Sqlite3QuotedInfoTableName, &QuotedInfo{
ID: id,
PrevID: msg.QuotedInfo.PrevID,
PrevGlobalID: msg.QuotedInfo.PrevGlobalID,
QuotedContent: utils.B2S(content),
})
s.Unlock()
if err == nil {
grpmsg.QuotedInfoID = id
}
}
content, err := yaml.Marshal(&msg)
if err != nil {
return errors.Wrap(err, "insert marshal Content error")
}
grpmsg.Content = utils.B2S(content)
s.Lock()
err = s.db.Insert(Sqlite3GroupMessageTableName, grpmsg)
s.Unlock()
if err != nil {
return errors.Wrap(err, "insert error")
}
return nil
}
func (s *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
privmsg := &StoredPrivateMessage{
GlobalID: msg.GlobalID,
ID: msg.ID,
SubType: msg.SubType,
SessionUin: msg.SessionUin,
TargetUin: msg.TargetUin,
}
h := crc64.New(crc64.MakeTable(crc64.ISO))
if msg.Attribute != nil {
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.Attribute.MessageSeq))
w.WriteUInt32(uint32(msg.Attribute.InternalID))
w.WriteUInt64(uint64(msg.Attribute.SenderUin))
w.WriteUInt64(uint64(msg.Attribute.Timestamp))
}))
h.Write(utils.S2B(msg.Attribute.SenderName))
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err := s.db.Insert(Sqlite3UinInfoTableName, &UinInfo{
Uin: msg.Attribute.SenderUin,
Name: msg.Attribute.SenderName,
})
if err == nil {
err = s.db.Insert(Sqlite3MessageAttributeTableName, &StoredMessageAttribute{
ID: id,
MessageSeq: msg.Attribute.MessageSeq,
InternalID: msg.Attribute.InternalID,
SenderUin: msg.Attribute.SenderUin,
Timestamp: msg.Attribute.Timestamp,
})
}
s.Unlock()
if err == nil {
privmsg.AttributeID = id
}
h.Reset()
}
if msg.QuotedInfo != nil {
h.Write(utils.S2B(msg.QuotedInfo.PrevID))
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.QuotedInfo.PrevGlobalID))
}))
content, err := yaml.Marshal(&msg.QuotedInfo)
if err != nil {
return errors.Wrap(err, "insert marshal QuotedContent error")
}
h.Write(content)
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err = s.db.Insert(Sqlite3QuotedInfoTableName, &QuotedInfo{
ID: id,
PrevID: msg.QuotedInfo.PrevID,
PrevGlobalID: msg.QuotedInfo.PrevGlobalID,
QuotedContent: utils.B2S(content),
})
s.Unlock()
if err == nil {
privmsg.QuotedInfoID = id
}
}
content, err := yaml.Marshal(&msg)
if err != nil {
return errors.Wrap(err, "insert marshal Content error")
}
privmsg.Content = utils.B2S(content)
s.Lock()
err = s.db.Insert(Sqlite3PrivateMessageTableName, privmsg)
s.Unlock()
if err != nil {
return errors.Wrap(err, "insert error")
}
return nil
}
func (s *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
guildmsg := &StoredGuildChannelMessage{
ID: msg.ID,
GuildID: int64(msg.GuildID),
ChannelID: int64(msg.ChannelID),
}
h := crc64.New(crc64.MakeTable(crc64.ISO))
if msg.Attribute != nil {
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.Attribute.MessageSeq))
w.WriteUInt32(uint32(msg.Attribute.InternalID))
w.WriteUInt64(uint64(msg.Attribute.SenderTinyID))
w.WriteUInt64(uint64(msg.Attribute.Timestamp))
}))
h.Write(utils.S2B(msg.Attribute.SenderName))
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err := s.db.Insert(Sqlite3TinyInfoTableName, &TinyInfo{
ID: int64(msg.Attribute.SenderTinyID),
Name: msg.Attribute.SenderName,
})
if err == nil {
err = s.db.Insert(Sqlite3MessageAttributeTableName, &StoredGuildMessageAttribute{
ID: id,
MessageSeq: int64(msg.Attribute.MessageSeq),
InternalID: int64(msg.Attribute.InternalID),
SenderTinyID: int64(msg.Attribute.SenderTinyID),
Timestamp: msg.Attribute.Timestamp,
})
}
s.Unlock()
if err == nil {
guildmsg.AttributeID = id
}
h.Reset()
}
if msg.QuotedInfo != nil {
h.Write(utils.S2B(msg.QuotedInfo.PrevID))
h.Write(binary.NewWriterF(func(w *binary.Writer) {
w.WriteUInt32(uint32(msg.QuotedInfo.PrevGlobalID))
}))
content, err := yaml.Marshal(&msg.QuotedInfo)
if err != nil {
return errors.Wrap(err, "insert marshal QuotedContent error")
}
h.Write(content)
id := int64(h.Sum64())
if id == 0 {
id++
}
s.Lock()
err = s.db.Insert(Sqlite3QuotedInfoTableName, &QuotedInfo{
ID: id,
PrevID: msg.QuotedInfo.PrevID,
PrevGlobalID: msg.QuotedInfo.PrevGlobalID,
QuotedContent: utils.B2S(content),
})
s.Unlock()
if err == nil {
guildmsg.QuotedInfoID = id
}
}
content, err := yaml.Marshal(&msg)
if err != nil {
return errors.Wrap(err, "insert marshal Content error")
}
guildmsg.Content = utils.B2S(content)
s.Lock()
err = s.db.Insert(Sqlite3GuildChannelMessageTableName, guildmsg)
s.Unlock()
if err != nil {
return errors.Wrap(err, "insert error")
}
return nil
}

View File

@ -1,20 +0,0 @@
#!/bin/sh
USER=abc
echo "---Setup Timezone to ${TZ}---"
echo "${TZ}" > /etc/timezone
echo "---Checking if UID: ${UID} matches user---"
usermod -o -u ${UID} ${USER}
echo "---Checking if GID: ${GID} matches user---"
groupmod -o -g ${GID} ${USER} > /dev/null 2>&1 ||:
usermod -g ${GID} ${USER}
echo "---Setting umask to ${UMASK}---"
umask ${UMASK}
echo "---Taking ownership of data...---"
chown -R ${UID}:${GID} /app /data
chmod +x /app/cqhttp
echo "Starting..."
su-exec ${USER} /app/cqhttp

View File

@ -1,12 +1,10 @@
# 事件过滤器
配置文件填写对应通信方式的 `middlewares.filter` 即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则不会启用事件过滤器。
go-cqhttp同级目录下新建`filter.json`文件即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则不会启用事件过滤器。
事件过滤器会处理所有事件(包括心跳事件在内的元事件),请谨慎使用!!
注意: 与客户端建立连接的握手事件**不会**经过事件过滤器
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
## 示例
这节首先给出一些示例,演示过滤器的基本用法,下一节将给出具体语法说明。
@ -116,35 +114,6 @@
}
```
## 进阶指南
1. 对于嵌套的值,可以使用 `.` 进行简化,如
```json
{
"sender": {
"sex": "male"
}
}
```
与下面的配置文件作用相同
```json
{
"sender.sex": "male"
}
```
2. 对于数组,可以使用数字索引,如
```json
{
"message.0.type": "text"
}
```
更多进阶语法请参考[GJSON语法](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)
## 语法说明
过滤规则最外层是一个 JSON 对象,其中的键,如果以 `.`(点号)开头,则表示运算符,其值为运算符的参数,如果不以 `.` 开头,则表示对事件数据对象中相应键的过滤。过滤规则中任何一个对象,只有在它的所有项都匹配的情况下,才会让事件通过(等价于一个 `and` 运算);其中,不以 `.` 开头的键,若其值不是对象,则只有在这个值和事件数据相应值相等的情况下,才会通过(等价于一个 `eq` 运算符)。
@ -165,9 +134,9 @@
## 过滤时的事件数据对象
过滤器在go-cqhttp构建好事件数据后运行各事件的数据字段见[OneBot标准]( https://github.com/botuniverse/onebot-11/blob/master/event/README.md )。
过滤器在go-cqhttp构建好事件数据后运行各事件的数据字段见[OneBot标准]( https://github.com/howmanybots/onebot/blob/master/v11/specs/event/README.md )。
这里有几点需要注意:
- `message` 字段在运行过滤器时和上报信息类型相同(见 [消息格式]( https://github.com/botuniverse/onebot-11/blob/master/message/array.md )
- `message` 字段在运行过滤器时和上报信息类型相同(见 [消息格式]( https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md )
- `raw_message` 字段为未经**CQ码**处理的原始消息字符串,这意味着其中可能会出现形如 `[CQ:face,id=123]` 的 CQ 码

View File

@ -1,7 +1,5 @@
# 常见问题
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
### Q: 为什么挂一段时间后就会出现 `消息发送失败,账号可能被风控`?
### A: 如果你刚开始使用 go-cqhttp 建议挂机3-7天即可解除风控

View File

@ -1,12 +0,0 @@
# 文档
> 文档目前依旧保留以便往前兼容
\
下面的文档更易读以及人性化, 强烈建议您查看下面提供的文档
目前文档已移动到位于 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs) 的仓库
您可以在以下其中任意一个链接查看:
- <https://docs.go-cqhttp.org>
- <https://ishkong.github.io/go-cqhttp-docs>

View File

@ -1,201 +1,102 @@
# 配置
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
go-cqhttp 包含 `config.hjson``device.json` 两个配置文件, 其中 `config.json` 为运行配置 `device.json` 为虚拟设备信息.
go-cqhttp 包含 `config.yml``device.json` 两个配置文件, 其中 `config.yml` 为运行配置 `device.json` 为虚拟设备信息.
## 从原CQHTTP导入配置
go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
1. 找到CQHTTP原配置文件 `{CQ工作目录}/app/io.github.richardchien.coolqhttpapi/config/{qq号}.json`
2. 将文件复制到go-cqhttp根目录并重命名为 `cqhttp.json`
3. 重启go-cqhttp后将自动导入配置
## 配置信息
go-cqhttp 的配置文件采用 YAML , 在使用之前希望你能了解 YAML 的语法([教程](https://www.runoob.com/w3cnote/yaml-intro.html))
默认生成的配置文件如下所示:
````yaml
# go-cqhttp 默认配置文件
account: # 账号相关
uin: 1233456 # QQ账号
password: '' # 密码为空时使用扫码登录
encrypt: false # 是否开启密码加密
status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
relogin: # 重连设置
delay: 3 # 首次重连延迟, 单位秒
interval: 3 # 重连间隔
max-times: 0 # 最大重连次数, 0为无限制
# 是否使用服务器下发的新地址进行重连
# 注意, 此设置可能导致在海外服务器上连接情况更差
use-sso-address: true
heartbeat:
# 心跳频率, 单位秒
# -1 为关闭心跳
interval: 5
message:
# 上报数据类型
# 可选: string,array
post-format: string
# 是否忽略无效的CQ码, 如果为假将原样发送
ignore-invalid-cqcode: false
# 是否强制分片发送消息
# 分片发送将会带来更快的速度
# 但是兼容性会有些问题
force-fragment: false
# 是否将url分片发送
fix-url: false
# 下载图片等请求网络代理
proxy-rewrite: ''
# 是否上报自身消息
report-self-message: false
# 移除服务端的Reply附带的At
remove-reply-at: false
# 为Reply附加更多信息
extra-reply-data: false
# 跳过 Mime 扫描, 忽略错误数据
skip-mime-scan: false
output:
# 日志等级 trace,debug,info,warn,error
log-level: warn
# 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留.
log-aging: 15
# 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写
log-force-new: true
# 是否启用 DEBUG
debug: false # 开启调试模式
# 默认中间件锚点
default-middlewares: &default
# 访问密钥, 强烈推荐在公网的服务器设置
access-token: ''
# 事件过滤器文件目录
filter: ''
# API限速设置
# 该设置为全局生效
# 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
# 目前该限速设置为令牌桶算法, 请参考:
# https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
rate-limit:
enabled: false # 是否启用限速
frequency: 1 # 令牌回复频率, 单位秒
bucket: 1 # 令牌桶大小
# 连接服务列表
servers:
# HTTP 通信设置
- http:
# 服务端监听地址
# 如需指定监听ipv4 可使用 `address: tcp4://0.0.0.0:5700` (ipv6同理)
address: 0.0.0.0:5700
# 反向HTTP超时时间, 单位秒
# 最小值为5小于5将会忽略本项设置
timeout: 5
middlewares:
<<: *default # 引用默认中间件
# 反向HTTP POST地址列表
post:
#- url: '' # 地址
# secret: '' # 密钥
#- url: 127.0.0.1:5701 # 地址
# secret: '' # 密钥
# 正向WS设置
- ws:
# 正向WS服务器监听地址
# 如需指定监听ipv4 可使用 `address: tcp4://0.0.0.0:6700` (ipv6同理)
address: 0.0.0.0:6700
middlewares:
<<: *default # 引用默认中间件
- 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 # 引用默认中间件
# pprof 性能分析服务器, 一般情况下不需要启用.
# 如果遇到性能问题请上传报告给开发者处理
# 注意: pprof服务不支持中间件、不支持鉴权. 请不要开放到公网
- pprof:
# pprof服务器监听地址
host: 127.0.0.1
# pprof服务器监听端口
port: 7700
# LambdaServer 配置
- lambda:
type: scf # 可用 scf,aws (aws未经过测试)
middlewares:
<<: *default # 引用默认中间件
# 可添加更多
#- ws-reverse:
#- ws:
#- http:
database: # 数据库相关设置
leveldb:
# 是否启用内置leveldb数据库
# 启用将会增加10-20MB的内存占用和一定的磁盘空间
# 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable: true
````json
{
"uin": 0,
"password": "",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"enable_self_message": false,
"access_token": "",
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"_rate_limit": {
"enabled": false,
"frequency": 1,
"bucket_size": 1
},
"post_message_format": "string",
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 5,
"use_sso_address": false,
"http_config": {
"enabled": true,
"host": "0.0.0.0",
"port": 5700,
"timeout": 5,
"post_urls": {
"url:port": "secret"
}
},
"ws_config": {
"enabled": true,
"host": "0.0.0.0",
"port": 6700
},
"ws_reverse_servers": [
{
"enabled": false,
"reverse_url": "ws://you_websocket_universal.server",
"reverse_api_url": "ws://you_websocket_api.server",
"reverse_event_url": "ws://you_websocket_event.server",
"reverse_reconnect_interval": 3000
}
]
}
````
> 注1: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码的哈希将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
| 字段 | 类型 | 说明 |
| --------------------- | -------- | ---------------------------------------------------------------------------------------- |
| uin | int64 | 登录用QQ号 |
| password | string | 登录用密码 |
| encrypt_password | bool | 是否对密码进行加密. |
| password_encrypted | string | 加密后的密码(请勿修改) |
| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
| enable_self_message | bool | 是否启用 `message_sent` 事件 |
| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
| relogin | bool | 是否自动重新登录 |
| relogin_delay | int | 重登录延时(秒) |
| max_relogin_times | uint | 最大重登录次数若0则不设置上限 |
| _rate_limit | bool | 是否启用API调用限速 |
| frequency | float64 | 1s内能调用API的次数 |
| bucket_size | int | 令牌桶的大小默认为1修改此值可允许一定程度内连续调用api |
| post_message_format | string | 上报信息类型 |
| ignore_invalid_cqcode | bool | 是否忽略错误的CQ码 |
| force_fragmented | bool | 是否强制分片发送群长消息 |
| fix_url | bool | 是否对链接的发送进行预处理, 可缓解链接信息被风控导致无法发送的情况, 但可能影响客户端着色(不影响内容)|
| use_sso_address | bool | 是否使用服务器下发的地址 |
| heartbeat_interval | int64 | 心跳间隔时间单位秒。小于0则关闭心跳等于0使用默认值(5秒) |
| http_config | object | HTTP API配置 |
| ws_config | object | Websocket API 配置 |
| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
| log_level | string | 指定日志收集级别,将收集的日志单独存放到固定文件中,便于查看日志线索 当前支持 warn,error |
> 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
> 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥
> 注2: 对于不需要的通信方式,你可以使用注释将其停用(推荐),或者添加配置 `disabled: true` 将其关闭
> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
> 注3: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
> 注4关闭心跳服务可能引起断线请谨慎关闭
> 注5关于MIME扫描 详见[MIME](file.md#MIME)
### 环境变量
go-cqhttp 配置文件可以使用占位符来读取**环境变量**的值。
```yaml
account: # 账号相关
uin: ${CQ_UIN} # 读取环境变量 CQ_UIN
password: ${CQ_PWD:123456} # 当 CQ_PWD 为空时使用默认值 123456
```
## 在线状态
| 状态 | 值 |
| -----|----|
| 在线 | 0 |
| 离开 | 1 |
| 隐身 | 2 |
| 忙 | 3 |
| 听歌中 | 4 |
| 星座运势 | 5 |
| 今日天气 | 6 |
| 遇见春天 | 7 |
| Timi中 | 8 |
| 吃鸡中 | 9 |
| 恋爱中 | 10 |
| 汪汪汪 | 11 |
| 干饭中 | 12 |
| 学习中 | 13 |
| 熬夜中 | 14 |
| 打球中 | 15 |
| 信号弱 | 16 |
| 在线学习 | 17 |
| 游戏中 | 18 |
| 度假中 | 19 |
| 追剧中 | 20 |
| 健身中 | 21 |
> 注3:关闭心跳服务可能引起断线,请谨慎关闭
## 设备信息
@ -220,7 +121,6 @@ account: # 账号相关
| 1 | Android Phone | 无 |
| 2 | Android Watch | 无法接收 `notify` 事件、无法接收口令红包、无法接收撤回消息 |
| 3 | MacOS | 无 |
| 4 | 企点 | 只能登录企点账号或企点子账号 |
> 注意, 根据协议的不同, 各类消息有所限制
@ -236,14 +136,3 @@ account: # 账号相关
1.1.1.1:53
1.1.2.2:8899
````
## 云函数部署
使用CustomRuntime进行部署 bootstrap 文件在 `scripts/bootstrap` 中已给出。
在部署前,请在本地完成登录,并将 `config.yml` `device.json` `bootstrap` 和 `go-cqhttp`
一起打包。
在触发器中创建一个API网关触发器并启用集成响应创建完成后即可通过api网关访问go-cqhttp(建议配置 AccessToken)。
> scripts/bootstrap 中使用的工作路径为 /tmp, 这个目录最大能容下500M文件, 如需长期使用,
> 请挂载文件存储(CFS).

View File

@ -1,19 +1,17 @@
# 拓展API
由于部分 api 原版 CQHTTP 并未实现go-cqhttp 修改并增加了一些拓展 api
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足..
由于部分 api 原版 CQHTTP 并未实现go-cqhttp 修改并增加了一些拓展 api .
<details>
<summary>目录</summary>
<p>
##### CQCode
- [图片](#图片)
- [回复](#回复)
- [红包](#红包)
- [戳一戳](#戳一戳)
- [礼物](#礼物)
- [合并转发](#合并转发)
- [合并转发消息节点](#合并转发消息节点)
- [XML 消息](#xml-消息)
@ -23,7 +21,6 @@
- [图片](#图片)
##### API
- [设置群名](#设置群名)
- [设置群头像](#设置群头像)
- [获取图片信息](#获取图片信息)
@ -44,15 +41,12 @@
- [设置群名](#设置群名)
- [获取用户VIP信息](#获取用户vip信息)
- [发送群公告](#发送群公告)
- [获取群公告](#获取群公告)
- [删除群公告](#删除群公告)
- [设置精华消息](#设置精华消息)
- [移出精华消息](#移出精华消息)
- [获取精华消息列表](#获取精华消息列表)
- [重载事件过滤器](#重载事件过滤器)
##### 事件
- [群消息撤回](#群消息撤回)
- [好友消息撤回](#好友消息撤回)
- [好友戳一戳](#好友戳一戳)
@ -78,13 +72,12 @@ Type : `image`
| 参数名 | 可能的值 | 说明 |
| ------- | --------------- | ---------------------------------------------------------------------- |
| `file` | - | 图片文件名 |
| `type` | `flash``show` | 图片类型,`flash` 表示闪照,`show` 表示秀图,默认普通图片 |
| `subType`| - | 图片子类型, 只出现在群聊. |
| `url` | - | 图片 URL |
| `cache` | `0` `1` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `1` |
| `id` | - | 发送秀图时的特效id默认为40000 |
| `c` | `2` `3` | 通过网络下载图片时的线程数, 默认单线程. (在资源不支持并发时会自动处理) |
| `file` | - | 图片文件名 |
| `type` | `flash``show` | 图片类型,`flash` 表示闪照,`show` 表示秀图,默认普通图片 |
| `url` | - | 图片 URL |
| `cache` | `0` `1` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `1` |
| `id` | - | 发送秀图时的特效id默认为40000 |
| `c` | `2` `3` | 通过网络下载图片时的线程数, 默认单线程. (在资源不支持并发时会自动处理) |
可用的特效ID:
@ -97,21 +90,6 @@ Type : `image`
| 40004 | 爱你 |
| 40005 | 征友 |
子类型列表:
| value | 说明 |
| ----- | ---- |
| 0 | 正常图片 |
| 1 | 表情包, 在客户端会被分类到表情包图片并缩放显示 |
| 2 | 热图 |
| 3 | 斗图 |
| 4 | 智图? |
| 7 | 贴图 |
| 8 | 自拍 |
| 9 | 贴图广告? |
| 10 | 有待测试 |
| 13 | 热搜图 |
示例: `[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]`
> 注意图片总大小不能超过30MBgif总帧数不能超过300帧
@ -134,6 +112,8 @@ Type : `reply`
| `time` | int64 | 可选. 自定义回复时的时间, 格式为Unix时间 |
| `seq` | int64 | 起始消息序号, 可通过 `get_msg` 获得 |
示例: `[CQ:reply,id=123456]`
\
自定义回复示例: `[CQ:reply,text=Hello World,qq=10086,time=3376656000,seq=5123]`
@ -142,11 +122,11 @@ Type : `reply`
```json
{
"type": "music",
"data": {
"type": "163",
"id": "28949129"
}
"type": "music",
"data": {
"type": "163",
"id": "28949129"
}
}
```
@ -163,13 +143,13 @@ Type : `reply`
```json
{
"type": "music",
"data": {
"type": "custom",
"url": "http://baidu.com",
"audio": "http://baidu.com/1.mp3",
"title": "音乐标题"
}
"type": "music",
"data": {
"type": "custom",
"url": "http://baidu.com",
"audio": "http://baidu.com/1.mp3",
"title": "音乐标题"
}
}
```
@ -217,7 +197,45 @@ Type: `poke`
示例: `[CQ:poke,qq=123456]`
### 合并转发
### 礼物
> 注意:仅支持免费礼物,发送群礼物消息无法撤回,返回的 `message id` 恒定为 `0`
Type: `gift`
范围: **发送(仅群聊,接收的时候不是CQ码)**
参数:
| 参数名 | 类型 | 说明 |
| ------ | ----- | -------------- |
| `qq` | int64 | 接收礼物的成员 |
| `id` | int | 礼物的类型 |
目前支持的礼物ID:
| id | 类型 |
| --- | ---------- |
| 0 | 甜Wink |
| 1 | 快乐肥宅水 |
| 2 | 幸运手链 |
| 3 | 卡布奇诺 |
| 4 | 猫咪手表 |
| 5 | 绒绒手套 |
| 6 | 彩虹糖果 |
| 7 | 坚强 |
| 8 | 告白话筒 |
| 9 | 牵你的手 |
| 10 | 可爱猫咪 |
| 11 | 神秘面具 |
| 12 | 我超忙的 |
| 13 | 爱心口罩 |
示例: `[CQ:gift,qq=123456,id=8]`
### 合并转发
Type: `forward`
@ -241,15 +259,13 @@ Type: `node`
| 参数名 | 类型 | 说明 | 特殊说明 |
| --------- | ------- | -------------- | -------------------------------------------------------------------------------------- |
| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** |
| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** |
| `name` | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) |
| `uin` | int64 | 发送者QQ号 | 用于自定义消息 |
| `content` | message | 具体消息 | 用于自定义消息 |
| `seq` | message | 具体消息 | 用于自定义消息 |
特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送并且由于消息段较为复杂仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序.
另外按 [Onebot v11](https://github.com/botuniverse/onebot-11/blob/master/message/array.md) 文档说明, `data` 应全为字符串,
但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃**
特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送并且由于消息段较为复杂仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. 另外按 [CQHTTP](https://git.io/JtxtN) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃**
示例:
@ -257,18 +273,18 @@ Type: `node`
````json
[
{
"type": "node",
"data": {
"id": "123"
{
"type": "node",
"data": {
"id": "123"
}
},
{
"type": "node",
"data": {
"id": "456"
}
}
},
{
"type": "node",
"data": {
"id": "456"
}
}
]
````
@ -276,29 +292,27 @@ Type: `node`
````json
[
{
"type": "node",
"data": {
"name": "消息发送者A",
"uin": "10086",
"content": [
{
"type": "text",
"data": {
"text": "测试消息1"
}
{
"type": "node",
"data": {
"name": "消息发送者A",
"uin": "10086",
"content": [
{
"type": "text",
"data": {"text": "测试消息1"}
}
]
}
},
{
"type": "node",
"data": {
"name": "消息发送者B",
"uin": "10087",
"content": "[CQ:image,file=xxxxx]测试消息2"
}
]
}
},
{
"type": "node",
"data": {
"name": "消息发送者B",
"uin": "10087",
"content": "[CQ:image,file=xxxxx]测试消息2"
}
}
]
````
@ -306,25 +320,24 @@ Type: `node`
````json
[
{
"type": "node",
"data": {
"name": "自定义发送者",
"uin": "10086",
"content": "我是自定义消息",
"seq": "5123",
"time": "3376656000"
{
"type": "node",
"data": {
"name": "自定义发送者",
"uin": "10086",
"content": "我是自定义消息",
"seq": "5123",
"time": "3376656000"
}
},
{
"type": "node",
"data": {
"id": "123"
}
}
},
{
"type": "node",
"data": {
"id": "123"
}
}
]
````
### 短视频消息
Type: `video`
@ -338,8 +351,7 @@ Type: `video`
| `file` | string | 支持http和file发送 |
| `cover` | string | 视频封面支持httpfile和base64发送格式必须为jpg |
| `c` | `2` `3` | 通过网络下载视频时的线程数, 默认单线程. (在资源不支持并发时会自动处理) |
示例: `[CQ:video,file=file:///C:\\Users\Richard\Videos\1.mp4]`
示例: `[CQ:image,file=file:///C:\\Users\Richard\Pictures\1.mp4]`
### XML 消息
@ -363,61 +375,30 @@ Type: `xml`
#### qq音乐
```xml
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0"
url="https://i.y.qq.com/v8/playsong.html?_wv=1&amp;songid=4830342&amp;souce=qqshare&amp;source=qqshare&amp;ADTAG=qqshare"
flag="0" adverSign="0" multiMsgFlag="0">
<item layout="2">
<audio cover="http://imgcache.qq.com/music/photo/album_500/26/500_albumpic_89526_0.jpg"
src="http://ws.stream.qqmusic.qq.com/C400003mAan70zUy5O.m4a?guid=1535153710&amp;vkey=D5315B8C0603653592AD4879A8A3742177F59D582A7A86546E24DD7F282C3ACF81526C76E293E57EA1E42CF19881C561275D919233333ADE&amp;uin=&amp;fromtag=3"/>
<title>十年</title>
<summary>陈奕迅</summary>
</item>
<source name="QQ音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png"
url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app"
a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856"/>
</msg>
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0" url="https://i.y.qq.com/v8/playsong.html?_wv=1&amp;songid=4830342&amp;souce=qqshare&amp;source=qqshare&amp;ADTAG=qqshare" flag="0" adverSign="0" multiMsgFlag="0" ><item layout="2"><audio cover="http://imgcache.qq.com/music/photo/album_500/26/500_albumpic_89526_0.jpg" src="http://ws.stream.qqmusic.qq.com/C400003mAan70zUy5O.m4a?guid=1535153710&amp;vkey=D5315B8C0603653592AD4879A8A3742177F59D582A7A86546E24DD7F282C3ACF81526C76E293E57EA1E42CF19881C561275D919233333ADE&amp;uin=&amp;fromtag=3" /><title>十年</title><summary>陈奕迅</summary></item><source name="QQ音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>
```
#### 网易音乐
```xml
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0"
url="http://music.163.com/m/song/409650368" flag="0" adverSign="0" multiMsgFlag="0">
<item layout="2">
<audio cover="http://p2.music.126.net/g-Qgb9ibk9Wp_0HWra0xQQ==/16636710440565853.jpg?param=90y90"
src="https://music.163.com/song/media/outer/url?id=409650368.mp3"/>
<title>十年</title>
<summary>黄梦之</summary>
</item>
<source name="网易云音乐" icon="https://pic.rmb.bdstatic.com/911423bee2bef937975b29b265d737b3.png"
url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app"
a_actionData="com.netease.cloudmusic" i_actionData="tencent100495085://" appid="100495085"/>
</msg>
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0" url="http://music.163.com/m/song/409650368" flag="0" adverSign="0" multiMsgFlag="0" ><item layout="2"><audio cover="http://p2.music.126.net/g-Qgb9ibk9Wp_0HWra0xQQ==/16636710440565853.jpg?param=90y90" src="https://music.163.com/song/media/outer/url?id=409650368.mp3" /><title>十年</title><summary>黄梦之</summary></item><source name="网易云音乐" icon="https://pic.rmb.bdstatic.com/911423bee2bef937975b29b265d737b3.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.netease.cloudmusic" i_actionData="tencent100495085://" appid="100495085" /></msg>
```
#### 卡片消息1
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<msg serviceID="1">
<item>
<title>生死8秒女司机高速急刹他一个操作救下一车性命</title>
</item>
<source name="官方认证消息" icon="https://qzs.qq.com/ac/qzone_v5/client/auth_icon.png" action="" appid="-1"/>
<item><title>生死8秒女司机高速急刹他一个操作救下一车性命</title></item>
<source name="官方认证消息" icon="https://qzs.qq.com/ac/qzone_v5/client/auth_icon.png" action="" appid="-1" />
</msg>
```
#### 卡片消息2
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<msg serviceID="1">
<item layout="4">
<title>test title</title>
<picture cover="http://url.cn/5CEwIUy"/>
</item>
<item layout="4">
<title>test title</title>
<picture cover="http://url.cn/5CEwIUy"/>
</item>
</msg>
```
@ -436,24 +417,23 @@ Type: `json`
json中的字符串需要进行转义
> ","=> `&#44;`
>","=> `&#44;`
> "&"=> `&amp;`
>"&"=> `&amp;`
> "["=> `&#91;`
>"["=> `&#91;`
> "]"=> `&#93;`
>"]"=> `&#93;`
否则无法正确得到解析
示例json 的cq码
```test
[CQ:json,data={"app":"com.tencent.miniapp"&#44;"desc":""&#44;"view":"notification"&#44;"ver":"0.0.0.1"&#44;"prompt":"&#91;应用&#93;"&#44;"appID":""&#44;"sourceName":""&#44;"actionData":""&#44;"actionData_A":""&#44;"sourceUrl":""&#44;"meta":{"notification":{"appInfo":{"appName":"全国疫情数据统计"&#44;"appType":4&#44;"appid":1109659848&#44;"iconUrl":"http:\/\/gchat.qpic.cn\/gchatpic_new\/719328335\/-2010394141-6383A777BEB79B70B31CE250142D740F\/0"}&#44;"data":&#91;{"title":"确诊"&#44;"value":"80932"}&#44;{"title":"今日确诊"&#44;"value":"28"}&#44;{"title":"疑似"&#44;"value":"72"}&#44;{"title":"今日疑似"&#44;"value":"5"}&#44;{"title":"治愈"&#44;"value":"60197"}&#44;{"title":"今日治愈"&#44;"value":"1513"}&#44;{"title":"死亡"&#44;"value":"3140"}&#44;{"title":"今**亡"&#44;"value":"17"}&#93;&#44;"title":"中国加油,武汉加油"&#44;"button":&#91;{"name":"病毒SARS-CoV-2其导致疾病命名 COVID-19"&#44;"action":""}&#44;{"name":"传染源:新冠肺炎的患者。无症状感染者也可能成为传染源。"&#44;"action":""}&#93;&#44;"emphasis_keyword":""}}&#44;"text":""&#44;"sourceAd":""}]
```
### cardimage
### cardimage
一种xml的图片消息装逼大图
ps: xml 接口的消息都存在风控风险,请自行兼容发送失败后的处理(可以失败后走普通图片模式)
@ -474,8 +454,8 @@ Type: `cardimage`
| `source` | string | 分享来源的名称,可以留空 |
| `icon` | string | 分享来源的icon图标url可以留空 |
示例cardimage 的cq码
示例cardimage 的cq码
```test
[CQ:cardimage,file=https://i.pixiv.cat/img-master/img/2020/03/25/00/00/08/80334602_p0_master1200.jpg]
```
@ -496,18 +476,6 @@ Type: `tts`
示例: `[CQ:tts,text=这是一条测试消息]`
### 猜拳消息
Type: `rps`
参数:
| 参数名 | 类型 | 说明 |
|---------|-----|------------------|
| `value` | int | 0石头, 1剪刀, 2布 |
示例: `[CQ:rps,value=0]`
## API
### 设置群名
@ -537,8 +505,7 @@ Type: `rps`
- 绝对路径,例如 `file:///C:\\Users\Richard\Pictures\1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089)
- 网络 URL例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg`
- Base64
编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
[2]`cache`参数: 通过网络 URL 发送时有效,`1`表示使用缓存,`0`关闭关闭缓存,默认 为`1`
@ -604,42 +571,40 @@ Type: `rps`
````json5
{
"data": {
"messages": [
{
"content": "合并转发1",
"sender": {
"nickname": "发送者A",
"user_id": 10086
},
"time": 1595694374
},
{
"content": "合并转发2[CQ:image,file=xxxx,url=xxxx]",
"sender": {
"nickname": "发送者B",
"user_id": 10087
},
"time": 1595694393
// 可选
}
]
},
"retcode": 0,
"status": "ok"
"data": {
"messages": [
{
"content": "合并转发1",
"sender": {
"nickname": "发送者A",
"user_id": 10086
},
"time": 1595694374
},
{
"content": "合并转发2[CQ:image,file=xxxx,url=xxxx]",
"sender": {
"nickname": "发送者B",
"user_id": 10087
},
"time": 1595694393 // 可选
}
]
},
"retcode": 0,
"status": "ok"
}
````
### 发送合并转发(群/私聊)
### 发送合并转发(群)
终结点: `/send_group_forward_msg`, `send_private_forward_msg`, `send_forward_msg`
终结点: `/send_group_forward_msg`
**参数**
| 字段 | 类型 | 说明 |
|------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `group_id` | int64 | 群号 |
| `user_id` | int64 | 私聊QQ号 |
| 字段 | 类型 | 说明 |
| ---------- | -------------- | ---------------------------- |
| `group_id` | int64 | 群号 |
| `messages` | forward node[] | 自定义转发消息, 具体看 [CQCode](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/cqhttp.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9) |
响应数据
@ -743,6 +708,7 @@ Type: `rps`
| `confidence` | int32 | 置信度 |
| `coordinates` | vector2 | 坐标 |
### 获取群系统消息
终结点: `/get_group_system_msg`
@ -754,9 +720,9 @@ Type: `rps`
| `invited_requests` | InvitedRequest[] | 邀请消息列表 |
| `join_requests` | JoinRequest[] | 进群消息列表 |
> 注意: 如果列表不存在任何消息, 将返回 `null`
> 注意: 如果列表不存在任何消息, 将返回 `null`
**InvitedRequest**
**InvitedRequest**
| 字段 | 类型 | 说明 |
| -------------- | ------ | ----------------- |
@ -768,7 +734,7 @@ Type: `rps`
| `checked` | bool | 是否已被处理 |
| `actor` | int64 | 处理者, 未处理为0 |
**JoinRequest**
**JoinRequest**
| 字段 | 类型 | 说明 |
| ---------------- | ------ | ----------------- |
@ -859,7 +825,7 @@ Type: `rps`
| ----- | ------ | ------------ |
| `url` | string | 文件下载链接 |
**File**
**File**
| 字段 | 类型 | 说明 |
| ---------------- | ------ | ---------------------- |
@ -874,7 +840,7 @@ Type: `rps`
| `uploader` | int64 | 上传者ID |
| `uploader_name` | string | 上传者名字 |
**Folder**
**Folder**
| 字段 | 类型 | 说明 |
| ------------------ | ------ | ---------- |
@ -901,36 +867,6 @@ Type: `rps`
> 在不提供 `folder` 参数的情况下默认上传到根目录
> 只能上传本地文件, 需要上传 `http` 文件的话请先调用 `download_file` API下载
### 上传私聊文件
终结点: `/upload_private_file`
**参数**
| 字段 | 类型 | 说明 |
|-----------|--------|--------|
| `user_id` | int64 | 接收者id |
| `file` | string | 本地文件路径 |
| `name` | string | 储存名称 |
> 只能上传本地文件, 需要上传 `http` 文件的话请先调用 `download_file` API下载
### 设置 QQ 个人资料
终结点: `/set_qq_profile`
**参数**
| 字段 | 类型 | 说明 |
|-----------------|--------|------|
| `nickname` | int64 | 昵称 |
| `company` | string | 公司 |
| `email` | string | 邮箱 |
| `college` | string | 大学 |
| `personal_note` | string | 个人签名 |
> 所有参数字段都为可选。
### 获取状态
终结点: `/get_status`
@ -949,6 +885,7 @@ Type: `rps`
**Statistics**
| 字段 | 类型 | 说明 |
| ------------------ | ------ | ---------------- |
| `packet_received` | uint64 | 收到的数据包总数 |
@ -1113,130 +1050,13 @@ JSON数组:
`该 API 没有响应数据`
### 获取群公告
终结点: `/_get_group_notice`
**参数**
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `group_id` | int64 | | 群号 |
**响应数据**
数组信息:
| 字段名 | 数据类型 | 默认值 | 说明 |
|----------------|--------| ------ |-------|
| `notice_id` | string | | 公告id |
| `sender_id` | string | | 发布者id |
| `publish_time` | string | | 发布时间 |
| `message` | GroupNoticeMessage | | 公告id |
响应示例
```json
{
"data": [
{
"notice_id": "8850de2e00000000cc6bbd628a150c00",
"sender_id": 1111111,
"publish_time": 1656581068,
"message": {
"text": "这是一条公告",
"images": []
}
}
],
"retcode": 0,
"status": "ok"
}
```
### 删除群公告
终结点: `/_del_group_notice`
**参数**
| 字段名 | 数据类型 | 默认值 | 说明 |
|-------------| -------- | ------ |------|
| `group_id` | int64 | | 群号 |
| `notice_id` | string | | 公告id |
`该 API 没有响应数据`
### 获取单向好友列表
终结点: `/get_unidirectional_friend_list`
**响应数据**
数组信息:
| 字段 | 类型 | 说明 |
| ------------- | ------ | -------- |
| `nickname` | string | 昵称 |
| `user_id` | int64 | 用户QQ号 |
| `source` | string | 添加途径 |
> 添加途径为用户显示内容, 如 `精确查找` `QQ群 - xxxx`
### 删除单向好友
终结点: `/delete_unidirectional_friend`
**参数**
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `user_id` | int64 | | 好友ID |
`该 API 没有响应数据`
### 删除好友
终结点: `/delete_friend`
**参数**
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `user_id` | int64 | | 好友ID |
`该 API 没有响应数据`
### 获取企点账号信息
> 该API只有企点协议可用
终结点: `/qidian_get_account_info`
**响应数据**
| 字段 | 类型 | 说明 |
| ------------------ | ------- | ------------ |
| `master_id` | int64 | 父账号ID |
| `ext_name` | string | 用户昵称 |
| `create_time` | int64 | 账号创建时间 |
### 标记消息已读
终结点: `/mark_msg_as_read`
**参数**
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `message_id` | int32 | | 消息ID |
### 重载事件过滤器
终结点:`/reload_event_filter`
`该 API 无需参数也没有响应数据`
## 事件
### 群消息撤回
@ -1335,23 +1155,11 @@ JSON数组:
| `notice_type` | string | `group_card` | 消息类型 |
| `group_id` | int64 | | 群号 |
| `user_id` | int64 | | 成员id |
| `card_new` | string | | 新名片 |
| `card_old` | string | | 旧名片 |
| `card_new` | int64 | | 新名片 |
| `card_old` | int64 | | 旧名片 |
> PS: 当名片为空时 `card_xx` 字段为空字符串, 并不是昵称
### 群成员头衔更新事件
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | ------------ | -------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `notify` | 消息类型 |
| `group_id` | int64 | | 群号 |
| `user_id` | int64 | | 成员id |
| `title` | string | | 新头衔 |
### 接收到离线文件
**上报数据**

View File

@ -2,67 +2,37 @@
go-cqhttp 默认生成的文件树如下所示:
```
````
.
├── go-cqhttp
├── config.yml
├── config.hjson
├── device.json
├── logs
│ └── xx-xx-xx.log
└── data
├── images
│ └── xxxx.image
└── levleldb
```
└── db
````
| 文件 | 用途 |
| ------------ | -------------------- |
| go-cqhttp | go-cqhttp 可执行文件 |
| config.yml | 运行配置文件 |
| device.json | 虚拟设备配置文件 |
| logs | 日志存放目录 |
| data | 数据目录 |
| data/leveldb | 数据库目录 |
| data/images | 图片缓存目录 |
| data/voices | 语音缓存目录 |
| data/videos | 视频缓存目录 |
| data/cache | 发送图片缓存目录 |
| 文件 | 用途 |
| ----------- | ------------------- |
| go-cqhttp | go-cqhttp可执行文件 |
| config.hjson | 运行配置文件 |
| device.json | 虚拟设备配置文件 |
| logs | 日志存放目录 |
| data | 数据目录 |
| data/images | 图片缓存目录 |
| data/db | 数据库目录 |
## 图片缓存文件
出于性能考虑go-cqhttp 并不会将图片源文件下载到本地,而是生成一个可以和 QQ 服务器对应的缓存文件 (.image),该缓存文件结构如下:
出于性能考虑go-cqhttp 并不会将图片源文件下载到本地而是生成一个可以和QQ服务器对应的缓存文件 (.image),该缓存文件结构如下:
| 偏移 | 类型 | 说明 |
| --------------- | -------- | -------------------- |
| 0x00 | [16]byte | 图片源文件 MD5 HASH |
| 0x10 | uint32 | 图片源文件大小 |
| 偏移 | 类型 | 说明 |
| --------------- | -------- | ------------------ |
| 0x00 | [16]byte | 图片源文件MD5 HASH |
| 0x10 | uint32 | 图片源文件大小 |
| 0x14 | string | 图片原名(QQ内部ID) |
| 0x14 + 原名长度 | string | 图片下载链接 |
# MIME
启用MINE检查可以及时发现媒体资源格式错误引起的上传失败(通常表现为请求网页图片但服务端返回404.html)
在配置文件中设置 `skip-mine-scan: false`go-cqhttp 会在上传媒体资源(视频暂不支持)前对MIME进行检查
详细允许类型如下所示:
图片:
> image/bmp
> image/gif
> image/jpeg
> image/png
> image/webp
语音:
> audio/aac
> audio/aiff
> audio/amr
> audio/ape
> audio/flac
> audio/midi
> audio/mp4
> audio/mpeg
> audio/ogg
> audio/wav
> audio/x-m4a
| 0x14 + 原名长度 | string | 图片下载链接 |

View File

@ -1,414 +0,0 @@
# 频道相关API
> 注意: QQ频道功能目前还在测试阶段, go-cqhttp 也在适配的初期阶段, 以下 `API` `Event` 的字段名可能存在错误并均有可能在后续版本修改/添加/删除.
> 目前仅供开发者测试以及适配使用
QQ频道相关功能的事件以及API
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
## 命名说明
API以及字段相关命名均为参考QQ官方命名或相似产品命名规则, 由于QQ频道的账号系统独立于QQ本体, 所以各个 `ID` 并不能和QQ通用.也无法通过 `tiny_id` 获取到 `QQ号`
下表为常见字段命名说明
| 命名 | 说明 |
| ------------ | -------------------- |
| `tiny_id` | 在频道系统中代表用户ID, 与QQ号并不通用 |
| `guild_id` | 频道ID |
| `channel_id` | 子频道ID |
> 所有频道相关事件的 `user_id` 均为 `tiny_id`
## 特殊说明
- 由于频道的限制, 目前无法通过图片摘要查询到频道图片消息的详细信息, 所以通过频道消息收到的图片均会下载完整文件到 `images/guild-images`. (群图片转发不受此限制)
- 由于无法通过 `GlobalID` 放下频道消息的ID, 所以所有频道消息的 `message_id` 均为 `string` 类型
- `send_msg` API将无法发送频道消息
- `get_msg` API暂时无法获取频道消息
- `reply` 等消息类型暂不支持解析
- `at` 消息的 `target` 依然使用 `qq` 字段, 以保证一致性. 但内容为 `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
### 获取频道系统内BOT的资料
终结点: `/get_guild_service_profile`
**响应数据**
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `nickname` | string | 昵称 |
| `tiny_id` | string | 自身的ID |
| `avatar_url` | string | 头像链接 |
### 获取频道列表
终结点: `/get_guild_list`
**响应数据**
正常情况下响应 `GuildInfo` 数组, 未加入任何频道响应 `null`
GuildInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `guild_id` | string | 频道ID |
| `guild_name` | string | 频道名称 |
| `guild_display_id` | int64 | 频道显示ID, 公测后可能作为搜索ID使用 |
### 通过访客获取频道元数据
终结点: `/get_guild_meta_by_guest`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
**响应数据**
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `guild_id` | string | 频道ID |
| `guild_name` | string | 频道名称 |
| `guild_profile` | string | 频道简介 |
| `create_time` | int64 | 创建时间 |
| `max_member_count` | int64 | 频道人数上限 |
| `max_robot_count` | int64 | 频道BOT数上限 |
| `max_admin_count` | int64 | 频道管理员人数上限 |
| `member_count` | int64 | 已加入人数 |
| `owner_id` | string | 创建者ID |
### 获取子频道列表
终结点: `/get_guild_channel_list`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
| `no_cache` | bool | 是否无视缓存 |
**响应数据**
正常情况下响应 `ChannelInfo` 数组, 未找到任何子频道响应 `null`
ChannelInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `owner_guild_id` | string | 所属频道ID |
| `channel_id` | string | 子频道ID |
| `channel_type` | int32 | 子频道类型 |
| `channel_name` | string | 子频道名称 |
| `create_time` | int64 | 创建时间 |
| `creator_tiny_id` | string | 创建者ID |
| `talk_permission` | int32 | 发言权限类型 |
| `visible_type` | int32 | 可视性类型 |
| `current_slow_mode` | int32 | 当前启用的慢速模式Key |
| `slow_modes` | []SlowModeInfo | 频道内可用慢速模式类型列表|
SlowModeInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `slow_mode_key` | int32 | 慢速模式Key |
| `slow_mode_text` | string | 慢速模式说明 |
| `speak_frequency` | int32 | 周期内发言频率限制 |
| `slow_mode_circle` | int32 | 单位周期时间, 单位秒 |
已知子频道类型列表
| 类型 | 说明 |
| ------------- | ---------- |
| 1 | 文字频道 |
| 2 | 语音频道 |
| 5 | 直播频道 |
| 7 | 主题频道 |
### 获取频道成员列表
终结点: `/get_guild_member_list`
> 由于频道人数较多(数万), 请尽量不要全量拉取成员列表, 这将会导致严重的性能问题
>
> 尽量使用 `get_guild_member_profile` 接口代替全量拉取
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
| `next_token` | string | 翻页Token |
> `next_token` 为空的情况下, 将返回第一页的数据, 并在返回值附带下一页的 `token`
**响应数据**
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `members` | []GuildMemberInfo | 成员列表 |
| `finished` | bool | 是否最终页 |
| `next_token` | string | 翻页Token |
GuildMemberInfo:
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `tiny_id` | string | 成员ID |
| `title` | string | 成员头衔 |
| `nickname` | string | 成员昵称 |
| `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 | 权限组名称 |
### 发送信息到子频道
终结点: `/send_guild_channel_msg`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `guild_id` | string | 频道ID |
| `channel_id` | string | 子频道ID |
| `message` | Message | 消息, 与原有消息类型相同 |
**响应数据**
| 字段 | 类型 | 说明 |
| ------------- | ----- | ---------- |
| `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 |
## 事件
### 收到频道消息
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `message` | 上报类型 |
| `message_type` | string | `guild` | 消息类型 |
| `sub_type` | string | `channel` | 消息子类型 |
| `guild_id` | string | | 频道ID |
| `channel_id` | string | | 子频道ID |
| `user_id` | string | | 消息发送者ID |
| `message_id` | string | | 消息ID |
| `sender` | Sender | | 发送者 |
| `message` | Message | | 消息内容 |
> 注: 此处的 `Sender` 对象为保证一致性, `user_id` 为 `uint64` 类型, 并添加了 `string` 类型的 `tiny_id` 字段
### 频道消息表情贴更新
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `message_reactions_updated` | 消息类型 |
| `guild_id` | string | | 频道ID |
| `channel_id` | string | | 子频道ID |
| `user_id` | string | | 操作者ID |
| `message_id` | string | | 消息ID |
| `current_reactions` | []ReactionInfo | | 当前消息被贴表情列表 |
ReactionInfo:
| 字段 | 类型 | 说明 |
| ---------- | ----- | ---- |
| `emoji_id` | string | 表情ID |
| `emoji_index` | int32 | 表情对应数值ID |
| `emoji_type` | int32 | 表情类型 |
| `emoji_name` | string | 表情名字 |
| `count` | int32 | 当前表情被贴数量 |
| `clicked` | bool | BOT是否点击 |
### 子频道信息更新
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_updated` | 消息类型 |
| `guild_id` | string | | 频道ID |
| `channel_id` | string | | 子频道ID |
| `user_id` | string | | 操作者ID |
| `operator_id` | string | | 操作者ID |
| `old_info` | ChannelInfo | | 更新前的频道信息 |
| `new_info` | ChannelInfo | | 更新后的频道信息 |
### 子频道创建
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_created` | 消息类型 |
| `guild_id` | string | | 频道ID |
| `channel_id` | string | | 子频道ID |
| `user_id` | string | | 操作者ID |
| `operator_id` | string | | 操作者ID |
| `channel_info` | ChannelInfo | | 频道信息 |
### 子频道删除
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `channel_destroyed` | 消息类型 |
| `guild_id` | string | | 频道ID |
| `channel_id` | string | | 子频道ID |
| `user_id` | string | | 操作者ID |
| `operator_id` | string | | 操作者ID |
| `channel_info` | ChannelInfo | | 频道信息 |

View File

@ -2,8 +2,6 @@
欢迎来到 go-cqhttp 文档 目前还在咕
> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
# 基础教程
## 下载
从[release](https://github.com/Mrs4s/go-cqhttp/releases)界面下载最新版本的go-cqhttp

View File

@ -1,7 +1,5 @@
# 滑块验证码
> 该文档已过期, 最新版本下可直接使用手机扫描二维码通过验证.
由于TX最新的限制, 所有协议在陌生设备/IP登录时都有可能被要求通过滑块验证码, 否则将会出现 `当前上网环境异常` 的错误. 目前我们准备了两个临时方案应对该验证码.
> 如果您有一台运行Windows的PC/Server 并且不会抓包操作, 我们建议直接使用方案B

View File

@ -1,36 +0,0 @@
package global
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestVersionNameCompare(t *testing.T) {
tests := [...]struct {
current string
remote string
expected bool
}{
// Normal Tests:
{"v0.9.29-fix2", "v0.9.29-fix2", false},
{"v0.9.29-fix1", "v0.9.29-fix2", true},
{"v0.9.29-fix2", "v0.9.29-fix1", false},
{"v0.9.29-fix2", "v0.9.30", true},
{"v1.0.0-alpha", "v1.0.0-alpha2", true},
{"v1.0.0-alpha2", "v1.0.0-beta1", true},
{"v1.0.0", "v1.0.0-beta1", false},
{"v1.0.0-alpha", "v1.0.0", true},
{"v1.0.0", "v1.0.0", false},
{"v1.0.0-alpha", "v1.0.0-rc1", true},
// Issue Fixes:
{"v1.0.0-beta1", "v0.9.40-fix5", false}, // issue #877
}
for i := 0; i < len(tests); i++ {
t.Run("test case "+strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tests[i].expected, VersionNameCompare(tests[i].current, tests[i].remote))
})
}
}

View File

@ -2,16 +2,26 @@ package global
import (
"bytes"
"github.com/Mrs4s/MiraiGo/binary" // 和 MiraiGo 共用同一 buffer 池
"sync"
)
// NewBuffer 从池中获取新 bytes.Buffer
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// NewBuffer 从池钟获取新 bytes.Buffer
func NewBuffer() *bytes.Buffer {
return (*bytes.Buffer)(binary.SelectWriter())
return bufferPool.Get().(*bytes.Buffer)
}
// PutBuffer 将 Buffer放入池中
func PutBuffer(buf *bytes.Buffer) {
binary.PutWriter((*binary.Writer)(buf))
// See https://golang.org/issue/23199
const maxSize = 1 << 16
if buf.Cap() < maxSize { // 对于大Buffer直接丢弃
buf.Reset()
bufferPool.Put(buf)
}
}

View File

@ -2,14 +2,14 @@ package global
import (
"crypto/md5"
"encoding/hex"
"os"
"fmt"
"io/ioutil"
"os/exec"
"path"
"github.com/pkg/errors"
"github.com/Mrs4s/go-cqhttp/global/codec"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/pkg/errors"
)
// EncoderSilk 将音频编码为Silk
@ -19,11 +19,11 @@ func EncoderSilk(data []byte) ([]byte, error) {
if err != nil {
return nil, errors.Wrap(err, "calc md5 failed")
}
tempName := hex.EncodeToString(h.Sum(nil))
tempName := fmt.Sprintf("%x", h.Sum(nil))
if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) {
return os.ReadFile(silkPath)
return ioutil.ReadFile(silkPath)
}
slk, err := base.EncodeSilk(data, tempName)
slk, err := codec.EncodeToSilk(data, tempName, true)
if err != nil {
return nil, errors.Wrap(err, "encode silk failed")
}
@ -33,15 +33,9 @@ func EncoderSilk(data []byte) ([]byte, error) {
// EncodeMP4 将给定视频文件编码为MP4
func EncodeMP4(src string, dst string) error { // -y 覆盖文件
cmd1 := exec.Command("ffmpeg", "-i", src, "-y", "-c", "copy", "-map", "0", dst)
if errors.Is(cmd1.Err, exec.ErrDot) {
cmd1.Err = nil
}
err := cmd1.Run()
if err != nil {
cmd2 := exec.Command("ffmpeg", "-i", src, "-y", "-c:v", "h264", "-c:a", "mp3", dst)
if errors.Is(cmd2.Err, exec.ErrDot) {
cmd2.Err = nil
}
return errors.Wrap(cmd2.Run(), "convert mp4 failed")
}
return err
@ -49,9 +43,6 @@ func EncodeMP4(src string, dst string) error { // -y 覆盖文件
// ExtractCover 获取给定视频文件的Cover
func ExtractCover(src string, target string) error {
cmd := exec.Command("ffmpeg", "-i", src, "-y", "-ss", "0", "-frames:v", "1", target)
if errors.Is(cmd.Err, exec.ErrDot) {
cmd.Err = nil
}
cmd := exec.Command("ffmpeg", "-i", src, "-y", "-r", "1", "-f", "image2", target)
return errors.Wrap(cmd.Run(), "extract video cover failed")
}

53
global/codec/codec.go Normal file
View File

@ -0,0 +1,53 @@
// +build linux windows,!arm darwin
// +build 386 amd64 arm arm64
// Package codec Slik编码核心模块
package codec
import (
"io/ioutil"
"os"
"os/exec"
"path"
"github.com/pkg/errors"
"github.com/wdvxdr1123/go-silk"
)
const (
silkCachePath = "data/cache"
)
// EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) (silkWav []byte, err error) {
// 1. 写入缓存文件
rawPath := path.Join(silkCachePath, tempName+".wav")
err = ioutil.WriteFile(rawPath, record, os.ModePerm)
if err != nil {
return nil, errors.Wrap(err, "write temp file error")
}
defer os.Remove(rawPath)
// 2.转换pcm
pcmPath := path.Join(silkCachePath, tempName+".pcm")
cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath)
if err = cmd.Run(); err != nil {
return nil, errors.Wrap(err, "convert pcm file error")
}
defer os.Remove(pcmPath)
// 3. 转silk
pcm, err := ioutil.ReadFile(pcmPath)
if err != nil {
return nil, errors.Wrap(err, "read pcm file err")
}
silkWav, err = silk.EncodePcmBuffToSilk(pcm, 24000, 24000, true)
if err != nil {
return nil, errors.Wrap(err, "silk encode error")
}
if useCache {
silkPath := path.Join(silkCachePath, tempName+".silk")
err = ioutil.WriteFile(silkPath, silkWav, 0666)
}
return
}

View File

@ -0,0 +1,10 @@
// +build !arm,!arm64,!amd64,!386
package codec
import "errors"
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
return nil, errors.New("not supported now")
}

View File

@ -0,0 +1,10 @@
// +build !windows,!linux,!darwin
package codec
import "errors"
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
return nil, errors.New("not supported now")
}

View File

@ -0,0 +1,8 @@
package codec
import "errors"
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
return nil, errors.New("not supported now")
}

346
global/config.go Normal file
View File

@ -0,0 +1,346 @@
package global
import (
"os"
"path"
"strconv"
"time"
"github.com/hjson/hjson-go"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
var currentPath = getCurrentPath()
var DefaultConfFile = path.Join(currentPath, "config.hjson")
// DefaultConfigWithComments 为go-cqhttp的默认配置文件
var DefaultConfigWithComments = `
/*
go-cqhttp 默认配置文件
*/
{
// QQ号
uin: 0
// QQ密码
password: ""
// 是否启用密码加密
encrypt_password: false
// 加密后的密码, 如未启用密码加密将为空, 请勿随意修改.
password_encrypted: ""
// 是否启用内置数据库
// 启用将会增加10-20MB的内存占用和一定的磁盘空间
// 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable_db: true
// 访问密钥, 强烈推荐在公网的服务器设置
access_token: ""
// 重连设置
relogin: {
// 是否启用自动重连
// 如不启用掉线后将不会自动重连
enabled: true
// 重连延迟, 单位秒
relogin_delay: 3
// 最大重连次数, 0为无限制
max_relogin_times: 0
}
// API限速设置
// 该设置为全局生效
// 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
// 目前该限速设置为令牌桶算法, 请参考:
// https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
_rate_limit: {
// 是否启用限速
enabled: false
// 令牌回复频率, 单位秒
frequency: 1
// 令牌桶大小
bucket_size: 1
}
// 是否忽略无效的CQ码
// 如果为假将原样发送
ignore_invalid_cqcode: false
// 是否强制分片发送消息
// 分片发送将会带来更快的速度
// 但是兼容性会有些问题
force_fragmented: false
// 心跳频率, 单位秒
// -1 为关闭心跳
heartbeat_interval: 0
// HTTP设置
http_config: {
// 是否启用正向HTTP服务器
enabled: true
// 服务端监听地址
host: 0.0.0.0
// 服务端监听端口
port: 5700
// 反向HTTP超时时间, 单位秒
// 最小值为5小于5将会忽略本项设置
timeout: 0
// 反向HTTP POST地址列表
// 格式:
// {
// 地址: secret
// }
post_urls: {}
}
// 正向WS设置
ws_config: {
// 是否启用正向WS服务器
enabled: true
// 正向WS服务器监听地址
host: 0.0.0.0
// 正向WS服务器监听端口
port: 6700
}
// 反向WS设置
ws_reverse_servers: [
// 可以添加多个反向WS推送
{
// 是否启用该推送
enabled: false
// 反向WS Universal 地址
// 注意 设置了此项地址后下面两项将会被忽略
// 留空请使用 ""
reverse_url: ws://you_websocket_universal.server
// 反向WS API 地址
reverse_api_url: ws://you_websocket_api.server
// 反向WS Event 地址
reverse_event_url: ws://you_websocket_event.server
// 重连间隔 单位毫秒
reverse_reconnect_interval: 3000
}
]
// 上报数据类型
// 可选: string array
post_message_format: string
// 是否使用服务器下发的新地址进行重连
// 注意, 此设置可能导致在海外服务器上连接情况更差
use_sso_address: false
// 是否启用 DEBUG
debug: false
// 日志等级 trace,debug,info,warn,error
log_level: "info"
// WebUi 设置
web_ui: {
// 是否启用 WebUi
enabled: true
// 监听地址
host: 127.0.0.1
// 监听端口
web_ui_port: 9999
// 是否接收来自web的输入
web_input: false
}
}
`
// PasswordHash 存储QQ密码哈希供登录使用
var PasswordHash [16]byte
// JSONConfig Config对应的结构体
type JSONConfig struct {
Uin int64 `json:"uin"`
Password string `json:"password"`
EncryptPassword bool `json:"encrypt_password"`
PasswordEncrypted string `json:"password_encrypted"`
EnableDB bool `json:"enable_db"`
EnableSelfMessage bool `json:"enable_self_message"`
AccessToken string `json:"access_token"`
ReLogin struct {
Enabled bool `json:"enabled"`
ReLoginDelay int `json:"relogin_delay"`
MaxReloginTimes uint `json:"max_relogin_times"`
} `json:"relogin"`
RateLimit struct {
Enabled bool `json:"enabled"`
Frequency float64 `json:"frequency"`
BucketSize int `json:"bucket_size"`
} `json:"_rate_limit"`
IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"`
ForceFragmented bool `json:"force_fragmented"`
FixURL bool `json:"fix_url"`
ProxyRewrite string `json:"proxy_rewrite"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
HTTPConfig *GoCQHTTPConfig `json:"http_config"`
WSConfig *GoCQWebSocketConfig `json:"ws_config"`
ReverseServers []*GoCQReverseWebSocketConfig `json:"ws_reverse_servers"`
PostMessageFormat string `json:"post_message_format"`
UseSSOAddress bool `json:"use_sso_address"`
Debug bool `json:"debug"`
LogLevel string `json:"log_level"`
//WebUI *GoCQWebUI `json:"web_ui"`
}
// CQHTTPAPIConfig HTTPAPI对应的Config结构体
type CQHTTPAPIConfig struct {
Host string `json:"host"`
Port uint16 `json:"port"`
UseHTTP bool `json:"use_http"`
WSHost string `json:"ws_host"`
WSPort uint16 `json:"ws_port"`
UseWS bool `json:"use_ws"`
WSReverseURL string `json:"ws_reverse_url"`
WSReverseAPIURL string `json:"ws_reverse_api_url"`
WSReverseEventURL string `json:"ws_reverse_event_url"`
WSReverseReconnectInterval uint16 `json:"ws_reverse_reconnect_interval"`
WSReverseReconnectOnCode1000 bool `json:"ws_reverse_reconnect_on_code_1000"`
UseWsReverse bool `json:"use_ws_reverse"`
PostURL string `json:"post_url"`
AccessToken string `json:"access_token"`
Secret string `json:"secret"`
PostMessageFormat string `json:"post_message_format"`
}
// GoCQHTTPConfig 正向HTTP对应config结构体
type GoCQHTTPConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
Timeout int32 `json:"timeout"`
PostUrls map[string]string `json:"post_urls"`
}
// GoCQWebSocketConfig 正向WebSocket对应Config结构体
type GoCQWebSocketConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
}
// GoCQReverseWebSocketConfig 反向WebSocket对应Config结构体
type GoCQReverseWebSocketConfig struct {
Enabled bool `json:"enabled"`
ReverseURL string `json:"reverse_url"`
ReverseAPIURL string `json:"reverse_api_url"`
ReverseEventURL string `json:"reverse_event_url"`
ReverseReconnectInterval uint16 `json:"reverse_reconnect_interval"`
}
/*
// GoCQWebUI WebUI对应Config结构体
type GoCQWebUI struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
WebUIPort uint64 `json:"web_ui_port"`
WebInput bool `json:"web_input"`
}
*/
// DefaultConfig 返回一份默认配置对应结构体
func DefaultConfig() *JSONConfig {
return &JSONConfig{
EnableDB: true,
ReLogin: struct {
Enabled bool `json:"enabled"`
ReLoginDelay int `json:"relogin_delay"`
MaxReloginTimes uint `json:"max_relogin_times"`
}{
Enabled: true,
ReLoginDelay: 3,
MaxReloginTimes: 0,
},
RateLimit: struct {
Enabled bool `json:"enabled"`
Frequency float64 `json:"frequency"`
BucketSize int `json:"bucket_size"`
}{
Enabled: false,
Frequency: 1,
BucketSize: 1,
},
PostMessageFormat: "string",
ForceFragmented: false,
HTTPConfig: &GoCQHTTPConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 5700,
PostUrls: map[string]string{},
},
WSConfig: &GoCQWebSocketConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 6700,
},
ReverseServers: []*GoCQReverseWebSocketConfig{
{
Enabled: false,
ReverseURL: "ws://you_websocket_universal.server",
ReverseAPIURL: "ws://you_websocket_api.server",
ReverseEventURL: "ws://you_websocket_event.server",
ReverseReconnectInterval: 3000,
},
},
}
}
// LoadConfig 加载配置文件
func LoadConfig(p string) *JSONConfig {
if !PathExists(p) {
log.Warnf("尝试加载配置文件 %v 失败: 文件不存在", p)
return nil
}
var dat map[string]interface{}
var c = JSONConfig{}
err := hjson.Unmarshal([]byte(ReadAllText(p)), &dat)
if err == nil {
b, _ := json.Marshal(dat)
err = json.Unmarshal(b, &c)
}
if err != nil {
log.Warnf("尝试加载配置文件 %v 时出现错误: %v", p, err)
log.Infoln("原文件已备份")
_ = os.Rename(p, p+".backup"+strconv.FormatInt(time.Now().Unix(), 10))
return nil
}
return &c
}
// Save 写入配置文件至path
func (c *JSONConfig) Save(path string) error {
data, err := hjson.MarshalWithOptions(c, hjson.EncoderOptions{
Eol: "\n",
BracesSameLine: true,
IndentBy: " ",
})
if err != nil {
return err
}
return WriteAllText(path, string(data))
}
// getCurrentPath 获取当前文件的路径直接返回string
func getCurrentPath() string {
cwd, e := os.Getwd()
if e != nil {
panic(e)
}
return cwd
}
/*
// GetCurrentPath 预留,获取当前目录地址
func GetCurrentPath() (string, error) {
file, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
fpath, err := filepath.Abs(file)
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
// fpath = strings.Replace(fpath, "\\", "/", -1)
fpath = strings.ReplaceAll(fpath, "\\", "/")
}
i := strings.LastIndex(fpath, "/")
if i < 0 {
return "", errors.New("system/path_error,Can't find '/' or '\\'")
}
return fpath[0 : i+1], nil
}
*/

312
global/filter.go Normal file
View File

@ -0,0 +1,312 @@
package global
import (
"fmt"
"io/ioutil"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
// MSG 消息Map
type MSG map[string]interface{}
// Get 尝试从消息Map中取出key为s的值,若不存在则返回MSG{}
//
// 若所给key对应的值的类型是global.MSG,则返回此值
//
// 若所给key对应值的类型不是global.MSG,则返回MSG{"__str__": Val}
func (m MSG) Get(s string) MSG {
if v, ok := m[s]; ok {
if msg, ok := v.(MSG); ok {
return msg
}
return MSG{"__str__": v} // 用这个名字应该没问题吧
}
return nil // 不存在为空
}
// String 将消息Map转化为String。若Map存在key "__str__",则返回此key对应的值,否则将输出整张消息Map对应的JSON字符串
func (m MSG) String() string {
if m == nil {
return "" // 空 JSON
}
if str, ok := m["__str__"]; ok {
if str == nil {
return "" // 空 JSON
}
return fmt.Sprint(str)
}
str, _ := json.MarshalToString(m)
return str
}
// Filter 定义了一个消息上报过滤接口
type Filter interface {
Eval(payload MSG) bool
}
type operationNode struct {
key string
filter Filter
}
// NotOperator 定义了过滤器中Not操作符
type NotOperator struct {
operand Filter
}
func notOperatorConstruct(argument gjson.Result) *NotOperator {
if !argument.IsObject() {
panic("the argument of 'not' operator must be an object")
}
op := new(NotOperator)
op.operand = Generate("and", argument)
return op
}
// Eval 对payload执行Not过滤
func (op *NotOperator) Eval(payload MSG) bool {
return !op.operand.Eval(payload)
}
// AndOperator 定义了过滤器中And操作符
type AndOperator struct {
operands []operationNode
}
func andOperatorConstruct(argument gjson.Result) *AndOperator {
if !argument.IsObject() {
panic("the argument of 'and' operator must be an object")
}
op := new(AndOperator)
argument.ForEach(func(key, value gjson.Result) bool {
switch {
case key.Str[0] == '.':
// is an operator
// ".foo": {
// "bar": "baz"
// }
opKey := key.Str[1:]
op.operands = append(op.operands, operationNode{"", Generate(opKey, value)})
case value.IsObject():
// is an normal key with an object as the value
// "foo": {
// ".bar": "baz"
// }
opKey := key.String()
op.operands = append(op.operands, operationNode{opKey, Generate("and", value)})
default:
// is an normal key with a non-object as the value
// "foo": "bar"
opKey := key.String()
op.operands = append(op.operands, operationNode{opKey, Generate("eq", value)})
}
return true
})
return op
}
// Eval 对payload执行And过滤
func (op *AndOperator) Eval(payload MSG) bool {
res := true
for _, operand := range op.operands {
if len(operand.key) == 0 {
// is an operator
res = res && operand.filter.Eval(payload)
} else {
// is an normal key
val := payload.Get(operand.key)
res = res && operand.filter.Eval(val)
}
if !res {
break
}
}
return res
}
// OrOperator 定义了过滤器中Or操作符
type OrOperator struct {
operands []Filter
}
func orOperatorConstruct(argument gjson.Result) *OrOperator {
if !argument.IsArray() {
panic("the argument of 'or' operator must be an array")
}
op := new(OrOperator)
argument.ForEach(func(_, value gjson.Result) bool {
op.operands = append(op.operands, Generate("and", value))
return true
})
return op
}
// Eval 对payload执行Or过滤
func (op *OrOperator) Eval(payload MSG) bool {
res := false
for _, operand := range op.operands {
res = res || operand.Eval(payload)
if res {
break
}
}
return res
}
// EqualOperator 定义了过滤器中Equal操作符
type EqualOperator struct {
operand string
}
func equalOperatorConstruct(argument gjson.Result) *EqualOperator {
op := new(EqualOperator)
op.operand = argument.String()
return op
}
// Eval 对payload执行Equal过滤
func (op *EqualOperator) Eval(payload MSG) bool {
return payload.String() == op.operand
}
// NotEqualOperator 定义了过滤器中NotEqual操作符
type NotEqualOperator struct {
operand string
}
func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator {
op := new(NotEqualOperator)
op.operand = argument.String()
return op
}
// Eval 对payload执行NotEqual过滤
func (op *NotEqualOperator) Eval(payload MSG) bool {
return !(payload.String() == op.operand)
}
// InOperator 定义了过滤器中In操作符
type InOperator struct {
operandString string
operandArray []string
}
func inOperatorConstruct(argument gjson.Result) *InOperator {
if argument.IsObject() {
panic("the argument of 'in' operator must be an array or a string")
}
op := new(InOperator)
if argument.IsArray() {
op.operandArray = []string{}
argument.ForEach(func(_, value gjson.Result) bool {
op.operandArray = append(op.operandArray, value.String())
return true
})
} else {
op.operandString = argument.String()
}
return op
}
// Eval 对payload执行In过滤
func (op *InOperator) Eval(payload MSG) bool {
payloadStr := payload.String()
if op.operandArray != nil {
for _, value := range op.operandArray {
if value == payloadStr {
return true
}
}
return false
}
return strings.Contains(op.operandString, payloadStr)
}
// ContainsOperator 定义了过滤器中Contains操作符
type ContainsOperator struct {
operand string
}
func containsOperatorConstruct(argument gjson.Result) *ContainsOperator {
if argument.IsArray() || argument.IsObject() {
panic("the argument of 'contains' operator must be a string")
}
op := new(ContainsOperator)
op.operand = argument.String()
return op
}
// Eval 对payload执行Contains过滤
func (op *ContainsOperator) Eval(payload MSG) bool {
return strings.Contains(payload.String(), op.operand)
}
// RegexOperator 定义了过滤器中Regex操作符
type RegexOperator struct {
regex *regexp.Regexp
}
func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
if argument.IsArray() || argument.IsObject() {
panic("the argument of 'regex' operator must be a string")
}
op := new(RegexOperator)
op.regex = regexp.MustCompile(argument.String())
return op
}
// Eval 对payload执行RegexO过滤
func (op *RegexOperator) Eval(payload MSG) bool {
matched := op.regex.MatchString(payload.String())
return matched
}
// Generate 根据给定操作符名opName及操作符参数argument创建一个过滤器实例
func Generate(opName string, argument gjson.Result) Filter {
switch opName {
case "not":
return notOperatorConstruct(argument)
case "and":
return andOperatorConstruct(argument)
case "or":
return orOperatorConstruct(argument)
case "neq":
return notEqualOperatorConstruct(argument)
case "eq":
return equalOperatorConstruct(argument)
case "in":
return inOperatorConstruct(argument)
case "contains":
return containsOperatorConstruct(argument)
case "regex":
return regexOperatorConstruct(argument)
default:
panic("the operator " + opName + " is not supported")
}
}
// EventFilter 初始化一个nil过滤器
var EventFilter Filter
// BootFilter 启动事件过滤器
func BootFilter() {
defer func() {
if e := recover(); e != nil {
log.Warnf("事件过滤器启动失败: %v", e)
EventFilter = nil
} else {
log.Info("事件过滤器启动成功.")
}
}()
f, err := ioutil.ReadFile("filter.json")
if err != nil {
panic(err)
} else {
EventFilter = Generate("and", gjson.ParseBytes(f))
}
}

View File

@ -1,53 +1,64 @@
package global
import (
"bufio"
"bytes"
"compress/bzip2"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"net/netip"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/Mrs4s/MiraiGo/utils"
b14 "github.com/fumiama/go-base16384"
"github.com/segmentio/asm/base64"
log "github.com/sirupsen/logrus"
"github.com/kardianos/osext"
"github.com/Mrs4s/go-cqhttp/internal/download"
"github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus"
)
const (
// ImagePath go-cqhttp使用的图片缓存目录
ImagePath = "data/images"
// ImagePathOld 兼容旧版go-cqhttp使用的图片缓存目录
ImagePathOld = "data/image"
// VoicePath go-cqhttp使用的语音缓存目录
VoicePath = "data/voices"
// VoicePathOld 兼容旧版go-cqhttp使用的语音缓存目录
VoicePathOld = "data/record"
// VideoPath go-cqhttp使用的视频缓存目录
VideoPath = "data/videos"
// VersionsPath go-cqhttp使用的版本信息目录
VersionsPath = "data/versions"
// CachePath go-cqhttp使用的缓存目录
CachePath = "data/cache"
// DumpsPath go-cqhttp使用错误转储目录
DumpsPath = "dumps"
)
var (
// ErrSyntax Path语法错误时返回的错误
ErrSyntax = errors.New("syntax error")
// HeaderAmr AMR文件头
HeaderAmr = "#!AMR"
HeaderAmr = []byte("#!AMR")
// HeaderSilk Silkv3文件头
HeaderSilk = "\x02#!SILK_V3"
HeaderSilk = []byte("\x02#!SILK_V3")
)
// PathExists 判断给定path是否存在
func PathExists(path string) bool {
_, err := os.Stat(path)
return err == nil || errors.Is(err, os.ErrExist)
return err == nil || os.IsExist(err)
}
// ReadAllText 读取给定path对应文件无法读取时返回空值
func ReadAllText(path string) string {
b, err := os.ReadFile(path)
b, err := ioutil.ReadFile(path)
if err != nil {
log.Error(err)
return ""
@ -57,51 +68,45 @@ func ReadAllText(path string) string {
// WriteAllText 将给定text写入给定path
func WriteAllText(path, text string) error {
return os.WriteFile(path, utils.S2B(text), 0o644)
return ioutil.WriteFile(path, []byte(text), 0644)
}
// Check 检测err是否为nil
func Check(err error, deleteSession bool) {
func Check(err error) {
if err != nil {
if deleteSession && PathExists("session.token") {
_ = os.Remove("session.token")
}
log.Fatalf("遇到错误: %v", err)
}
}
// IsAMRorSILK 判断给定文件是否为Amr或Silk格式
func IsAMRorSILK(b []byte) bool {
return bytes.HasPrefix(b, []byte(HeaderAmr)) || bytes.HasPrefix(b, []byte(HeaderSilk))
return bytes.HasPrefix(b, HeaderAmr) || bytes.HasPrefix(b, HeaderSilk)
}
// FindFile 从给定的File寻找文件并返回文件byte数组。File是一个合法的URL。p为文件寻找位置。
// 对于HTTP/HTTPS形式的URLCache为"1"或空时表示启用缓存
func FindFile(file, cache, p string) (data []byte, err error) {
data, err = nil, os.ErrNotExist
data, err = nil, ErrSyntax
switch {
case strings.HasPrefix(file, "http"): // https also has prefix http
case strings.HasPrefix(file, "http") || strings.HasPrefix(file, "https"):
if cache == "" {
cache = "1"
}
hash := md5.Sum([]byte(file))
cacheFile := path.Join(CachePath, hex.EncodeToString(hash[:])+".cache")
if (cache == "" || cache == "1") && PathExists(cacheFile) {
return os.ReadFile(cacheFile)
if PathExists(cacheFile) && cache == "1" {
return ioutil.ReadFile(cacheFile)
}
err = download.Request{URL: file}.WriteToFile(cacheFile)
data, err = GetBytes(file)
_ = ioutil.WriteFile(cacheFile, data, 0644)
if err != nil {
return nil, err
}
return os.ReadFile(cacheFile)
case strings.HasPrefix(file, "base64"):
data, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(file, "base64://"))
data, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(file, "base64://", ""))
if err != nil {
return nil, err
}
case strings.HasPrefix(file, "base16384"):
data, err = b14.UTF82UTF16BE(utils.S2B(strings.TrimPrefix(file, "base16384://")))
if err != nil {
return nil, err
}
data = b14.Decode(data)
case strings.HasPrefix(file, "file"):
var fu *url.URL
fu, err = url.Parse(file)
@ -111,12 +116,12 @@ func FindFile(file, cache, p string) (data []byte, err error) {
if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` {
fu.Path = fu.Path[1:]
}
data, err = os.ReadFile(fu.Path)
data, err = ioutil.ReadFile(fu.Path)
if err != nil {
return nil, err
}
case PathExists(path.Join(p, file)):
data, err = os.ReadFile(path.Join(p, file))
data, err = ioutil.ReadFile(path.Join(p, file))
if err != nil {
return nil, err
}
@ -138,19 +143,112 @@ func DelFile(path string) bool {
}
// ReadAddrFile 从给定path中读取合法的IP地址与端口,每个IP地址以换行符"\n"作为分隔
func ReadAddrFile(path string) []netip.AddrPort {
d, err := os.ReadFile(path)
func ReadAddrFile(path string) []*net.TCPAddr {
d, err := ioutil.ReadFile(path)
if err != nil {
return nil
}
str := string(d)
lines := strings.Split(str, "\n")
var ret []netip.AddrPort
var ret []*net.TCPAddr
for _, l := range lines {
addr, err := netip.ParseAddrPort(l)
if err == nil {
ret = append(ret, addr)
ip := strings.Split(strings.TrimSpace(l), ":")
if len(ip) == 2 {
port, _ := strconv.Atoi(ip[1])
ret = append(ret, &net.TCPAddr{IP: net.ParseIP(ip[0]), Port: port})
}
}
return ret
}
// WriteCounter 写入量计算实例
type WriteCounter struct {
Total uint64
}
// Write 方法将写入的byte长度追加至写入的总长度Total中
func (wc *WriteCounter) Write(p []byte) (int, error) {
n := len(p)
wc.Total += uint64(n)
wc.PrintProgress()
return n, nil
}
// PrintProgress 方法将打印当前的总写入量
func (wc *WriteCounter) PrintProgress() {
fmt.Printf("\r%s", strings.Repeat(" ", 35))
fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total))
}
// UpdateFromStream copy form getlantern/go-update
func UpdateFromStream(updateWith io.Reader) (err error, errRecover error) {
updatePath, err := osext.Executable()
if err != nil {
return
}
var newBytes []byte
// no patch to apply, go on through
var fileHeader []byte
bufBytes := bufio.NewReader(updateWith)
fileHeader, err = bufBytes.Peek(2)
if err != nil {
return
}
// The content is always bzip2 compressed except when running test, in
// which case is not prefixed with the magic byte sequence for sure.
if bytes.Equal([]byte{0x42, 0x5a}, fileHeader) {
// Identifying bzip2 files.
updateWith = bzip2.NewReader(bufBytes)
} else {
updateWith = io.Reader(bufBytes)
}
newBytes, err = ioutil.ReadAll(updateWith)
if err != nil {
return
}
// get the directory the executable exists in
updateDir := filepath.Dir(updatePath)
filename := filepath.Base(updatePath)
// Copy the contents of of newbinary to a the new executable file
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return
}
// We won't log this error, because it's always going to happen.
defer func() { _ = fp.Close() }()
if _, err = io.Copy(fp, bytes.NewReader(newBytes)); err != nil {
log.Errorf("Unable to copy data: %v\n", err)
}
// if we don't call fp.Close(), windows won't let us move the new executable
// because the file will still be "in use"
if err := fp.Close(); err != nil {
log.Errorf("Unable to close file: %v\n", err)
}
// this is where we'll move the executable to so that we can swap in the updated replacement
oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
// delete any existing old exec file - this is necessary on Windows for two reasons:
// 1. after a successful update, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldPath)
// move the existing executable to a new file in the same directory
err = os.Rename(updatePath, oldPath)
if err != nil {
return
}
// move the new executable in to become the new program
err = os.Rename(newPath, updatePath)
if err != nil {
// copy unsuccessful
errRecover = os.Rename(oldPath, updatePath)
} else {
// copy successful, remove the old binary
_ = os.Remove(oldPath)
}
return
}

View File

@ -6,10 +6,8 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/mattn/go-colorable"
"github.com/sirupsen/logrus"
)
@ -49,13 +47,14 @@ func (hook *LocalHook) pathWrite(entry *logrus.Entry) error {
return err
}
fd, err := os.OpenFile(hook.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
fd, err := os.OpenFile(hook.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
defer fd.Close()
log, err := hook.formatter.Format(entry)
if err != nil {
return err
}
@ -81,16 +80,24 @@ func (hook *LocalHook) Fire(entry *logrus.Entry) error {
}
// SetFormatter 设置日志格式
func (hook *LocalHook) SetFormatter(consoleFormatter, fileFormatter logrus.Formatter) {
func (hook *LocalHook) SetFormatter(formatter logrus.Formatter) {
hook.lock.Lock()
defer hook.lock.Unlock()
// 支持处理windows平台的console色彩
logrus.SetOutput(colorable.NewColorableStdout())
// 用于在console写出
logrus.SetFormatter(consoleFormatter)
// 用于写入文件
hook.formatter = fileFormatter
if formatter == nil {
// 用默认的
formatter = &logrus.TextFormatter{DisableColors: true}
} else {
switch f := formatter.(type) {
case *logrus.TextFormatter:
textFormatter := f
textFormatter.DisableColors = true
default:
// todo
}
}
logrus.SetFormatter(formatter)
hook.formatter = formatter
}
// SetWriter 设置Writer
@ -108,11 +115,11 @@ func (hook *LocalHook) SetPath(path string) {
}
// NewLocalHook 初始化本地日志钩子实现
func NewLocalHook(args any, consoleFormatter, fileFormatter logrus.Formatter, levels ...logrus.Level) *LocalHook {
func NewLocalHook(args interface{}, formatter logrus.Formatter, levels ...logrus.Level) *LocalHook {
hook := &LocalHook{
lock: new(sync.Mutex),
}
hook.SetFormatter(consoleFormatter, fileFormatter)
hook.SetFormatter(formatter)
hook.levels = append(hook.levels, levels...)
switch arg := args.(type) {
@ -135,101 +142,24 @@ func NewLocalHook(args any, consoleFormatter, fileFormatter logrus.Formatter, le
func GetLogLevel(level string) []logrus.Level {
switch level {
case "trace":
return []logrus.Level{
logrus.TraceLevel, logrus.DebugLevel,
return []logrus.Level{logrus.TraceLevel, logrus.DebugLevel,
logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel,
}
logrus.FatalLevel, logrus.PanicLevel}
case "debug":
return []logrus.Level{
logrus.DebugLevel, logrus.InfoLevel,
return []logrus.Level{logrus.DebugLevel, logrus.InfoLevel,
logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel,
}
logrus.FatalLevel, logrus.PanicLevel}
case "info":
return []logrus.Level{
logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel,
}
return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
case "warn":
return []logrus.Level{
logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel,
}
return []logrus.Level{logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel}
case "error":
return []logrus.Level{
logrus.ErrorLevel, logrus.FatalLevel,
logrus.PanicLevel,
}
return []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel,
logrus.PanicLevel}
default:
return []logrus.Level{
logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel,
}
}
}
// LogFormat specialize for go-cqhttp
type LogFormat struct {
EnableColor bool
}
// Format implements logrus.Formatter
func (f LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
buf := NewBuffer()
defer PutBuffer(buf)
if f.EnableColor {
buf.WriteString(GetLogLevelColorCode(entry.Level))
}
buf.WriteByte('[')
buf.WriteString(entry.Time.Format("2006-01-02 15:04:05"))
buf.WriteString("] [")
buf.WriteString(strings.ToUpper(entry.Level.String()))
buf.WriteString("]: ")
buf.WriteString(entry.Message)
buf.WriteString(" \n")
if f.EnableColor {
buf.WriteString(colorReset)
}
ret := make([]byte, len(buf.Bytes()))
copy(ret, buf.Bytes()) // copy buffer
return ret, nil
}
const (
colorCodePanic = "\x1b[1;31m" // color.Style{color.Bold, color.Red}.String()
colorCodeFatal = "\x1b[1;31m" // color.Style{color.Bold, color.Red}.String()
colorCodeError = "\x1b[31m" // color.Style{color.Red}.String()
colorCodeWarn = "\x1b[33m" // color.Style{color.Yellow}.String()
colorCodeInfo = "\x1b[37m" // color.Style{color.White}.String()
colorCodeDebug = "\x1b[32m" // color.Style{color.Green}.String()
colorCodeTrace = "\x1b[36m" // color.Style{color.Cyan}.String()
colorReset = "\x1b[0m"
)
// GetLogLevelColorCode 获取日志等级对应色彩code
func GetLogLevelColorCode(level logrus.Level) string {
switch level {
case logrus.PanicLevel:
return colorCodePanic
case logrus.FatalLevel:
return colorCodeFatal
case logrus.ErrorLevel:
return colorCodeError
case logrus.WarnLevel:
return colorCodeWarn
case logrus.InfoLevel:
return colorCodeInfo
case logrus.DebugLevel:
return colorCodeDebug
case logrus.TraceLevel:
return colorCodeTrace
default:
return colorCodeInfo
return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
}
}

View File

@ -1,27 +1,299 @@
package global
import (
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/guonaihong/gout"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/internal/download"
)
var (
client = &http.Client{
Transport: &http.Transport{
Proxy: func(request *http.Request) (u *url.URL, e error) {
if Proxy == "" {
return http.ProxyFromEnvironment(request)
}
return url.Parse(Proxy)
},
ForceAttemptHTTP2: true,
MaxConnsPerHost: 0,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 999,
},
}
// Proxy 存储Config.proxy_rewrite,用于设置代理
Proxy string
// ErrOverSize 响应主体过大时返回此错误
ErrOverSize = errors.New("oversize")
// UserAgent HTTP请求时使用的UA
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66"
)
// GetBytes 对给定URL发送Get请求返回响应主体
func GetBytes(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header["User-Agent"] = []string{UserAgent}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
buffer := bytes.NewBuffer(body)
r, _ := gzip.NewReader(buffer)
defer r.Close()
unCom, err := ioutil.ReadAll(r)
return unCom, err
}
return body, nil
}
// DownloadFile 将给定URL对应的文件下载至给定Path
func DownloadFile(url, path string, limit int64, headers map[string]string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer file.Close()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; !ok {
req.Header["User-Agent"] = []string{UserAgent}
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if limit > 0 && resp.ContentLength > limit {
return ErrOverSize
}
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}
// DownloadFileMultiThreading 使用threadCount个线程将给定URL对应的文件下载至给定Path
func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, headers map[string]string) error {
if threadCount < 2 {
return DownloadFile(url, path, limit, headers)
}
type BlockMetaData struct {
BeginOffset int64
EndOffset int64
DownloadedSize int64
}
var blocks []*BlockMetaData
var contentLength int64
errUnsupportedMultiThreading := errors.New("unsupported multi-threading")
// 初始化分块或直接下载
initOrDownload := func() error {
copyStream := func(s io.ReadCloser) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer file.Close()
if _, err = io.Copy(file, s); err != nil {
return err
}
return errUnsupportedMultiThreading
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; !ok {
req.Header["User-Agent"] = []string{UserAgent}
}
req.Header.Set("range", "bytes=0-")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
}
if resp.StatusCode == 200 {
if limit > 0 && resp.ContentLength > limit {
return ErrOverSize
}
return copyStream(resp.Body)
}
if resp.StatusCode == 206 {
contentLength = resp.ContentLength
if limit > 0 && resp.ContentLength > limit {
return ErrOverSize
}
blockSize := func() int64 {
if contentLength > 1024*1024 {
return (contentLength / int64(threadCount)) - 10
}
return contentLength
}()
if blockSize == contentLength {
return copyStream(resp.Body)
}
var tmp int64
for tmp+blockSize < contentLength {
blocks = append(blocks, &BlockMetaData{
BeginOffset: tmp,
EndOffset: tmp + blockSize - 1,
})
tmp += blockSize
}
blocks = append(blocks, &BlockMetaData{
BeginOffset: tmp,
EndOffset: contentLength - 1,
})
return nil
}
return errors.New("unknown status code")
}
// 下载分块
downloadBlock := func(block *BlockMetaData) error {
req, _ := http.NewRequest("GET", url, nil)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer file.Close()
_, _ = file.Seek(block.BeginOffset, io.SeekStart)
writer := bufio.NewWriter(file)
defer writer.Flush()
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; ok {
req.Header["User-Agent"] = []string{UserAgent}
}
req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10))
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
}
var buffer = make([]byte, 1024)
i, err := resp.Body.Read(buffer)
for {
if err != nil && err != io.EOF {
return err
}
i64 := int64(len(buffer[:i]))
needSize := block.EndOffset + 1 - block.BeginOffset
if i64 > needSize {
i64 = needSize
err = io.EOF
}
_, e := writer.Write(buffer[:i64])
if e != nil {
return e
}
block.BeginOffset += i64
block.DownloadedSize += i64
if err == io.EOF || block.BeginOffset > block.EndOffset {
break
}
i, err = resp.Body.Read(buffer)
}
return nil
}
if err := initOrDownload(); err != nil {
if err == errUnsupportedMultiThreading {
return nil
}
return err
}
wg := sync.WaitGroup{}
wg.Add(len(blocks))
var lastErr error
for i := range blocks {
go func(b *BlockMetaData) {
defer wg.Done()
if err := downloadBlock(b); err != nil {
lastErr = err
}
}(blocks[i])
}
wg.Wait()
return lastErr
}
// GetSliderTicket 通过给定的验证链接raw和id,获取验证结果Ticket
func GetSliderTicket(raw, id string) (string, error) {
var rsp string
if err := gout.POST("https://api.shkong.com/gocqhttpapi/task").SetJSON(gout.H{
"id": id,
"url": raw,
}).SetTimeout(time.Second * 35).BindBody(&rsp).Do(); err != nil {
return "", err
}
g := gjson.Parse(rsp)
if g.Get("error").Str != "" {
return "", errors.New(g.Get("error").Str)
}
return g.Get("ticket").Str, nil
}
// QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
func QQMusicSongInfo(id string) (gjson.Result, error) {
d, err := download.Request{URL: `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`}.JSON()
d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`)
if err != nil {
return gjson.Result{}, err
}
return d.Get("songinfo.data"), nil
return gjson.ParseBytes(d).Get("songinfo.data"), nil
}
// NeteaseMusicSongInfo 通过给定id在wdd音乐上查找曲目信息
func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
d, err := download.Request{URL: fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id)}.JSON()
d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id))
if err != nil {
return gjson.Result{}, err
}
return d.Get("songs.0"), nil
return gjson.ParseBytes(d).Get("songs.0"), nil
}

View File

@ -1,14 +1,66 @@
package global
import (
"math"
"regexp"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
// MSG 消息Map
type MSG = map[string]any
var trueSet = map[string]struct{}{
"true": {},
"yes": {},
"1": {},
}
var falseSet = map[string]struct{}{
"false": {},
"no": {},
"0": {},
}
// EnsureBool 判断给定的p是否可表示为合法Bool类型,否则返回defaultVal
//
// 支持的合法类型有
//
// type bool
//
// type gjson.True or gjson.False
//
// type string "true","yes","1" or "false","no","0" (case insensitive)
func EnsureBool(p interface{}, defaultVal bool) bool {
var str string
if b, ok := p.(bool); ok {
return b
}
if j, ok := p.(gjson.Result); ok {
if !j.Exists() {
return defaultVal
}
if j.Type == gjson.True {
return true
}
if j.Type == gjson.False {
return false
}
if j.Type != gjson.String {
return defaultVal
}
str = j.Str
} else if s, ok := p.(string); ok {
str = s
}
str = strings.ToLower(str)
if _, ok := trueSet[str]; ok {
return true
}
if _, ok := falseSet[str]; ok {
return false
}
return defaultVal
}
// VersionNameCompare 检查版本名是否需要更新, 仅适用于 go-cqhttp 的版本命名规则
//
@ -19,28 +71,37 @@ type MSG = map[string]any
// v0.9.29-fix2 > v0.9.29-fix1 -> false
//
// v0.9.29-fix2 < v0.9.30 -> true
//
// v1.0.0-alpha2 < v1.0.0-beta1 -> true
//
// v1.0.0 > v1.0.0-beta1 -> false
func VersionNameCompare(current, remote string) bool {
defer func() { // 应该不会panic 为了保险还是加个
if err := recover(); err != nil {
log.Warn("检查更新失败!")
}
}()
sp := regexp.MustCompile(`v(\d+)\.(\d+)\.(\d+)-?(.+)?`)
cur := sp.FindStringSubmatch(current)
re := sp.FindStringSubmatch(remote)
for i := 1; i <= 3; i++ {
curSub, _ := strconv.Atoi(cur[i])
reSub, _ := strconv.Atoi(re[i])
if curSub != reSub {
return curSub < reSub
sp := regexp.MustCompile(`[0-9]\d*`)
cur := sp.FindAllStringSubmatch(current, -1)
re := sp.FindAllStringSubmatch(remote, -1)
for i := 0; i < int(math.Min(float64(len(cur)), float64(len(re)))); i++ {
curSub, _ := strconv.Atoi(cur[i][0])
reSub, _ := strconv.Atoi(re[i][0])
if curSub < reSub {
return true
}
}
if cur[4] == "" || re[4] == "" {
return re[4] == "" && cur[4] != re[4]
}
return cur[4] < re[4]
return len(cur) < len(re)
}
// SplitURL 将给定URL字符串分割为两部分用于URL预处理防止风控
func SplitURL(s string) []string {
reg := regexp.MustCompile(`(?i)[a-z\d][-a-z\d]{0,62}(\.[a-z\d][-a-z\d]{0,62})+\.?`)
idx := reg.FindAllStringIndex(s, -1)
if len(idx) == 0 {
return []string{s}
}
var result []string
last := 0
for i := 0; i < len(idx); i++ {
if len(idx[i]) != 2 {
continue
}
m := int(math.Abs(float64(idx[i][0]-idx[i][1]))/1.5) + idx[i][0]
result = append(result, s[last:m])
last = m
}
result = append(result, s[last:])
return result
}

23
global/ratelimit.go Normal file
View File

@ -0,0 +1,23 @@
package global
import (
"context"
"golang.org/x/time/rate"
)
var limiter *rate.Limiter
var limitEnable = false
// RateLimit 执行API调用速率限制
func RateLimit(ctx context.Context) {
if limitEnable {
_ = limiter.Wait(ctx)
}
}
// InitLimiter 初始化速率限制器
func InitLimiter(frequency float64, bucketSize int) {
limitEnable = true
limiter = rate.NewLimiter(rate.Limit(frequency), bucketSize)
}

View File

@ -1,52 +0,0 @@
package global
import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
var (
mainStopCh chan struct{}
mainOnce sync.Once
dumpMutex sync.Mutex
)
func dumpStack() {
dumpMutex.Lock()
defer dumpMutex.Unlock()
log.Info("开始 dump 当前 goroutine stack 信息")
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, true)
if n < len(buf) {
buf = buf[:n]
break
}
buf = make([]byte, 2*len(buf))
}
fileName := fmt.Sprintf("%s.%d.stacks.%d.log", filepath.Base(os.Args[0]), os.Getpid(), time.Now().Unix())
fd, err := os.Create(fileName)
if err != nil {
log.Errorf("保存 stackdump 到文件时出现错误: %v", err)
log.Warnf("无法保存 stackdump. 将直接打印\n %s", buf)
return
}
defer fd.Close()
_, err = fd.Write(buf)
if err != nil {
log.Errorf("写入 stackdump 失败: %v", err)
log.Warnf("无法保存 stackdump. 将直接打印\n %s", buf)
return
}
log.Infof("stackdump 已保存至 %s", fileName)
}

View File

@ -1,34 +0,0 @@
//go:build !windows
// +build !windows
package global
import (
"os"
"os/signal"
"sync"
"syscall"
)
// SetupMainSignalHandler is for main to use at last
func SetupMainSignalHandler() <-chan struct{} {
mainOnce.Do(func() {
mainStopCh = make(chan struct{})
mc := make(chan os.Signal, 4)
closeOnce := sync.Once{}
signal.Notify(mc, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
for {
switch <-mc {
case os.Interrupt, syscall.SIGTERM:
closeOnce.Do(func() {
close(mainStopCh)
})
case syscall.SIGQUIT, syscall.SIGUSR1:
dumpStack()
}
}
}()
})
return mainStopCh
}

View File

@ -1,89 +0,0 @@
//go:build windows
// +build windows
package global
import (
"errors"
"fmt"
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/Microsoft/go-winio"
log "github.com/sirupsen/logrus"
)
var validTasks = map[string]func(){
"dumpstack": dumpStack,
}
// SetupMainSignalHandler is for main to use at last
func SetupMainSignalHandler() <-chan struct{} {
mainOnce.Do(func() {
// for stack trace collecting on windows
pipeName := fmt.Sprintf(`\\.\pipe\go-cqhttp-%d`, os.Getpid())
pipe, err := winio.ListenPipe(pipeName, &winio.PipeConfig{})
if err != nil {
log.Errorf("创建 named pipe 失败. 将无法使用 dumpstack 功能: %v", err)
} else {
maxTaskLen := 0
for t := range validTasks {
if l := len(t); l > maxTaskLen {
maxTaskLen = l
}
}
go func() {
for {
c, err := pipe.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), "closed") {
return
}
log.Errorf("accept named pipe 失败: %v", err)
continue
}
go func() {
defer c.Close()
_ = c.SetReadDeadline(time.Now().Add(5 * time.Second))
buf := make([]byte, maxTaskLen)
n, err := c.Read(buf)
if err != nil {
log.Errorf("读取 named pipe 失败: %v", err)
return
}
cmd := string(buf[:n])
if task, ok := validTasks[cmd]; ok {
task()
return
}
log.Warnf("named pipe 读取到未知指令: %q", cmd)
}()
}
}()
}
// setup the main stop channel
mainStopCh = make(chan struct{})
mc := make(chan os.Signal, 2)
closeOnce := sync.Once{}
signal.Notify(mc, os.Interrupt, syscall.SIGTERM)
go func() {
for {
switch <-mc {
case os.Interrupt, syscall.SIGTERM:
closeOnce.Do(func() {
close(mainStopCh)
if pipe != nil {
_ = pipe.Close()
}
})
}
}
}()
})
return mainStopCh
}

View File

@ -1,2 +0,0 @@
// Package terminal 包含用于检测在windows下是否通过双击运行go-cqhttp, 禁用快速编辑, 启用VT100的函数
package terminal

View File

@ -1,13 +1,7 @@
//go:build !windows
// +build !windows
package terminal
// RunningByDoubleClick 检查是否通过双击直接运行非Windows系统永远返回false
func RunningByDoubleClick() bool {
return false
}
// NoMoreDoubleClick 提示用户不要双击运行非Windows系统永远返回nil
func NoMoreDoubleClick() error {
return nil
}

View File

@ -1,18 +1,15 @@
// +build windows
package terminal
import (
"os"
"path/filepath"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"github.com/pkg/errors"
)
// RunningByDoubleClick 检查是否通过双击直接运行
func RunningByDoubleClick() bool {
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll")
lp := kernel32.NewProc("GetConsoleProcessList")
if lp != nil {
var ids [2]uint32
@ -24,68 +21,3 @@ func RunningByDoubleClick() bool {
}
return true
}
// NoMoreDoubleClick 提示用户不要双击运行,并生成安全启动脚本
func NoMoreDoubleClick() error {
toHighDPI()
r := boxW(getConsoleWindows(), "请勿通过双击直接运行本程序, 这将导致一些非预料的后果.\n请在shell中运行./go-cqhttp.exe\n点击确认将释出安全启动脚本点击取消则关闭程序", "警告", 0x00000030|0x00000001)
if r == 2 {
return nil
}
r = boxW(0, "点击确认将覆盖go-cqhttp.bat点击取消则关闭程序", "警告", 0x00000030|0x00000001)
if r == 2 {
return nil
}
f, err := os.OpenFile("go-cqhttp.bat", os.O_CREATE|os.O_RDWR, 0o666)
if err != nil {
return err
}
if err != nil {
return errors.Errorf("打开go-cqhttp.bat失败: %v", err)
}
_ = f.Truncate(0)
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 {
return errors.Errorf("写入go-cqhttp.bat失败: %v", err)
}
f.Close()
boxW(0, "安全启动脚本已生成请双击go-cqhttp.bat启动", "提示", 0x00000040|0x00000000)
return nil
}
// BoxW of Win32 API. Check https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw for more detail.
func boxW(hwnd uintptr, caption, title string, flags uint) int {
captionPtr, _ := windows.UTF16PtrFromString(caption)
titlePtr, _ := windows.UTF16PtrFromString(title)
u32 := windows.NewLazySystemDLL("user32.dll")
ret, _, _ := u32.NewProc("MessageBoxW").Call(
hwnd,
uintptr(unsafe.Pointer(captionPtr)),
uintptr(unsafe.Pointer(titlePtr)),
uintptr(flags))
return int(ret)
}
// GetConsoleWindows retrieves the window handle used by the console associated with the calling process.
func getConsoleWindows() (hWnd uintptr) {
hWnd, _, _ = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetConsoleWindow").Call()
return
}
// toHighDPI tries to raise DPI awareness context to DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED
func toHighDPI() {
systemAware := ^uintptr(2) + 1
unawareGDIScaled := ^uintptr(5) + 1
u32 := windows.NewLazySystemDLL("user32.dll")
proc := u32.NewProc("SetThreadDpiAwarenessContext")
if proc.Find() != nil {
return
}
for i := unawareGDIScaled; i <= systemAware; i++ {
_, _, _ = u32.NewProc("SetThreadDpiAwarenessContext").Call(i)
}
}

View File

@ -1,13 +0,0 @@
//go:build !windows
package terminal
// RestoreInputMode 还原输入模式非Windows系统永远返回nil
func RestoreInputMode() error {
return nil
}
// DisableQuickEdit 禁用快速编辑非Windows系统永远返回nil
func DisableQuickEdit() error {
return nil
}

View File

@ -1,44 +0,0 @@
package terminal
import (
"os"
"golang.org/x/sys/windows"
)
var inputmode uint32
// RestoreInputMode 还原输入模式
func RestoreInputMode() error {
if inputmode == 0 {
return nil
}
stdin := windows.Handle(os.Stdin.Fd())
return windows.SetConsoleMode(stdin, inputmode)
}
// DisableQuickEdit 禁用快速编辑
func DisableQuickEdit() error {
stdin := windows.Handle(os.Stdin.Fd())
var mode uint32
err := windows.GetConsoleMode(stdin, &mode)
if err != nil {
return err
}
inputmode = mode
mode &^= windows.ENABLE_QUICK_EDIT_MODE // 禁用快速编辑模式
mode |= windows.ENABLE_EXTENDED_FLAGS // 启用扩展标志
mode &^= windows.ENABLE_MOUSE_INPUT // 禁用鼠标输入
mode |= windows.ENABLE_PROCESSED_INPUT // 启用控制输入
mode &^= windows.ENABLE_INSERT_MODE // 禁用插入模式
mode |= windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT // 启用输入回显&逐行输入
mode &^= windows.ENABLE_WINDOW_INPUT // 禁用窗口输入
mode &^= windows.ENABLE_VIRTUAL_TERMINAL_INPUT // 禁用虚拟终端输入
return windows.SetConsoleMode(stdin, mode)
}

View File

@ -1,15 +0,0 @@
//go:build !windows
package terminal
import (
"fmt"
"time"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
// SetTitle 设置标题为 go-cqhttp `版本` `版权`
func SetTitle() {
fmt.Printf("\033]0;go-cqhttp "+base.Version+" © 2020 - %d Mrs4s"+"\007", time.Now().Year())
}

View File

@ -1,29 +0,0 @@
package terminal
import (
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
func setConsoleTitle(title string) error {
p0, err := syscall.UTF16PtrFromString(title)
if err != nil {
return err
}
r1, _, err := windows.NewLazySystemDLL("kernel32.dll").NewProc("SetConsoleTitleW").Call(uintptr(unsafe.Pointer(p0)))
if r1 == 0 {
return err
}
return nil
}
// SetTitle 设置标题为 go-cqhttp `版本` `版权`
func SetTitle() {
_ = setConsoleTitle(fmt.Sprintf("go-cqhttp "+base.Version+" © 2020 - %d Mrs4s", time.Now().Year()))
}

View File

@ -1,8 +0,0 @@
//go:build !windows
package terminal
// EnableVT100 启用颜色、控制字符非Windows系统永远返回nil
func EnableVT100() error {
return nil
}

View File

@ -1,23 +0,0 @@
package terminal
import (
"os"
"golang.org/x/sys/windows"
)
// EnableVT100 启用颜色、控制字符
func EnableVT100() error {
stdout := windows.Handle(os.Stdout.Fd())
var mode uint32
err := windows.GetConsoleMode(stdout, &mode)
if err != nil {
return err
}
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING // 启用虚拟终端处理
mode |= windows.ENABLE_PROCESSED_OUTPUT // 启用处理后的输出
return windows.SetConsoleMode(stdout, mode)
}

83
go.mod
View File

@ -1,68 +1,31 @@
module github.com/Mrs4s/go-cqhttp
go 1.20
go 1.16
require (
github.com/FloatTech/sqlite v1.5.7
github.com/Microsoft/go-winio v0.6.0
github.com/Mrs4s/MiraiGo v0.0.0-20230401072048-f8d9841755b5
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e
github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc
github.com/fumiama/go-base16384 v1.6.1
github.com/fumiama/go-hide-param v0.1.4
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Mrs4s/MiraiGo v0.0.0-20210323143736-d233c90d5083
github.com/dustin/go-humanize v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/gorilla/websocket v1.4.2
github.com/guonaihong/gout v0.1.5
github.com/hjson/hjson-go v3.1.0+incompatible
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/json-iterator/go v1.1.10
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-colorable v0.1.13
github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/pkg/errors v0.9.1
github.com/segmentio/asm v1.2.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/tidwall/gjson v1.14.4
github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/syndtr/goleveldb v1.0.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
github.com/tidwall/gjson v1.6.8
github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2
github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60
go.mongodb.org/mongo-driver v1.11.0
golang.org/x/crypto v0.3.0
golang.org/x/image v0.5.0
golang.org/x/sys v0.2.0
golang.org/x/term v0.2.0
golang.org/x/time v0.2.0
gopkg.ilharper.com/x/isatty v1.1.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b // indirect
github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fumiama/imgsz v0.0.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.21.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.20.0 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
github.com/willf/bitset v1.1.11 // indirect
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
)

334
go.sum
View File

@ -1,240 +1,218 @@
github.com/FloatTech/sqlite v1.5.7 h1:Bvo4LSojcZ6dVtbHrkqvt6z4v8e+sj0G5PSUIvdawsk=
github.com/FloatTech/sqlite v1.5.7/go.mod h1:zFbHzRfB+CJ+VidfjuVbrcin3DAz283F7hF1hIeHzpY=
github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJGLDNIdRX3BI546D3O7k7vrVueZw=
github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Mrs4s/MiraiGo v0.0.0-20230401072048-f8d9841755b5 h1:E4fIQ0l/LNZK44NjdViRb/hx4cIeHXyQFPzzkx7cjVE=
github.com/Mrs4s/MiraiGo v0.0.0-20230401072048-f8d9841755b5/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0=
github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d h1:/Xuj3fIiMY2ls1TwvPKmaqQrtJsPY+c9s+0lOScVHd8=
github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d/go.mod h1:2Ie+hdBFQpQFDHfeklgxoFmQRCE7O+KwFpISeXq7OwA=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
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=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Mrs4s/MiraiGo v0.0.0-20210323143736-d233c90d5083 h1:ELaNvv80OTwHTYhKwoQpgV4dneKPM1qE5Geu3A1kM/8=
github.com/Mrs4s/MiraiGo v0.0.0-20210323143736-d233c90d5083/go.mod h1:NjiWhlvGxwv1ftOWIoiFa/OzklnAYI4YqNexFOKSZKw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fumiama/go-base16384 v1.6.1 h1:4yb4JgmBJDnQtq3XGXXdLrVwEnRpjhMUt4eAcsNeA30=
github.com/fumiama/go-base16384 v1.6.1/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM=
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/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak=
github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.0/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
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/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/guonaihong/gout v0.1.5 h1:1FeFFJWWdWYApBW9d6vzMDB4eR4Zr8T/gaVrjDVcl5U=
github.com/guonaihong/gout v0.1.5/go.mod h1:0rFYAYyzbcxEg11eY2qUbffJs7hHRPeugAnlVYSp8Ic=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
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/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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/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/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/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4 h1:u9jwvcKbQpghIXgNl/EOL8hzhAFXh4ePrEP493W3tNA=
github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4/go.mod h1:kcRFpEzolcEklV6rD7W95mG49/sbdX/PlFmd7ni3RvA=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
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/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/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-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
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/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
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.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA=
github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2 h1:BWVtt2VBY+lmVDu9MGKqLGKl04B+iRHcrW1Ptyi/8tg=
github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2/go.mod h1:lPnW9HVS0vJdeYyQtOvIvlXgZPNhUAhwz+z5r8AJk0Y=
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/go.mod h1:ecFKZPX81BaB70I6ruUgEwYcDOtuNgJGnjdK+MIl5ko=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE=
go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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 h1:f1CIuDlJhwANEC2MM87MBEVMr3jl5bifgsfj90XAF9c=
golang.org/x/sys v0.0.0-20201126233918-771906719818/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-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.ilharper.com/x/isatty v1.1.1 h1:RAg32Pxq/nIK4AVtdm9RBqxsxZZX1uRKRSS21E5SHMk=
gopkg.ilharper.com/x/isatty v1.1.1/go.mod h1:ofpv77Td5qQO6R1dmDd3oNt8TZdRo+l5gYAMxopRyS0=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/libc v1.8.1 h1:y9oPIhwcaFXxX7kMp6Qb2ZLKzr0mDkikWN3CV5GS63o=
modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
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.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=

View File

@ -1,19 +0,0 @@
package base
import (
"github.com/pkg/errors"
)
// silk encode features
var (
EncodeSilk = encodeSilk // 编码 SilkV3 音频
ResampleSilk = resampleSilk // 将silk重新编码为 24000 bit rate
)
func encodeSilk(_ []byte, _ string) ([]byte, error) {
return nil, errors.New("not supported now")
}
func resampleSilk(data []byte) []byte {
return data
}

View File

@ -1,129 +0,0 @@
// Package base provides base config for go-cqhttp
package base
import (
"flag"
"fmt"
"os"
"time"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"github.com/Mrs4s/go-cqhttp/modules/config"
)
// command flags
var (
LittleC string // config file
LittleD bool // daemon
LittleH bool // Help
LittleWD string // working directory
)
// config file flags
var (
Debug bool // 是否开启 debug 模式
RemoveReplyAt bool // 是否删除reply后的at
ExtraReplyData bool // 是否上报额外reply信息
IgnoreInvalidCQCode bool // 是否忽略无效CQ码
SplitURL bool // 是否分割URL
ForceFragmented bool // 是否启用强制分片
SkipMimeScan bool // 是否跳过Mime扫描
ConvertWebpImage bool // 是否转换Webp图片
ReportSelfMessage bool // 是否上报自身消息
UseSSOAddress bool // 是否使用服务器下发的新地址进行重连
LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志
LogColorful bool // 是否启用日志颜色
FastStart bool // 是否为快速启动
AllowTempSession bool // 是否允许发送临时会话信息
UpdateProtocol bool // 是否更新协议
SignServerOverwrite string // 使用特定的服务器进行签名
PostFormat string // 上报格式 string or array
Proxy string // 存储 proxy_rewrite,用于设置代理
PasswordHash [16]byte // 存储QQ密码哈希供登录使用
AccountToken []byte // 存储 AccountToken 供登录使用
Account *config.Account // 账户配置
Reconnect *config.Reconnect // 重连配置
LogLevel string // 日志等级
LogAging = time.Hour * 24 * 365 // 日志时效
HeartbeatInterval = time.Second * 5 // 心跳间隔
Servers []map[string]yaml.Node // 连接服务列表
Database map[string]yaml.Node // 数据库列表
)
// Parse parse flags
func Parse() {
flag.StringVar(&LittleC, "c", "config.yml", "configuration filename")
flag.BoolVar(&LittleD, "d", false, "running as a daemon")
flag.BoolVar(&LittleH, "h", false, "this Help")
flag.StringVar(&LittleWD, "w", "", "cover the working directory")
d := flag.Bool("D", false, "debug mode")
flag.BoolVar(&FastStart, "faststart", false, "skip waiting 5 seconds")
flag.BoolVar(&UpdateProtocol, "update-protocol", false, "update protocol")
flag.StringVar(&SignServerOverwrite, "sign-server", "", "use special server to sign tlv")
flag.Parse()
if *d {
Debug = true
}
}
// Init read config from yml file
func Init() {
conf := config.Parse(LittleC)
{ // bool config
if conf.Output.Debug {
Debug = true
}
IgnoreInvalidCQCode = conf.Message.IgnoreInvalidCQCode
SplitURL = conf.Message.FixURL
RemoveReplyAt = conf.Message.RemoveReplyAt
ExtraReplyData = conf.Message.ExtraReplyData
ForceFragmented = conf.Message.ForceFragment
SkipMimeScan = conf.Message.SkipMimeScan
ConvertWebpImage = conf.Message.ConvertWebpImage
ReportSelfMessage = conf.Message.ReportSelfMessage
UseSSOAddress = conf.Account.UseSSOAddress
AllowTempSession = conf.Account.AllowTempSession
}
{ // others
Proxy = conf.Message.ProxyRewrite
Account = conf.Account
Reconnect = conf.Account.ReLogin
Servers = conf.Servers
Database = conf.Database
LogLevel = conf.Output.LogLevel
LogColorful = conf.Output.LogColorful == nil || *conf.Output.LogColorful
if conf.Message.PostFormat != "string" && conf.Message.PostFormat != "array" {
log.Warnf("post-format 配置错误, 将自动使用 string")
PostFormat = "string"
} else {
PostFormat = conf.Message.PostFormat
}
if conf.Output.LogAging > 0 {
LogAging = time.Hour * 24 * time.Duration(conf.Output.LogAging)
}
if conf.Heartbeat.Interval > 0 {
HeartbeatInterval = time.Second * time.Duration(conf.Heartbeat.Interval)
}
if conf.Heartbeat.Disabled || conf.Heartbeat.Interval < 0 {
HeartbeatInterval = 0
}
}
}
// Help cli命令行-h的帮助提示
func Help() {
fmt.Printf(`go-cqhttp service
version: %s
Usage:
server [OPTIONS]
Options:
`, Version)
flag.PrintDefaults()
os.Exit(0)
}

View File

@ -1,16 +0,0 @@
package base
import "runtime/debug"
// Version go-cqhttp的版本信息在编译时使用ldflags进行覆盖
var Version = "unknown"
func init() {
if Version != "unknown" {
return
}
info, ok := debug.ReadBuildInfo()
if ok {
Version = info.Main.Version
}
}

View File

@ -1,51 +0,0 @@
// Package cache impl the cache for gocq
package cache
import (
log "github.com/sirupsen/logrus"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
// Media Cache DBs
var (
Image Cache
Video Cache
// todo: Voice?
)
// Cache wraps the btree.DB for concurrent safe
type Cache struct {
ldb *leveldb.DB
}
// Insert 添加媒体缓存
func (c *Cache) Insert(md5, data []byte) {
_ = c.ldb.Put(md5, data, nil)
}
// Get 获取缓存信息
func (c *Cache) Get(md5 []byte) []byte {
got, _ := c.ldb.Get(md5, nil)
return got
}
// Delete 删除指定缓存
func (c *Cache) Delete(md5 []byte) {
_ = c.ldb.Delete(md5, nil)
}
// Init 初始化 Cache
func Init() {
open := func(typ, path string, cache *Cache) {
ldb, err := leveldb.OpenFile(path, &opt.Options{
WriteBuffer: 4 * opt.KiB,
})
if err != nil {
log.Fatalf("open cache %s db failed: %v", typ, err)
}
cache.ldb = ldb
}
open("image", "data/images", &Image)
open("video", "data/videos", &Video)
}

View File

@ -1,308 +0,0 @@
// Package download provide download utility functions
package download
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
var client = &http.Client{
Transport: &http.Transport{
Proxy: func(request *http.Request) (u *url.URL, e error) {
if base.Proxy == "" {
return http.ProxyFromEnvironment(request)
}
return url.Parse(base.Proxy)
},
ForceAttemptHTTP2: false,
MaxConnsPerHost: 0,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 999,
},
Timeout: time.Second * 15,
}
// ErrOverSize 响应主体过大时返回此错误
var ErrOverSize = errors.New("oversize")
// UserAgent HTTP请求时使用的UA
const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66"
// Request is a file download request
type Request struct {
Method string
URL string
Header map[string]string
Limit int64
Body io.Reader
}
func (r Request) do() (*http.Response, error) {
if r.Method == "" {
r.Method = http.MethodGet
}
req, err := http.NewRequest(r.Method, r.URL, r.Body)
if err != nil {
return nil, err
}
req.Header["User-Agent"] = []string{UserAgent}
for k, v := range r.Header {
req.Header.Set(k, v)
}
return client.Do(req)
}
func (r Request) body() (io.ReadCloser, error) {
resp, err := r.do()
if err != nil {
return nil, err
}
limit := r.Limit // check file size limit
if limit > 0 && resp.ContentLength > limit {
_ = resp.Body.Close()
return nil, ErrOverSize
}
if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
return gzipReadCloser(resp.Body)
}
return resp.Body, err
}
// Bytes 对给定URL发送请求返回响应主体
func (r Request) Bytes() ([]byte, error) {
rd, err := r.body()
if err != nil {
return nil, err
}
defer rd.Close()
return io.ReadAll(rd)
}
// JSON 发送请求, 并转换响应为JSON
func (r Request) JSON() (gjson.Result, error) {
rd, err := r.body()
if err != nil {
return gjson.Result{}, err
}
defer rd.Close()
var sb strings.Builder
_, err = io.Copy(&sb, rd)
if err != nil {
return gjson.Result{}, err
}
return gjson.Parse(sb.String()), nil
}
func writeToFile(reader io.ReadCloser, path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o644)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = file.ReadFrom(reader)
return err
}
// WriteToFile 下载到制定目录
func (r Request) WriteToFile(path string) error {
rd, err := r.body()
if err != nil {
return err
}
defer rd.Close()
return writeToFile(rd, path)
}
// WriteToFileMultiThreading 多线程下载到制定目录
func (r Request) WriteToFileMultiThreading(path string, thread int) error {
if thread < 2 {
return r.WriteToFile(path)
}
limit := r.Limit
type BlockMetaData struct {
BeginOffset int64
EndOffset int64
DownloadedSize int64
}
var blocks []*BlockMetaData
var contentLength int64
errUnsupportedMultiThreading := errors.New("unsupported multi-threading")
// 初始化分块或直接下载
initOrDownload := func() error {
header := make(map[string]string, len(r.Header))
for k, v := range r.Header { // copy headers
header[k] = v
}
header["range"] = "bytes=0-"
req := Request{
URL: r.URL,
Header: header,
}
resp, err := req.do()
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
}
if resp.StatusCode == http.StatusOK {
if limit > 0 && resp.ContentLength > limit {
return ErrOverSize
}
if err = writeToFile(resp.Body, path); err != nil {
return err
}
return errUnsupportedMultiThreading
}
if resp.StatusCode == http.StatusPartialContent {
contentLength = resp.ContentLength
if limit > 0 && resp.ContentLength > limit {
return ErrOverSize
}
blockSize := contentLength
if contentLength > 1024*1024 {
blockSize = (contentLength / int64(thread)) - 10
}
if blockSize == contentLength {
return writeToFile(resp.Body, path)
}
var tmp int64
for tmp+blockSize < contentLength {
blocks = append(blocks, &BlockMetaData{
BeginOffset: tmp,
EndOffset: tmp + blockSize - 1,
})
tmp += blockSize
}
blocks = append(blocks, &BlockMetaData{
BeginOffset: tmp,
EndOffset: contentLength - 1,
})
return nil
}
return errors.New("unknown status code")
}
// 下载分块
downloadBlock := func(block *BlockMetaData) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return err
}
defer file.Close()
_, _ = file.Seek(block.BeginOffset, io.SeekStart)
writer := bufio.NewWriter(file)
defer writer.Flush()
header := make(map[string]string, len(r.Header))
for k, v := range r.Header { // copy headers
header[k] = v
}
header["range"] = fmt.Sprintf("bytes=%d-%d", block.BeginOffset, block.EndOffset)
req := Request{
URL: r.URL,
Header: header,
}
resp, err := req.do()
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
}
buffer := make([]byte, 1024)
i, err := resp.Body.Read(buffer)
for {
if err != nil && err != io.EOF {
return err
}
i64 := int64(len(buffer[:i]))
needSize := block.EndOffset + 1 - block.BeginOffset
if i64 > needSize {
i64 = needSize
err = io.EOF
}
_, e := writer.Write(buffer[:i64])
if e != nil {
return e
}
block.BeginOffset += i64
block.DownloadedSize += i64
if err == io.EOF || block.BeginOffset > block.EndOffset {
break
}
i, err = resp.Body.Read(buffer)
}
return nil
}
if err := initOrDownload(); err != nil {
if err == errUnsupportedMultiThreading {
return nil
}
return err
}
wg := sync.WaitGroup{}
wg.Add(len(blocks))
var lastErr error
for i := range blocks {
go func(b *BlockMetaData) {
defer wg.Done()
if err := downloadBlock(b); err != nil {
lastErr = err
}
}(blocks[i])
}
wg.Wait()
return lastErr
}
type gzipCloser struct {
f io.Closer
r *gzip.Reader
}
// gzipReadCloser 从 io.ReadCloser 创建 gunzip io.ReadCloser
func gzipReadCloser(reader io.ReadCloser) (io.ReadCloser, error) {
gzipReader, err := gzip.NewReader(reader)
if err != nil {
return nil, err
}
return &gzipCloser{
f: reader,
r: gzipReader,
}, nil
}
// Read impls io.Reader
func (g *gzipCloser) Read(p []byte) (n int, err error) {
return g.r.Read(p)
}
// Close impls io.Closer
func (g *gzipCloser) Close() error {
_ = g.f.Close()
return g.r.Close()
}

View File

@ -1,3 +0,0 @@
package encryption
var T544Signer = map[string]func(int64, []byte) []byte{}

View File

@ -1,10 +0,0 @@
//go:build amd64
package t544
func cpuid(op uint32) (eax, ebx, ecx, edx uint32)
var canusesse2 = func() bool {
_, _, _, d := cpuid(1)
return d&(1<<26) > 0
}()

View File

@ -1,15 +0,0 @@
//go:build amd64
// +build amd64
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
// func cpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·cpuid(SB), 7, $0
XORQ CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET

File diff suppressed because one or more lines are too long

View File

@ -1,53 +0,0 @@
//go:build amd64
package t544
import (
"encoding/binary"
"hash/crc32"
"io"
)
var crc32Table = func() (tab crc32.Table) {
f, err := cryptoZip.Open("crc32.bin")
if err != nil {
panic(err)
}
data, err := io.ReadAll(f)
if err != nil {
panic(err)
}
for i := range tab {
tab[i] = binary.LittleEndian.Uint32(data[i*4 : (i+1)*4])
}
return
}()
func tencentCrc32(tab *crc32.Table, b []byte) uint32
func sub_a([]byte, []uint32)
func sub_b([]byte, []uint32)
func sub_c(*[16][16]byte, []byte)
func sub_d(*[16]byte, []byte)
func sub_e(*[256][6]byte, []byte)
func sub_f(*[16]byte, *[15]uint32, *[16][16]byte) (w [44]uint32)
func sub_aa(int, *[16][2][16][16]byte, *[16]byte, []byte) byte
// transformInner see com/tencent/mobileqq/dt/model/FEBound
func transformInner(*[0x15]byte, *[32][16]byte)
func initState(*state, []byte, []byte, uint64)
func (c *state) init(key []byte, data []byte, counter uint64, nr uint8) {
c.nr = nr
c.p = 0
initState(c, key, data, counter)
}
func sub_ad([]uint32)

View File

@ -1,637 +0,0 @@
//go:build amd64
// +build amd64
#include "textflag.h"
DATA LC0<>+0(SB)/4, $1634760805
DATA LC0<>+4(SB)/4, $857760878
DATA LC0<>+8(SB)/4, $2036477234
DATA LC0<>+12(SB)/4, $1797285236
GLOBL LC0<>(SB), NOPTR, $16
TEXT ·sub_a(SB), NOSPLIT, $0-48
MOVQ ·a+0(FP), DI
MOVQ ·b+24(FP), CX
MOVQ CX, DX
MOVBLZX 3(CX), CX
XORB CX, (DI)
MOVBLZX 2(DX), CX
XORB CX, 1(DI)
MOVBLZX 1(DX), CX
XORB CX, 2(DI)
MOVBLZX (DX), CX
XORB CX, 3(DI)
MOVBLZX 7(DX), CX
XORB CX, 4(DI)
MOVBLZX 6(DX), CX
XORB CX, 5(DI)
MOVBLZX 5(DX), CX
XORB CX, 6(DI)
MOVBLZX 4(DX), CX
XORB CX, 7(DI)
MOVBLZX 11(DX),CX
XORB CX, 8(DI)
MOVBLZX 10(DX),CX
XORB CX, 9(DI)
MOVBLZX 9(DX), CX
XORB CX,10(DI)
MOVBLZX 8(DX), CX
XORB CX,11(DI)
MOVBLZX 15(DX),CX
XORB CX,12(DI)
MOVBLZX 14(DX),CX
XORB CX,13(DI)
MOVBLZX 13(DX),CX
XORB CX,14(DI)
MOVBLZX 12(DX),DX
XORB DL,15(DI)
RET
TEXT ·sub_b(SB), NOSPLIT, $0-48
MOVQ ·a+0(FP), DI
MOVQ ·b+24(FP), CX
MOVQ CX, DX
MOVBLZX 3(CX), CX
XORB CX, (DI)
MOVBLZX 6(DX), CX
XORB CX, 1(DI)
MOVBLZX 9(DX), CX
XORB CX, 2(DI)
MOVBLZX 12(DX),CX
XORB CX, 3(DI)
MOVBLZX 7(DX), CX
XORB CX, 4(DI)
MOVBLZX 10(DX),CX
XORB CX, 5(DI)
MOVBLZX 13(DX),CX
XORB CX, 6(DI)
MOVBLZX (DX), CX
XORB CX,7(DI)
MOVBLZX 11(DX),CX
XORB CX,8(DI)
MOVBLZX 14(DX),CX
XORB CX,9(DI)
MOVBLZX 1(DX), CX
XORB CX,10(DI)
MOVBLZX 4(DX), CX
XORB CX,11(DI)
MOVBLZX 15(DX),CX
XORB CX,12(DI)
MOVBLZX 2(DX), CX
XORB CX,13(DI)
MOVBLZX 5(DX), CX
XORB CX,14(DI)
MOVBLZX 8(DX), DX
XORB DL,15(DI)
RET
TEXT ·sub_c(SB), NOSPLIT, $0-32
MOVQ ·a+0(FP), DI
MOVQ ·b+8(FP), SI
MOVQ SI, AX
MOVBLZX (SI), SI
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 1(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, (AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 2(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 1(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 3(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 2(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 4(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 3(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 5(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 4(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 6(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 5(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 7(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 6(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 8(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 7(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 9(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 8(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 10(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 9(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 11(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 10(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 12(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 11(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 13(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 12(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 14(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 13(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVBLZX 15(AX), SI
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 14(AX)
MOVL SI, CX
ANDL $15, SI
SHRL $4, CX
SHLL $4, CX
ADDL SI, CX
MOVLQSX CX, CX
MOVBLZX (DI)(CX*1), CX
MOVB CX, 15(AX)
RET
TEXT ·sub_d(SB), NOSPLIT, $16-32
MOVQ ·t+0(FP), BX
MOVQ ·s+8(FP), SI
MOVOU (SI), X0
MOVOU X0, in-16(SP)
MOVQ SI, DI
ADDQ $15, DI
MOVB $16, CX
STD
lop:
LEAQ -1(CX), AX
XLAT
MOVBLZX in-16(SP)(AX*1), AX
STOSB
LOOP lop
RET
TEXT ·sub_e(SB), NOSPLIT, $0-32
MOVQ ·a+0(FP), DI
MOVQ ·n+8(FP), SI
MOVQ $4, AX
lop:
MOVBQZX -4(SI)(AX*4), DX
MOVBQZX -3(SI)(AX*4), CX
MOVBQZX -2(SI)(AX*4), R10
MOVBQZX -1(SI)(AX*4), R8
LEAQ (DX)(DX*2), R9
LEAQ (R9*2), R9
LEAQ (CX)(CX*2), R11
LEAQ (R11*2), R11
LEAQ (R10)(R10*2), BX
LEAQ (BX*2), BX
MOVB DX, R13
XORB CX, DX
XORB R10, CX
MOVB (DI)(R9*1), R12
XORB 1(DI)(R11*1), R12
XORB R8, R10
XORB R12, R10
MOVB R10, -4(SI)(AX*4)
MOVB (DI)(R11*1), R10
XORB 1(DI)(BX*1), R10
XORB R8, R13
XORB R10, R13
MOVB R13, -3(SI)(AX*4)
MOVB (DI)(BX*1), R10
LEAQ (R8)(R8*2), R8
LEAQ (R8*2), R8
XORB 1(DI)(R8*1), R10
XORB R10, DX
MOVB DX, -2(SI)(AX*4)
MOVB 1(DI)(R9*1), DX
XORB (DI)(R8*1), DX
XORB DX, CX
MOVB CX, -1(SI)(AX*4)
DECB AX
JNZ lop
RET
TEXT sub_ab(SB), NOSPLIT, $0-24
MOVQ ·s+0(FP), DI
MOVQ ·w+8(FP), SI
MOVL SI, AX
MOVL SI, CX
MOVL SI, DX
SHRL $28, AX
SHRL $24, CX
ANDL $15, CX
SALL $4, AX
ADDL CX, AX
MOVBLZX SI, CX
MOVBLZX (DI)(AX*1), AX
MOVBLZX (DI)(CX*1), CX
SALL $24, AX
ORL CX, AX
MOVL SI, CX
SHRL $8, SI
SHRL $8, CX
ANDL $15, SI
ANDL $240, CX
ADDL SI, CX
MOVBLZX (DI)(CX*1), CX
SALL $8, CX
ORL CX, AX
MOVL DX, CX
SHRL $16, DX
SHRL $16, CX
ANDL $15, DX
ANDL $240, CX
ADDL CX, DX
MOVBLZX (DI)(DX*1), DX
SALL $16, DX
ORL DX, AX
MOVQ AX, ·retval+16(FP)
RET
TEXT ·sub_f(SB), NOSPLIT, $24-68
MOVQ ·k+0(FP), DI
MOVQ ·r+8(FP), SI
MOVQ ·s+16(FP), DX
MOVQ $·w+24(FP), CX
MOVQ CX, R10
MOVQ SI, R9
MOVQ DX, R8
MOVL $4, BX
MOVL (DI), AX
BSWAPL AX
MOVL AX, (CX)
MOVL 4(DI), AX
BSWAPL AX
MOVL AX, 4(CX)
MOVL 8(DI), AX
BSWAPL AX
MOVL AX, 8(CX)
MOVL 12(DI), AX
BSWAPL AX
MOVL AX, 12(CX)
JMP inner
for:
XORL -16(R10)(BX*4), AX
MOVL AX, (R10)(BX*4)
ADDQ $1, BX
CMPQ BX, $44
JE end
inner:
MOVL -4(R10)(BX*4), AX
TESTB $3, BX
JNE for
ROLL $8, AX
MOVQ R8, 0(SP)
MOVL AX, 8(SP)
CALL sub_ab(SB)
MOVQ 16(SP), AX
LEAL -1(BX), DX
SARL $2, DX
MOVLQSX DX, DX
XORL (R9)(DX*4), AX
JMP for
end:
RET
TEXT ·sub_aa(SB), NOSPLIT, $0-56
MOVQ ·i+0(FP), DI
MOVQ ·t+8(FP), SI
MOVQ ·b+16(FP), DX
MOVQ ·m+24(FP), CX
MOVL DI, AX
MOVLQSX DI, DI
MOVQ SI, R8
MOVQ DX, SI
MOVBLZX (CX)(DI*1), CX
ANDL $15, AX
MOVBLZX (SI)(AX*1), SI
MOVQ AX, DX
MOVL CX, AX
SALQ $9, DX
ANDL $15, CX
SHRB $4, AX
MOVL SI, DI
ADDQ R8, DX
SALQ $4, CX
ANDL $15, AX
SHRB $4, DI
ANDL $15, SI
SALQ $4, AX
ANDL $15, DI
ADDQ DX, AX
ADDQ CX, DX
MOVBLZX (AX)(DI*1), AX
SALL $4, AX
ORB 256(SI)(DX*1), AX
MOVQ AX, ·retval+48(FP)
RET
// func transformInner(x *[0x15]byte, tab *[32][16]byte)
TEXT ·transformInner(SB), NOSPLIT, $0-16
MOVQ ·x+0(FP), DI
MOVQ ·tab+8(FP), SI
MOVQ DI, AX
MOVL $1, CX
MOVQ SI, DI
MOVQ AX, SI
lop:
MOVBLZX (SI), R8
LEAL -1(CX), AX
ADDQ $1, SI
ANDL $31, AX
MOVL R8, DX
SALL $4, AX
ANDL $15, R8
SHRB $4, DX
MOVBLZX DX, DX
ADDL DX, AX
CDQE
MOVBLSX (DI)(AX*1), AX
SALL $4, AX
MOVL AX, DX
MOVL CX, AX
ADDL $2, CX
ANDL $31, AX
SALL $4, AX
ADDL R8, AX
CDQE
ORB (DI)(AX*1), DX
MOVB DX, -1(SI)
CMPL CX, $43
JNE lop
RET
TEXT ·initState(SB), NOSPLIT, $0-64
MOVQ ·c+0(FP), DI
MOVQ ·key+8(FP), SI
MOVQ ·data+32(FP), R8
MOVQ ·counter+56(FP), AX
MOVOA LC0<>(SB), X0
MOVUPS X0, (DI)
MOVOU (SI), X1
MOVOU (DI), X3
MOVUPS X1, 16(DI)
MOVOU 16(SI), X2
MOVQ AX, 48(DI)
MOVUPS X2, 32(DI)
MOVQ (R8), AX
MOVUPS X3, 64(DI)
MOVQ AX, 56(DI)
MOVQ 48(DI), AX
MOVUPS X1, 80(DI)
MOVUPS X2, 96(DI)
MOVUPS X6,112(DI)
RET
TEXT ·sub_ad(SB), NOSPLIT, $8-24
MOVQ ·a+0(FP), DI
MOVQ DI, AX
MOVL 40(DI), R10
MOVL 12(DI), R12
MOVL 44(DI), BP
MOVL 16(DI), DX
MOVL (DI), R15
MOVL 48(DI), R9
MOVL 20(DI), SI
MOVL 32(DI), R11
ADDL DX, R15
MOVL 4(DI), R14
MOVL 52(DI), R8
XORL R15, R9
MOVL 24(DI), CX
MOVL 8(DI), R13
ROLL $16, R9
ADDL SI, R14
MOVL 36(DI), BX
MOVL 56(DI), DI
ADDL R9, R11
XORL R14, R8
ADDL CX, R13
XORL R11, DX
ROLL $16, R8
XORL R13, DI
ROLL $12, DX
ADDL R8, BX
ROLL $16, DI
ADDL DX, R15
XORL BX, SI
ADDL DI, R10
XORL R15, R9
ROLL $12, SI
XORL R10, CX
ROLL $8, R9
ADDL SI, R14
ROLL $12, CX
ADDL R9, R11
XORL R14, R8
ADDL CX, R13
XORL R11, DX
ROLL $8, R8
XORL R13, DI
ROLL $7, DX
LEAL (BX)(R8*1), BX
ROLL $8, DI
MOVL DX, tmp0-8(SP)
MOVL 28(AX), DX
XORL BX, SI
MOVL BX, tmp1-4(SP)
MOVL R10, BX
MOVL 60(AX), R10
ROLL $7, SI
ADDL DI, BX
ADDL DX, R12
ADDL SI, R15
XORL R12, R10
XORL BX, CX
ROLL $16, R10
ROLL $7, CX
ADDL R10, BP
ADDL CX, R14
XORL BP, DX
XORL R14, R9
ROLL $12, DX
ROLL $16, R9
ADDL DX, R12
XORL R12, R10
ROLL $8, R10
ADDL R10, BP
XORL R15, R10
ROLL $16, R10
XORL BP, DX
ADDL R9, BP
ADDL R10, BX
ROLL $7, DX
XORL BP, CX
XORL BX, SI
ROLL $12, SI
ADDL SI, R15
XORL R15, R10
MOVL R15, (AX)
ROLL $8, R10
ADDL R10, BX
MOVL R10, 60(AX)
XORL BX, SI
MOVD BX, X1
ROLL $7, SI
ROLL $12, CX
ADDL DX, R13
XORL R13, R8
ADDL CX, R14
MOVL SI, 20(AX)
ROLL $16, R8
XORL R14, R9
MOVL R14, 4(AX)
ADDL R8, R11
ROLL $8, R9
XORL R11, DX
ADDL R9, BP
MOVL R9, 48(AX)
ROLL $12, DX
XORL BP, CX
MOVD BP, X2
ADDL DX, R13
ROLL $7, CX
PUNPCKLLQ X2, X1
XORL R13, R8
MOVL CX, 24(AX)
ROLL $8, R8
MOVL R13, 8(AX)
ADDL R8, R11
XORL R11, DX
MOVD R11, X0
ROLL $7, DX
MOVL DX, 28(AX)
MOVL R8, 52(AX)
MOVL tmp0-8(SP), SI
MOVL tmp1-4(SP), CX
ADDL SI, R12
XORL R12, DI
ROLL $16, DI
ADDL DI, CX
XORL CX, SI
MOVL SI, DX
ROLL $12, DX
ADDL DX, R12
XORL R12, DI
MOVL R12, 12(AX)
ROLL $8, DI
ADDL DI, CX
MOVL DI, 56(AX)
MOVD CX, X3
XORL CX, DX
PUNPCKLLQ X3, X0
ROLL $7, DX
PUNPCKLQDQ X1, X0
MOVL DX, 16(AX)
MOVUPS X0, 32(AX)
RET
// func tencentCrc32(tab *crc32.Table, b []byte) uint32
TEXT ·tencentCrc32(SB), NOSPLIT, $0-40
MOVQ ·tab+0(FP), DI
MOVQ ·bptr+8(FP), SI
MOVQ ·bngas+16(FP), DX
TESTQ DX, DX
JE quickend
ADDQ SI, DX
MOVL $-1, AX
lop:
MOVBLZX (SI), CX
ADDQ $1, SI
XORL AX, CX
SHRL $8, AX
MOVBLZX CX, CX
XORL (DI)(CX*4), AX
CMPQ SI, DX
JNE lop
NOTL AX
MOVQ AX, ·bngas+32(FP)
RET
quickend:
XORL AX, AX
RET

View File

@ -1,117 +0,0 @@
//go:build amd64
package t544
import (
"encoding/binary"
"io"
)
type encryptionData struct {
tableA [16][2][16][16]byte
tableB [16][16]byte
tableC [256][6]byte
tableD [16]byte
tableE [16]byte
tableF [15]uint32
}
type state struct {
state [16]uint32 // 16
orgstate [16]uint32 // 16
nr uint8
p uint8
}
var crypto = encryptionData{
tableA: readData[[16][2][16][16]byte]("table_a.bin"),
tableB: readData[[16][16]byte]("table_b.bin"),
tableC: readData[[256][6]byte]("table_c.bin"),
tableD: readData[[16]byte]("table_d.bin"),
tableE: readData[[16]byte]("table_e.bin"),
tableF: func() (tab [15]uint32) {
f, err := cryptoZip.Open("table_f.bin")
if err != nil {
panic(err)
}
data, err := io.ReadAll(f)
if err != nil {
panic(err)
}
for i := range tab {
tab[i] = binary.LittleEndian.Uint32(data[i*4 : (i+1)*4])
}
return
}(),
}
func (e *encryptionData) tencentEncryptB(p1 []byte, p2 []uint32) {
const c = 10
for r := 0; r < 9; r++ {
sub_d(&e.tableD, p1)
sub_b(p1, p2[r*4:(r+1)*4])
sub_c(&e.tableB, p1)
sub_e(&e.tableC, p1)
}
sub_d(&e.tableD, p1)
sub_b(p1, p2[(c-1)*4:c*4])
sub_c(&e.tableB, p1)
sub_a(p1, p2[c*4:(c+1)*4])
}
func (e *encryptionData) tencentEncryptionB(c []byte, m []byte) (out [0x15]byte) {
var buf [16]byte
w := sub_f(&e.tableE, &e.tableF, &e.tableB)
for i := range out {
if (i & 0xf) == 0 {
copy(buf[:], c)
e.tencentEncryptB(buf[:], w[:])
for j := 15; j >= 0; j-- {
c[j]++
if c[j] != 0 {
break
}
}
}
out[i] = sub_aa(i, &e.tableA, &buf, m)
}
return
}
func tencentEncryptionA(input, key, data []byte) {
var s state
s.init(key, data, 0, 20)
s.encrypt(input)
}
func (c *state) encrypt(data []byte) {
bp := 0
dataLen := uint32(len(data))
for dataLen > 0 {
if c.p == 0 {
for i := uint8(0); i < c.nr; i += 2 {
sub_ad(c.state[:])
}
for i := 0; i < 16; i++ {
c.state[i] += c.orgstate[i]
}
}
var sb [16 * 4]byte
for i, v := range c.state {
binary.LittleEndian.PutUint32(sb[i*4:(i+1)*4], v)
}
for c.p != 64 && dataLen != 0 {
data[bp] ^= sb[c.p]
c.p++
bp++
dataLen--
}
if c.p >= 64 {
c.p = 0
c.orgstate[12]++
c.state = c.orgstate
}
}
}

View File

@ -1,93 +0,0 @@
//go:build amd64
package t544
import (
"crypto/md5"
"crypto/rc4"
"encoding/binary"
"math/rand"
"unsafe"
"github.com/Mrs4s/go-cqhttp/internal/encryption"
)
const (
keyTable = "$%&()+,-456789:?ABCDEEFGHIJabcdefghijkopqrstuvwxyz"
table2 = "!#$%&)+.0123456789:=>?@ABCDEFGKMNabcdefghijkopqrst"
)
var (
magic = uint64(0x6EEDCF0DC4675540)
key1 = [8]byte{'a', '$', '(', 'e', 'T', '7', '*', '@'}
key2 = [8]byte{'&', 'O', '9', '!', '>', '6', 'X', ')'}
)
func init() {
if canusesse2 {
encryption.T544Signer["8.9.35.10440"] = sign
encryption.T544Signer["8.9.38.10545"] = sign
}
}
// sign t544 algorithm
// special thanks to the anonymous contributor who provided the algorithm
func sign(curr int64, input []byte) []byte {
curr %= 1000000
input = append(input, []byte{byte(curr >> 24), byte(curr >> 16), byte(curr >> 8), byte(curr)}...)
var kt [4 + 32 + 4]byte
r := rand.New(rand.NewSource(curr))
for i := 0; i < 2; i++ {
kt[i] = keyTable[r.Int()%0x32] + 50
}
kt[2] = kt[1] + 20
kt[3] = kt[2] + 20
key3 := kt[4 : 4+10]
k3calc := key3[2:10]
copy(k3calc, key1[:4])
for i := 0; i < 4; i++ {
k3calc[4+i] = key2[i] ^ kt[i]
}
key3[0], key3[1] = k3calc[6], k3calc[7]
key3 = key3[:8]
k3calc[6], k3calc[7] = 0, 0
rc4Cipher, _ := rc4.NewCipher(key3)
rc4Cipher.XORKeyStream(key3, key3)
var crcData [0x15]byte
copy(crcData[4:4+8], (*[8]byte)(unsafe.Pointer(&magic))[:])
tencentEncryptionA(input, kt[4:4+32], crcData[4:4+8])
result := md5.Sum(input)
crcData[2] = 1
crcData[4] = 1
copy(crcData[5:9], kt[:4])
binary.BigEndian.PutUint32(crcData[9:13], uint32(curr))
copy(crcData[13:], result[:8])
calcCrc := tencentCrc32(&crc32Table, crcData[2:])
copy(kt[4+32:4+32+4], (*[4]byte)(unsafe.Pointer(&calcCrc))[:])
crcData[0] = kt[4+32]
crcData[1] = kt[4+32+3]
nonce := uint32(r.Int() ^ r.Int() ^ r.Int())
on := kt[:16]
binary.BigEndian.PutUint32(on[:4], nonce)
copy(on[4:8], on[:4])
copy(on[8:16], on[:8])
ts.transformEncode(&crcData)
encryptedData := crypto.tencentEncryptionB(on, crcData[:])
ts.transformDecode(&encryptedData)
output := kt[:39]
output[0] = 0x0C
output[1] = 0x05
binary.BigEndian.PutUint32(output[2:6], nonce)
copy(output[6:27], encryptedData[:])
binary.LittleEndian.PutUint32(output[27:31], 0)
output[31] = table2[r.Int()%0x32]
output[32] = table2[r.Int()%0x32]
addition := r.Int() % 9
for addition&1 == 0 {
addition = r.Int() % 9
}
output[33] = output[31] + byte(addition)
output[34] = output[32] + byte(9-addition) + 1
binary.LittleEndian.PutUint32(output[35:39], 0)
return output
}

View File

@ -1,7 +0,0 @@
//go:build !amd64
package t544
func init() {
}

View File

@ -1,21 +0,0 @@
//go:build amd64
package t544
type transformer struct {
encode [32][16]byte
decode [32][16]byte
}
func (ts *transformer) transformEncode(bArr *[0x15]byte) {
transformInner(bArr, &ts.encode)
}
func (ts *transformer) transformDecode(bArr *[0x15]byte) {
transformInner(bArr, &ts.decode)
}
var ts = transformer{
encode: readData[[32][16]byte]("encode.bin"),
decode: readData[[32][16]byte]("decode.bin"),
}

View File

@ -1,51 +0,0 @@
// Package mime 提供MIME检查功能
package mime
import (
"io"
"net/http"
"strings"
"github.com/Mrs4s/go-cqhttp/internal/base"
)
const limit = 4 * 1024
func scan(r io.ReadSeeker) string {
_, _ = r.Seek(0, io.SeekStart)
defer r.Seek(0, io.SeekStart)
in := make([]byte, limit)
_, _ = r.Read(in)
return http.DetectContentType(in)
}
// CheckImage 判断给定流是否为合法图片
// 返回 是否合法, 实际Mime
// 判断后会自动将 Stream Seek 至 0
func CheckImage(r io.ReadSeeker) (t string, ok bool) {
if base.SkipMimeScan {
return "", true
}
if r == nil {
return "image/nil-stream", false
}
t = scan(r)
switch t {
case "image/bmp", "image/gif", "image/jpeg", "image/png", "image/webp":
ok = true
}
return
}
// CheckAudio 判断给定流是否为合法音频
func CheckAudio(r io.ReadSeeker) (string, bool) {
if base.SkipMimeScan {
return "", true
}
t := scan(r)
// std mime type detection is not full supported for audio
if strings.Contains(t, "text") || strings.Contains(t, "image") {
return t, false
}
return t, true
}

View File

@ -1,246 +0,0 @@
// Package msg 提供了go-cqhttp消息中间表示CQ码处理等等
package msg
import (
"bytes"
"strings"
"unicode/utf8"
"github.com/Mrs4s/MiraiGo/binary"
)
// @@@ CQ码转义处理 @@@
// EscapeText 将字符串raw中部分字符转义
//
// - & -> &amp;
// - [ -> &#91;
// - ] -> &#93;
func EscapeText(s string) string {
count := strings.Count(s, "&")
count += strings.Count(s, "[")
count += strings.Count(s, "]")
if count == 0 {
return s
}
// Apply replacements to buffer.
var b strings.Builder
b.Grow(len(s) + count*4)
start := 0
for i := 0; i < count; i++ {
j := start
for index, r := range s[start:] {
if r == '&' || r == '[' || r == ']' {
j += index
break
}
}
b.WriteString(s[start:j])
switch s[j] {
case '&':
b.WriteString("&amp;")
case '[':
b.WriteString("&#91;")
case ']':
b.WriteString("&#93;")
}
start = j + 1
}
b.WriteString(s[start:])
return b.String()
}
// EscapeValue 将字符串value中部分字符转义
//
// - , -> &#44;
// - & -> &amp;
// - [ -> &#91;
// - ] -> &#93;
func EscapeValue(value string) string {
ret := EscapeText(value)
return strings.ReplaceAll(ret, ",", "&#44;")
}
// UnescapeText 将字符串content中部分字符反转义
//
// - &amp; -> &
// - &#91; -> [
// - &#93; -> ]
func UnescapeText(content string) string {
ret := content
ret = strings.ReplaceAll(ret, "&#91;", "[")
ret = strings.ReplaceAll(ret, "&#93;", "]")
ret = strings.ReplaceAll(ret, "&amp;", "&")
return ret
}
// UnescapeValue 将字符串content中部分字符反转义
//
// - &#44; -> ,
// - &amp; -> &
// - &#91; -> [
// - &#93; -> ]
func UnescapeValue(content string) string {
ret := strings.ReplaceAll(content, "&#44;", ",")
return UnescapeText(ret)
}
// @@@ 消息中间表示 @@@
// Pair key value pair
type Pair struct {
K string
V string
}
// Element single message
type Element struct {
Type string
Data []Pair
}
// Get 获取指定值
func (e *Element) Get(k string) string {
for _, datum := range e.Data {
if datum.K == k {
return datum.V
}
}
return ""
}
// CQCode convert element to cqcode
func (e *Element) CQCode() string {
buf := strings.Builder{}
e.WriteCQCodeTo(&buf)
return buf.String()
}
// WriteCQCodeTo write element's cqcode into sb
func (e *Element) WriteCQCodeTo(sb *strings.Builder) {
if e.Type == "text" {
sb.WriteString(EscapeText(e.Data[0].V)) // must be {"text": value}
return
}
sb.WriteString("[CQ:")
sb.WriteString(e.Type)
for _, data := range e.Data {
sb.WriteByte(',')
sb.WriteString(data.K)
sb.WriteByte('=')
sb.WriteString(EscapeValue(data.V))
}
sb.WriteByte(']')
}
// MarshalJSON see encoding/json.Marshaler
func (e *Element) MarshalJSON() ([]byte, error) {
return binary.NewWriterF(func(w *binary.Writer) {
buf := (*bytes.Buffer)(w)
// fmt.Fprintf(buf, `{"type":"%s","data":{`, e.Type)
buf.WriteString(`{"type":"`)
buf.WriteString(e.Type)
buf.WriteString(`","data":{`)
for i, data := range e.Data {
if i != 0 {
buf.WriteByte(',')
}
// fmt.Fprintf(buf, `"%s":%q`, data.K, data.V)
buf.WriteByte('"')
buf.WriteString(data.K)
buf.WriteString(`":`)
buf.WriteString(QuoteJSON(data.V))
}
buf.WriteString(`}}`)
}), nil
}
const hex = "0123456789abcdef"
// QuoteJSON 按JSON转义为字符加上双引号
func QuoteJSON(s string) string {
i, j := 0, 0
var b strings.Builder
b.WriteByte('"')
for j < len(s) {
c := s[j]
if c >= 0x20 && c <= 0x7f && c != '\\' && c != '"' {
// fast path: most of the time, printable ascii characters are used
j++
continue
}
switch c {
case '\\', '"', '\n', '\r', '\t':
b.WriteString(s[i:j])
b.WriteByte('\\')
switch c {
case '\n':
c = 'n'
case '\r':
c = 'r'
case '\t':
c = 't'
}
b.WriteByte(c)
j++
i = j
continue
case '<', '>', '&':
b.WriteString(s[i:j])
b.WriteString(`\u00`)
b.WriteByte(hex[c>>4])
b.WriteByte(hex[c&0xF])
j++
i = j
continue
}
// This encodes bytes < 0x20 except for \t, \n and \r.
if c < 0x20 {
b.WriteString(s[i:j])
b.WriteString(`\u00`)
b.WriteByte(hex[c>>4])
b.WriteByte(hex[c&0xF])
j++
i = j
continue
}
r, size := utf8.DecodeRuneInString(s[j:])
if r == utf8.RuneError && size == 1 {
b.WriteString(s[i:j])
b.WriteString(`\ufffd`)
j += size
i = j
continue
}
switch r {
case '\u2028', '\u2029':
// U+2028 is LINE SEPARATOR.
// U+2029 is PARAGRAPH SEPARATOR.
// They are both technically valid characters in JSON strings,
// but don't work in JSONP, which has to be evaluated as JavaScript,
// and can lead to security holes there. It is valid JSON to
// escape them, so we do so unconditionally.
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
b.WriteString(s[i:j])
b.WriteString(`\u202`)
b.WriteByte(hex[r&0xF])
j += size
i = j
continue
}
j += size
}
b.WriteString(s[i:])
b.WriteByte('"')
return b.String()
}

View File

@ -1,29 +0,0 @@
package msg
import (
"encoding/json"
"testing"
)
func jsonMarshal(s string) string {
b, err := json.Marshal(s)
if err != nil {
panic(err)
}
return string(b)
}
func TestQuoteJSON(t *testing.T) {
testcase := []string{
"\u0005", // issue 1773
"\v",
}
for _, input := range testcase {
got := QuoteJSON(input)
expected := jsonMarshal(input)
if got != expected {
t.Errorf("want %v but got %v", expected, got)
}
}
}

View File

@ -1,44 +0,0 @@
package msg
import (
"io"
"github.com/Mrs4s/MiraiGo/message"
)
// Poke 拍一拍
type Poke struct {
Target int64
}
// Type 获取元素类型ID
func (e *Poke) Type() message.ElementType {
// Make message.IMessageElement Happy
return message.At
}
// LocalImage 本地图片
type LocalImage struct {
Stream io.ReadSeeker
File string
URL string
Flash bool
EffectID int32
}
// Type implements the message.IMessageElement.
func (e *LocalImage) Type() message.ElementType {
return message.Image
}
// LocalVideo 本地视频
type LocalVideo struct {
File string
Thumb io.ReadSeeker
}
// Type impl message.IMessageElement
func (e *LocalVideo) Type() message.ElementType {
return message.Video
}

View File

@ -1,104 +0,0 @@
package msg
import (
"github.com/tidwall/gjson"
)
// ParseObject 将消息JSON对象转为消息元素数组
func ParseObject(m gjson.Result) (r []Element) {
convert := func(e gjson.Result) {
var elem Element
elem.Type = e.Get("type").Str
e.Get("data").ForEach(func(key, value gjson.Result) bool {
pair := Pair{K: key.Str, V: value.String()}
elem.Data = append(elem.Data, pair)
return true
})
r = append(r, elem)
}
if m.IsArray() {
m.ForEach(func(_, e gjson.Result) bool {
convert(e)
return true
})
}
if m.IsObject() {
convert(m)
}
return
}
func text(txt string) Element {
return Element{
Type: "text",
Data: []Pair{
{
K: "text",
V: txt,
},
},
}
}
// ParseString 将字符串(CQ码)转为消息元素数组
func ParseString(raw string) (r []Element) {
var elem Element
for raw != "" {
i := 0
for i < len(raw) && !(raw[i] == '[' && i+4 < len(raw) && raw[i:i+4] == "[CQ:") {
i++
}
if i > 0 {
r = append(r, text(UnescapeText(raw[:i])))
}
if i+4 > len(raw) {
return
}
raw = raw[i+4:] // skip "[CQ:"
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
elem.Type = raw[:i]
elem.Data = nil // reset data
raw = raw[i:]
i = 0
for {
if raw[0] == ']' {
r = append(r, elem)
raw = raw[1:]
break
}
raw = raw[1:]
for i < len(raw) && raw[i] != '=' {
i++
}
if i+1 > len(raw) {
return
}
key := raw[:i]
raw = raw[i+1:] // skip "="
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
elem.Data = append(elem.Data, Pair{
K: key,
V: UnescapeValue(raw[:i]),
})
raw = raw[i:]
i = 0
}
}
return
}

View File

@ -1,57 +0,0 @@
package msg
import (
"fmt"
"strings"
"testing"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
func TestParseString(t *testing.T) {
// TODO: add more text
for _, v := range ParseString(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`) {
fmt.Println(v)
}
}
var (
bench = `asdfqwerqwerqwer[CQ:face,id=115,text=111]asdfasdfasdfasdfasdfasdfasd[CQ:face,id=217]&#93; 123 &#91;`
benchArray = gjson.Parse(`[{"type":"text","data":{"text":"asdfqwerqwerqwer"}},{"type":"face","data":{"id":"115","text":"111"}},{"type":"text","data":{"text":"asdfasdfasdfasdfasdfasdfasd"}},{"type":"face","data":{"id":"217"}},{"type":"text","data":{"text":"] "}},{"type":"text","data":{"text":"123"}},{"type":"text","data":{"text":" ["}}]`)
)
func BenchmarkParseString(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseString(bench)
}
b.SetBytes(int64(len(bench)))
}
func BenchmarkParseObject(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseObject(benchArray)
}
b.SetBytes(int64(len(benchArray.Raw)))
}
const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&987654321[]&`
func BenchmarkCQCodeEscapeText(b *testing.B) {
for i := 0; i < b.N; i++ {
ret := bText
EscapeText(ret)
}
}
func TestCQCodeEscapeText(t *testing.T) {
for i := 0; i < 200; i++ {
rs := utils.RandomStringRange(3000, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890[]&")
ret := rs
ret = strings.ReplaceAll(ret, "&", "&amp;")
ret = strings.ReplaceAll(ret, "[", "&#91;")
ret = strings.ReplaceAll(ret, "]", "&#93;")
assert.Equal(t, ret, EscapeText(rs))
}
}

View File

@ -1,83 +0,0 @@
// Package param provide some util for param parse
package param
import (
"math"
"regexp"
"strings"
"sync"
"github.com/tidwall/gjson"
)
// EnsureBool 判断给定的p是否可表示为合法Bool类型,否则返回defaultVal
//
// 支持的合法类型有
//
// type bool
//
// type gjson.True or gjson.False
//
// type string "true","yes","1" or "false","no","0" (case insensitive)
func EnsureBool(p any, defaultVal bool) bool {
var str string
if b, ok := p.(bool); ok {
return b
}
if j, ok := p.(gjson.Result); ok {
if !j.Exists() {
return defaultVal
}
switch j.Type { // nolint: exhaustive
case gjson.True:
return true
case gjson.False:
return false
case gjson.String:
str = j.Str
default:
return defaultVal
}
} else if s, ok := p.(string); ok {
str = s
}
str = strings.ToLower(str)
switch str {
case "true", "yes", "1":
return true
case "false", "no", "0":
return false
default:
return defaultVal
}
}
var (
// once lazy compile the reg
once sync.Once
// reg is splitURL regex pattern.
reg *regexp.Regexp
)
// SplitURL 将给定URL字符串分割为两部分用于URL预处理防止风控
func SplitURL(s string) []string {
once.Do(func() { // lazy init.
reg = regexp.MustCompile(`(?i)[a-z\d][-a-z\d]{0,62}(\.[a-z\d][-a-z\d]{0,62})+\.?`)
})
idx := reg.FindAllStringIndex(s, -1)
if len(idx) == 0 {
return []string{s}
}
var result []string
last := 0
for i := 0; i < len(idx); i++ {
if len(idx[i]) != 2 {
continue
}
m := int(math.Abs(float64(idx[i][0]-idx[i][1]))/1.5) + idx[i][0]
result = append(result, s[last:m])
last = m
}
result = append(result, s[last:])
return result
}

View File

@ -1,81 +0,0 @@
// Package selfdiagnosis 自我诊断相关
package selfdiagnosis
import (
"github.com/Mrs4s/MiraiGo/client"
log "github.com/sirupsen/logrus"
)
// NetworkDiagnosis 诊断网络状态并输出结果
func NetworkDiagnosis(c *client.QQClient) {
log.Infof("开始诊断网络情况")
qualityInfo := c.ConnectionQualityTest()
log.Debugf("聊天服务器连接延迟: %vms", qualityInfo.ChatServerLatency)
log.Debugf("聊天服务器丢包率: %v%%", qualityInfo.ChatServerPacketLoss*10)
log.Debugf("长消息服务器连接延迟: %vms", qualityInfo.LongMessageServerLatency)
log.Debugf("长消息服务器响应延迟: %vms", qualityInfo.LongMessageServerResponseLatency)
log.Debugf("媒体服务器连接延迟: %vms", qualityInfo.SrvServerLatency)
log.Debugf("媒体服务器丢包率: %v%%", qualityInfo.SrvServerPacketLoss*10)
const (
chatServerErrorMessage = "可能出现消息丢失/延迟或频繁掉线等情况, 请检查本地网络状态."
longMessageServerErrorMessage = "可能导致无法接收/发送长消息的情况, 请检查本地网络状态."
mediaServerErrorMessage = "可能导致无法上传/下载媒体文件, 无法上传群共享, 无法发送消息等情况, 请检查本地网络状态."
)
if qualityInfo.ChatServerLatency > 1000 {
if qualityInfo.ChatServerLatency == 9999 {
log.Errorf("错误: 聊天服务器延迟测试失败, %v", chatServerErrorMessage)
} else {
log.Warnf("警告: 聊天服务器延迟为 %vms大于 1000ms, %v", qualityInfo.ChatServerLatency, chatServerErrorMessage)
}
}
if qualityInfo.ChatServerPacketLoss > 0 {
log.Warnf("警告: 本地连接聊天服务器丢包率为 %v%%, %v", qualityInfo.ChatServerPacketLoss*10, chatServerErrorMessage)
}
if qualityInfo.LongMessageServerLatency > 1000 {
if qualityInfo.LongMessageServerLatency == 9999 {
log.Errorf("错误: 长消息服务器延迟测试失败, %v 如果您使用的腾讯云服务器, 请修改DNS到114.114.114.114", longMessageServerErrorMessage)
} else {
log.Warnf("警告: 长消息延迟为 %vms, 大于 1000ms, %v", qualityInfo.LongMessageServerLatency, longMessageServerErrorMessage)
}
}
if qualityInfo.LongMessageServerResponseLatency > 2000 {
if qualityInfo.LongMessageServerResponseLatency == 9999 {
log.Errorf("错误: 长消息服务器响应延迟测试失败, %v 如果您使用的腾讯云服务器, 请修改DNS到114.114.114.114", longMessageServerErrorMessage)
} else {
log.Warnf("警告: 长消息响应延迟为 %vms, 大于 1000ms, %v", qualityInfo.LongMessageServerResponseLatency, longMessageServerErrorMessage)
}
}
if qualityInfo.SrvServerLatency > 1000 {
if qualityInfo.SrvServerPacketLoss == 9999 {
log.Errorf("错误: 媒体服务器延迟测试失败, %v", mediaServerErrorMessage)
} else {
log.Warnf("警告: 媒体服务器延迟为 %vms大于 1000ms, %v", qualityInfo.SrvServerLatency, mediaServerErrorMessage)
}
}
if qualityInfo.SrvServerPacketLoss > 0 {
log.Warnf("警告: 本地连接媒体服务器丢包率为 %v%%, %v", qualityInfo.SrvServerPacketLoss*10, mediaServerErrorMessage)
}
if qualityInfo.ChatServerLatency > 1000 || qualityInfo.ChatServerPacketLoss > 0 || qualityInfo.LongMessageServerLatency > 1000 || qualityInfo.SrvServerLatency > 1000 || qualityInfo.SrvServerPacketLoss > 0 {
log.Infof("网络诊断完成. 发现问题, 请检查日志.")
} else {
log.Infof("网络诊断完成. 未发现问题")
}
}
// DNSDiagnosis 诊断DNS状态并输出结果
func DNSDiagnosis() {
// todo
}
// EnvironmentDiagnosis 诊断本地环境状态并输出结果
func EnvironmentDiagnosis() {
// todo
}

View File

@ -1,226 +0,0 @@
// Package selfupdate 版本升级检查和自更新
package selfupdate
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"hash"
"io"
"math"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/download"
)
func readLine() (str string) {
console := bufio.NewReader(os.Stdin)
str, _ = console.ReadString('\n')
str = strings.TrimSpace(str)
return
}
func lastVersion() (string, error) {
r, err := download.Request{URL: "https://api.github.com/repos/Mrs4s/go-cqhttp/releases/latest"}.JSON()
if err != nil {
return "", err
}
return r.Get("tag_name").Str, nil
}
// CheckUpdate 检查更新
func CheckUpdate() {
logrus.Infof("正在检查更新.")
if base.Version == "(devel)" {
logrus.Warnf("检查更新失败: 使用的 Actions 测试版或自编译版本.")
return
}
latest, err := lastVersion()
if err != nil {
logrus.Warnf("检查更新失败: %v", err)
return
}
if global.VersionNameCompare(base.Version, latest) {
logrus.Infof("当前有更新的 go-cqhttp 可供更新, 请前往 https://github.com/Mrs4s/go-cqhttp/releases 下载.")
logrus.Infof("当前版本: %v 最新版本: %v", base.Version, latest)
return
}
logrus.Infof("检查更新完成. 当前已运行最新版本.")
}
func binaryName() string {
goarch := runtime.GOARCH
if goarch == "arm" {
goarch += "v7"
}
ext := "tar.gz"
if runtime.GOOS == "windows" {
ext = "zip"
}
return fmt.Sprintf("go-cqhttp_%v_%v.%v", runtime.GOOS, goarch, ext)
}
func checksum(github, version string) []byte {
sumURL := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp_checksums.txt", github, version)
sum, err := download.Request{URL: sumURL}.Bytes()
if err != nil {
return nil
}
rd := bufio.NewReader(bytes.NewReader(sum))
for {
str, err := rd.ReadString('\n')
if err != nil {
break
}
str = strings.TrimSpace(str)
if strings.HasSuffix(str, binaryName()) {
sum, _ := hex.DecodeString(strings.TrimSuffix(str, " "+binaryName()))
return sum
}
}
return nil
}
func wait() {
logrus.Info("按 Enter 继续....")
readLine()
os.Exit(0)
}
// SelfUpdate 自更新
func SelfUpdate(github string) {
if github == "" {
github = "https://github.com"
}
logrus.Infof("正在检查更新.")
latest, err := lastVersion()
if err != nil {
logrus.Warnf("获取最新版本失败: %v", err)
wait()
}
url := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/%v", github, latest, binaryName())
if base.Version == latest {
logrus.Info("当前版本已经是最新版本!")
wait()
}
logrus.Info("当前最新版本为 ", latest)
logrus.Warn("是否更新(y/N): ")
r := strings.TrimSpace(readLine())
if r != "y" && r != "Y" {
logrus.Warn("已取消更新!")
wait()
}
logrus.Info("正在更新,请稍等...")
sum := checksum(github, latest)
if sum != nil {
err = update(url, sum)
if err != nil {
logrus.Error("更新失败: ", err)
} else {
logrus.Info("更新成功!")
}
} else {
logrus.Error("checksum 失败!")
}
wait()
}
// writeSumCounter 写入量计算实例
type writeSumCounter struct {
total uint64
hash hash.Hash
}
// Write 方法将写入的byte长度追加至写入的总长度Total中
func (wc *writeSumCounter) Write(p []byte) (int, error) {
n := len(p)
wc.total += uint64(n)
wc.hash.Write(p)
fmt.Printf("\r ")
fmt.Printf("\rDownloading... %s complete", humanBytes(wc.total))
return n, nil
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanBytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB"} // GB对于go-cqhttp来说已经够用了
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), 1000))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(1000, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// FromStream copy form getlantern/go-update
func fromStream(updateWith io.Reader) (err error, errRecover error) {
updatePath, err := os.Executable()
updatePath = filepath.Clean(updatePath)
if err != nil {
return
}
// get the directory the executable exists in
updateDir := filepath.Dir(updatePath)
filename := filepath.Base(updatePath)
// Copy the contents of of newbinary to a the new executable file
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil {
return
}
// We won't log this error, because it's always going to happen.
defer func() { _ = fp.Close() }()
if _, err = bufio.NewReader(updateWith).WriteTo(fp); err != nil {
logrus.Errorf("Unable to copy data: %v\n", err)
}
// if we don't call fp.Close(), windows won't let us move the new executable
// because the file will still be "in use"
if err := fp.Close(); err != nil {
logrus.Errorf("Unable to close file: %v\n", err)
}
// this is where we'll move the executable to so that we can swap in the updated replacement
oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
// delete any existing old exec file - this is necessary on Windows for two reasons:
// 1. after a successful update, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldPath)
// move the existing executable to a new file in the same directory
err = os.Rename(updatePath, oldPath)
if err != nil {
return
}
// move the new executable in to become the new program
err = os.Rename(newPath, updatePath)
if err != nil {
// copy unsuccessful
errRecover = os.Rename(oldPath, updatePath)
} else {
// copy successful, remove the old binary
_ = os.Remove(oldPath)
}
return
}

View File

@ -1,50 +0,0 @@
//go:build !windows
package selfupdate
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"io"
"net/http"
)
// update go-cqhttp自我更新
func update(url string, sum []byte) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
wc := writeSumCounter{
hash: sha256.New(),
}
rsp, err := io.ReadAll(io.TeeReader(resp.Body, &wc))
if err != nil {
return err
}
if !bytes.Equal(wc.hash.Sum(nil), sum) {
return errors.New("文件已损坏")
}
gr, err := gzip.NewReader(bytes.NewReader(rsp))
if err != nil {
return err
}
tr := tar.NewReader(gr)
for {
header, err := tr.Next()
if err != nil {
return err
}
if header.Name == "go-cqhttp" {
err, _ := fromStream(tr)
if err != nil {
return err
}
return nil
}
}
}

View File

@ -1,39 +0,0 @@
package selfupdate
import (
"archive/zip"
"bytes"
"crypto/sha256"
"errors"
"io"
"net/http"
)
// update go-cqhttp自我更新
func update(url string, sum []byte) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
wc := writeSumCounter{
hash: sha256.New(),
}
rsp, err := io.ReadAll(io.TeeReader(resp.Body, &wc))
if err != nil {
return err
}
if !bytes.Equal(wc.hash.Sum(nil), sum) {
return errors.New("文件已损坏")
}
reader, _ := zip.NewReader(bytes.NewReader(rsp), resp.ContentLength)
file, err := reader.Open("go-cqhttp.exe")
if err != nil {
return err
}
err, _ = fromStream(file)
if err != nil {
return err
}
return nil
}

164
login.go Normal file
View File

@ -0,0 +1,164 @@
package main
import (
"bufio"
"bytes"
"io/ioutil"
"os"
"strings"
"time"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Mrs4s/MiraiGo/client"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/tuotoo/qrcode"
"github.com/Mrs4s/go-cqhttp/global"
)
var console = bufio.NewReader(os.Stdin)
var readLine = func() (str string) {
str, _ = console.ReadString('\n')
str = strings.TrimSpace(str)
return
}
var cli *client.QQClient
// ErrSMSRequestError SMS请求出错
var ErrSMSRequestError = errors.New("sms request error")
func commonLogin() error {
res, err := cli.Login()
if err != nil {
return err
}
return loginResponseProcessor(res)
}
func qrcodeLogin() error {
rsp, err := cli.FetchQRCode()
if err != nil {
return err
}
fi, err := qrcode.Decode(bytes.NewReader(rsp.ImageData))
if err != nil {
return err
}
_ = ioutil.WriteFile("qrcode.png", rsp.ImageData, 0644)
log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
time.Sleep(time.Second)
qrcodeTerminal.New().Get(fi.Content).Print()
s, err := cli.QueryQRCodeStatus(rsp.Sig)
if err != nil {
return err
}
prevState := s.State
for {
time.Sleep(time.Second)
s, _ = cli.QueryQRCodeStatus(rsp.Sig)
if s == nil {
continue
}
if prevState == s.State {
continue
}
prevState = s.State
if s.State == client.QRCodeCanceled {
log.Fatalf("扫码被用户取消.")
}
if s.State == client.QRCodeTimeout {
log.Fatalf("二维码过期")
}
if s.State == client.QRCodeWaitingForConfirm {
log.Infof("扫码成功, 请在手机端确认登录.")
}
if s.State == client.QRCodeConfirmed {
res, err := cli.QRCodeLogin(s.LoginInfo)
if err != nil {
return nil
}
return loginResponseProcessor(res)
}
}
}
func loginResponseProcessor(res *client.LoginResponse) error {
var err error
for {
if err != nil {
return err
}
if res.Success {
return nil
}
var text string
switch res.Error {
case client.SliderNeededError:
log.Warnf("登录需要滑条验证码. ")
log.Warnf("请参考文档 -> https://github.com/Mrs4s/go-cqhttp/blob/master/docs/slider.md <- 抓包获取 Ticket")
println()
log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", res.VerifyUrl)
println()
log.Warn("请输入Ticket (Enter 提交)")
text = readLine()
res, err = cli.SubmitTicket(text)
continue
case client.NeedCaptcha:
log.Warnf("登录需要滑条验证码.")
_ = ioutil.WriteFile("captcha.jpg", res.CaptchaImage, 0644)
log.Warnf("请输入验证码 (captcha.jpg) (Enter 提交)")
text = readLine()
global.DelFile("captcha.jpg")
res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
continue
case client.SMSNeededError:
log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
readLine()
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
case client.SMSOrVerifyNeededError:
log.Warnf("账号已开启设备锁,请选择验证方式:")
log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
log.Warnf("2. 使用手机QQ扫码验证.")
log.Warn("请输入(1 - 2)")
text = readLine()
if strings.Contains(text, "1") {
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
}
fallthrough
case client.UnsafeDeviceError:
log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyUrl)
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
msg := res.ErrorMessage
if strings.Contains(msg, "版本") {
msg = "密码错误或账号被冻结"
}
if strings.Contains(msg, "冻结") {
log.Fatalf("账号被冻结")
}
log.Warnf("登录失败: %v", msg)
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
}
}
}

Some files were not shown because too many files have changed in this diff Show More