├── .dockerignore
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yaml
│ ├── config.yml
│ └── feat--.md
└── workflows
│ ├── build_docker_image.yml
│ ├── ci.yml
│ ├── close_pr.yml
│ ├── golint.yml
│ └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd
├── api-generator
│ ├── main.go
│ └── supported.go
└── gocq
│ ├── login.go
│ ├── main.go
│ └── qsign.go
├── coolq
├── api.go
├── api_v12.go
├── bot.go
├── converter.go
├── cqcode.go
├── doc.go
├── event.go
└── feed.go
├── db
├── database.go
├── leveldb
│ ├── const.go
│ ├── leveldb.go
│ ├── reader.go
│ ├── structs.go
│ └── writer.go
├── mongodb
│ └── mongodb.go
├── multidb.go
└── sqlite3
│ ├── model.go
│ └── sqlite3.go
├── docker-entrypoint.sh
├── docs
├── EventFilter.md
├── QA.md
├── README.md
├── adminApi.md
├── config.md
├── cqhttp.md
├── file.md
├── guild.md
├── quick_start.md
└── slider.md
├── global
├── all_test.go
├── buffer.go
├── codec.go
├── doc.go
├── fs.go
├── log_hook.go
├── net.go
├── param.go
├── signal.go
├── signal_unix.go
├── signal_windows.go
└── terminal
│ ├── doc.go
│ ├── double_click.go
│ ├── double_click_windows.go
│ ├── quick_edit.go
│ ├── quick_edit_windows.go
│ ├── title.go
│ ├── title_windows.go
│ ├── vt100.go
│ └── vt100_windows.go
├── go.mod
├── go.sum
├── internal
├── base
│ ├── feature.go
│ ├── flag.go
│ └── version.go
├── cache
│ └── cache.go
├── download
│ └── download.go
├── mime
│ └── mime.go
├── msg
│ ├── element.go
│ ├── element_test.go
│ ├── local.go
│ ├── parse.go
│ └── parse_test.go
├── param
│ └── param.go
├── selfdiagnosis
│ └── diagnoses.go
└── selfupdate
│ ├── update.go
│ ├── update_others.go
│ └── update_windows.go
├── main.go
├── modules
├── api
│ ├── api.go
│ └── caller.go
├── config
│ ├── config.go
│ ├── config_test.go
│ └── default_config.yml
├── filter
│ ├── filter.go
│ └── middlewares.go
├── pprof
│ └── pprof.go
├── servers
│ └── servers.go
└── silk
│ ├── codec.go
│ ├── codec_unsupported.go
│ └── stubs.go
├── pkg
└── onebot
│ ├── attr.go
│ ├── kind_string.go
│ ├── onebot.go
│ ├── spec.go
│ ├── supported.go
│ └── value.go
├── scripts
├── bootstrap
└── upload_dist.sh
├── server
├── daemon.go
├── doc.go
├── http.go
├── http_test.go
├── middlewares.go
├── scf.go
└── websocket.go
└── winres
├── .gitignore
├── gen
└── json.go
├── icon.png
├── icon16.png
└── init.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | .gitlab-ci.yml
2 | .dockerignore
3 | Dockerfile
4 | README.md
5 | LICENSE
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yaml:
--------------------------------------------------------------------------------
1 | name: 回报错误
2 | description: 在使用 go-cqhttp 的过程中遇到了错误
3 | title: '[Bug]: '
4 | labels: [ "bug?" ]
5 |
6 | body:
7 | # User's README and agreement
8 | - type: markdown
9 | attributes:
10 | value: |
11 | ## 感谢您愿意填写错误回报!
12 | ## 以下是一些注意事项,请务必阅读让我们能够更容易处理
13 |
14 | ### ❗ | 确定没有相同问题的ISSUE已被提出. (教程: https://forums.go-cqhttp.org/t/topic/141)
15 | ### 🌎| 请准确填写环境信息
16 | ### ❔ | 打开DEBUG模式复现,并提供出现问题前后至少 10 秒的完整日志内容。请自行删除日志内存在的个人信息及敏感内容。
17 | ### ⚠ | 如果涉及内存泄漏/CPU占用异常请打开DEBUG模式并下载pprof性能分析.
18 |
19 | ## 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》
20 | 链接: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
21 | ---
22 | - type: checkboxes
23 | id: terms
24 | attributes:
25 | label: 请确保您已阅读以上注意事项,并勾选下方的确认框。
26 | options:
27 | - label: "我已经仔细阅读上述教程和 [\"提问前需知\"](https://forums.go-cqhttp.org/t/topic/141)"
28 | required: true
29 | - label: "我已经使用 [dev分支版本](https://github.com/ProtocolScience/AstralGocq/actions/workflows/ci.yml) 测试过,问题依旧存在。"
30 | required: true
31 | - label: "我已经在 [Issue Tracker](https://github.com/ProtocolScience/AstralGocq/issues) 中找过我要提出的问题,没有找到相同问题的ISSUE。"
32 | required: true
33 | - label: 我已知晓并同意,此处仅用于汇报程序中存在的问题。若这个 Issue 是关于其他非程序本身问题,则我的 Issue 可能会被无条件自动关闭或/并锁定。(这些问题应当在 Discussion 板块提出。)
34 | required: true
35 |
36 | # User's data
37 | - type: markdown
38 | attributes:
39 | value: |
40 | ## 环境信息
41 | 请根据实际使用环境修改以下信息。
42 |
43 | # Env | go-cqhttp Version
44 | - type: input
45 | id: env-gocq-ver
46 | attributes:
47 | label: go-cqhttp 版本
48 | validations:
49 | required: true
50 |
51 | # Env | VM Version
52 | - type: dropdown
53 | id: env-vm-ver
54 | attributes:
55 | label: 运行环境
56 | description: 选择运行 go-cqhttp 的系统版本
57 | options:
58 | - Windows (64)
59 | - Windows (32/x84)
60 | - MacOS
61 | - Linux
62 | - Ubuntu
63 | - CentOS
64 | - ArchLinux
65 | - UNIX (Android)
66 | - 其它(请在下方说明)
67 | validations:
68 | required: true
69 |
70 | # Env | VM Arch
71 | - type: dropdown
72 | id: env-vm-arch
73 | attributes:
74 | label: 运行架构
75 | description: (可选) 选择运行 go-cqhttp 的系统架构
76 | options:
77 | - AMD64
78 | - x86
79 | - ARM [32] (别名:AArch32 / ARMv7)
80 | - ARM [64] (别名:AArch64 / ARMv8)
81 | - 其它
82 |
83 | # Env | Connection type
84 | - type: dropdown
85 | id: env-conn-type
86 | attributes:
87 | label: 连接方式
88 | description: 选择对接机器人的连接方式
89 | options:
90 | - HTTP
91 | - WebSocket (正向)
92 | - WebSocket (反向)
93 | - LambdaServer
94 | validations:
95 | required: true
96 |
97 | # Env | Protocol
98 | - type: dropdown
99 | id: env-protocol
100 | attributes:
101 | label: 使用协议
102 | description: 选择使用的协议
103 | options:
104 | - 0 | Default
105 | - 1 | Android Phone
106 | - 2 | Android Watch
107 | - 3 | MacOS
108 | - 4 | 企点
109 | - 5 | iPad
110 | - 6 | aPad
111 | validations:
112 | required: true
113 |
114 | # Input | Reproduce
115 | - type: textarea
116 | id: reproduce-steps
117 | attributes:
118 | label: 重现步骤
119 | description: |
120 | 我们需要执行哪些操作才能让 bug 出现?
121 | 简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
122 | validations:
123 | required: true
124 |
125 | # Input | Expected result
126 | - type: textarea
127 | id: expected
128 | attributes:
129 | label: 期望的结果是什么?
130 | validations:
131 | required: true
132 |
133 | # Input | Actual result
134 | - type: textarea
135 | id: actual
136 | attributes:
137 | label: 实际的结果是什么?
138 | validations:
139 | required: true
140 |
141 | # Optional | Reproduce code
142 | - type: textarea
143 | id: reproduce-code
144 | attributes:
145 | label: 简单的复现代码/链接(可选)
146 | render: golang
147 |
148 | # Optional | Logging
149 | - type: textarea
150 | id: logging
151 | attributes:
152 | label: 日志记录(可选)
153 | render: golang
154 |
155 | # Optional | Extra description
156 | - type: textarea
157 | id: extra-desc
158 | attributes:
159 | label: 补充说明(可选)
160 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 不知道怎么做?
4 | url: https://github.com/ProtocolScience/AstralGocq/issues/633
5 | about: 建议你先查看此教程
6 | - name: 讨论区
7 | url: https://github.com/ProtocolScience/AstralGocq/discussions/
8 | about: 使用中若遇到问题或有新点子新需求,请先在这里求助和征求意见。
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feat--.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 新功能提议
3 | about: 提出新功能
4 | title: ''
5 | labels: feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | **环境信息**
11 |
12 | go-cqhttp版本:
13 |
14 | **需要添加的功能内容**
15 |
16 |
--------------------------------------------------------------------------------
/.github/workflows/build_docker_image.yml:
--------------------------------------------------------------------------------
1 | name: Build And Push Docker Image
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | - 'dev'
8 | # Sequence of patterns matched against refs/tags
9 | tags:
10 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
11 |
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 |
17 | runs-on: ubuntu-latest
18 |
19 | permissions:
20 | packages: write
21 | contents: read
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 |
26 | - name: Set time zone
27 | uses: szenius/set-timezone@v1.1
28 | with:
29 | timezoneLinux: "Asia/Shanghai"
30 | timezoneMacos: "Asia/Shanghai"
31 | timezoneWindows: "China Standard Time"
32 |
33 | # # 如果有 dockerhub 账户,可以在github的secrets中配置下面两个,然后取消下面注释的这几行,并在meta步骤的images增加一行 ${{ github.repository }}
34 | # - name: Login to DockerHub
35 | # uses: docker/login-action@v1
36 | # with:
37 | # username: ${{ secrets.DOCKERHUB_USERNAME }}
38 | # password: ${{ secrets.DOCKERHUB_TOKEN }}
39 |
40 | - name: Login to GHCR
41 | uses: docker/login-action@v2
42 | with:
43 | registry: ghcr.io
44 | username: ${{ github.repository_owner }}
45 | password: ${{ secrets.GITHUB_TOKEN }}
46 |
47 | - name: Extract metadata (tags, labels) for Docker
48 | id: meta
49 | uses: docker/metadata-action@v4
50 | with:
51 | images: |
52 | ghcr.io/${{ github.repository }}
53 | # generate Docker tags based on the following events/attributes
54 | # nightly, master, pr-2, 1.2.3, 1.2, 1
55 | tags: |
56 | type=schedule,pattern=nightly
57 | type=edge
58 | type=ref,event=branch
59 | type=ref,event=pr
60 | type=semver,pattern={{version}}
61 | type=semver,pattern={{major}}.{{minor}}
62 | type=semver,pattern={{major}}
63 |
64 | - name: Set up QEMU
65 | uses: docker/setup-qemu-action@v2
66 |
67 | - name: Set up Docker Buildx
68 | uses: docker/setup-buildx-action@v2
69 |
70 | - name: Build and push
71 | id: docker_build
72 | uses: docker/build-push-action@v4
73 | with:
74 | context: .
75 | push: ${{ github.event_name != 'pull_request' }}
76 | tags: ${{ steps.meta.outputs.tags }}
77 | labels: ${{ steps.meta.outputs.labels }}
78 | cache-from: type=gha
79 | cache-to: type=gha,mode=max
80 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
81 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request,workflow_dispatch]
4 |
5 | env:
6 | BINARY_PREFIX: "go-cqhttp_"
7 | BINARY_SUFFIX: ""
8 | COMMIT_ID: "${{ github.sha }}"
9 | PR_PROMPT: "::warning:: Build artifact will not be uploaded due to the workflow is trigged by pull request."
10 |
11 | jobs:
12 | build:
13 | name: Build binary CI
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64, darwin/arm64
18 | goos: [linux, windows, darwin]
19 | goarch: ["386", amd64, arm, arm64]
20 | exclude:
21 | - goos: darwin
22 | goarch: arm
23 | - goos: darwin
24 | goarch: "386"
25 | fail-fast: true
26 | steps:
27 | - uses: actions/checkout@v3
28 | - name: Setup Go environment
29 | uses: actions/setup-go@v3
30 | with:
31 | cache: true
32 | go-version: '1.20'
33 | - name: Build binary file
34 | env:
35 | GOOS: ${{ matrix.goos }}
36 | GOARCH: ${{ matrix.goarch }}
37 | IS_PR: ${{ !!github.head_ref }}
38 | run: |
39 | if [ $GOOS = "windows" ]; then export BINARY_SUFFIX="$BINARY_SUFFIX.exe"; fi
40 | if $IS_PR ; then echo $PR_PROMPT; fi
41 | export BINARY_NAME="$BINARY_PREFIX"$GOOS"_$GOARCH$BINARY_SUFFIX"
42 | export CGO_ENABLED=0
43 | export LD_FLAGS="-w -s -X github.com/ProtocolScience/AstralGocq/internal/base.Version=${COMMIT_ID::7}"
44 | go build -o "output/$BINARY_NAME" -trimpath -ldflags "$LD_FLAGS" .
45 | - name: Upload artifact
46 | uses: actions/upload-artifact@v4
47 | if: ${{ !github.head_ref }}
48 | with:
49 | name: ${{ matrix.goos }}_${{ matrix.goarch }}
50 | path: output/
51 |
--------------------------------------------------------------------------------
/.github/workflows/close_pr.yml:
--------------------------------------------------------------------------------
1 | name: Check and Close Invalid PR
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, reopened]
6 |
7 | jobs:
8 | # This workflow closes invalid PR
9 | close_pr:
10 | # The type of runner that the job will run on
11 | runs-on: ubuntu-latest
12 | permissions: write-all
13 |
14 | # Steps represent a sequence of tasks that will be executed as part of the job
15 | steps:
16 | - name: Close PR if it is not pointed to dev branch
17 | if: github.event.pull_request.base.ref != 'dev'
18 | uses: superbrothers/close-pull-request@v3
19 | with:
20 | # Optional. Post a issue comment just before closing a pull request.
21 | comment: "Invalid PR to `non-dev` branch `${{ github.event.pull_request.base.ref }}`."
22 |
--------------------------------------------------------------------------------
/.github/workflows/golint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [push,pull_request,workflow_dispatch]
4 |
5 | jobs:
6 | golangci:
7 | name: lint
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 |
12 | - name: Setup Go environment
13 | uses: actions/setup-go@v3
14 | with:
15 | go-version: '1.20'
16 |
17 | - name: golangci-lint
18 | uses: golangci/golangci-lint-action@v3
19 | with:
20 | version: latest
21 |
22 | - name: Tests
23 | run: |
24 | go test $(go list ./...)
25 |
26 | - name: Commit back
27 | if: ${{ github.repository_owner == 'Mrs4s' && !github.event.pull_request }}
28 | continue-on-error: true
29 | run: |
30 | git config --local user.name 'github-actions[bot]'
31 | git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com'
32 | git add --all
33 | git commit -m "ci(chore): Fix stylings"
34 | git push
35 |
36 | - name: Suggester
37 | if: ${{ github.event.pull_request }}
38 | uses: reviewdog/action-suggester@v1
39 | with:
40 | github_token: ${{ secrets.GITHUB_TOKEN }}
41 | tool_name: golangci-lint
42 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | goreleaser:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | run: |
14 | git version
15 | git clone "${{ github.event.repository.html_url }}" /home/runner/work/AstralGocq/AstralGocq
16 | git checkout "${{ github.ref }}"
17 |
18 | - name: Set up Go
19 | uses: actions/setup-go@v3
20 | with:
21 | go-version: '1.20'
22 |
23 | - name: Run GoReleaser
24 | uses: goreleaser/goreleaser-action@v4
25 | with:
26 | version: latest
27 | args: release --clean
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.RELEASER_KEY }}
30 |
31 | #- name: Checkout Dist
32 | # uses: actions/checkout@v2
33 | # with:
34 | # repository: 'gocq/dist'
35 | # ref: master
36 | # ssh-key: ${{ secrets.SSH_KEY }}
37 | # path: upstream/dist
38 |
39 | #- name: Update Dist
40 | # run: |
41 | # chmod +x scripts/upload_dist.sh
42 | # ./scripts/upload_dist.sh
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | .idea
3 | .vscode
4 | config.hjson
5 | config.yml
6 | session.token
7 | device.json
8 | data/
9 | logs/
10 | internal/btree/*.lock
11 | internal/btree/*.db
12 |
13 | # binary builds
14 | go-cqhttp
15 | *.exe
16 |
17 | # macos
18 | .DS_Store
19 |
20 | # windwos rc
21 | *.syso
22 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters-settings:
2 | errcheck:
3 | ignore: fmt:.*,io/ioutil:^Read.*
4 | ignoretests: true
5 |
6 | goimports:
7 | local-prefixes: github.com/ProtocolScience/AstralGocq
8 |
9 | gocritic:
10 | disabled-checks:
11 | - exitAfterDefer
12 |
13 | forbidigo:
14 | # Forbid the following identifiers
15 | forbid:
16 | - ^fmt\.Errorf$ # consider errors.Errorf in github.com/pkg/errors
17 |
18 | linters:
19 | # please, do not use `enable-all`: it's deprecated and will be removed soon.
20 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
21 | disable-all: true
22 | fast: false
23 | enable:
24 | - bodyclose
25 | - durationcheck
26 | - gofmt
27 | - goimports
28 | - errcheck
29 | - copyloopvar
30 | - exhaustive
31 | - bidichk
32 | - gocritic
33 | - gosimple
34 | - govet
35 | - ineffassign
36 | #- nolintlint
37 | - staticcheck
38 | - stylecheck
39 | - unconvert
40 | - usestdlibvars
41 | - unparam
42 | - unused
43 | - whitespace
44 | - prealloc
45 | - predeclared
46 | - asciicheck
47 | - revive
48 | - forbidigo
49 | - makezero
50 |
51 | run:
52 | # default concurrency is a available CPU number.
53 | # concurrency: 4 # explicitly omit this value to fully utilize available resources.
54 | deadline: 5m
55 | issues-exit-code: 1
56 | skip-dirs:
57 | - db
58 | - cmd/api-generator
59 | - internal/encryption
60 | tests: true
61 |
62 | # output configuration options
63 | output:
64 | format: "colored-line-number"
65 | print-issued-lines: true
66 | print-linter-name: true
67 | uniq-by-line: true
68 |
69 | issues:
70 | # Fix found issues (if it's supported by the linter)
71 | fix: true
72 | exclude-use-default: false
73 | exclude:
74 | - "Error return value of .((os.)?std(out|err)..*|.*Close|.*Seek|.*Flush|os.Remove(All)?|.*print(f|ln)?|os.(Un)?Setenv). is not check"
75 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | env:
2 | - GO111MODULE=on
3 | before:
4 | hooks:
5 | - go mod tidy
6 | - go install github.com/tc-hib/go-winres@latest
7 | - go generate winres/init.go
8 | - go-winres make
9 | release:
10 | draft: true
11 | discussion_category_name: General
12 | builds:
13 | - id: nowin
14 | env:
15 | - CGO_ENABLED=0
16 | - GO111MODULE=on
17 | goos:
18 | - linux
19 | - darwin
20 | goarch:
21 | - '386'
22 | - amd64
23 | - arm
24 | - arm64
25 | goarm:
26 | - '7'
27 | ignore:
28 | - goos: darwin
29 | goarch: arm
30 | - goos: darwin
31 | goarch: '386'
32 | mod_timestamp: "{{ .CommitTimestamp }}"
33 | flags:
34 | - -trimpath
35 | ldflags:
36 | - -s -w -X github.com/ProtocolScience/AstralGocq/internal/base.Version=v{{.Version}}
37 | - id: win
38 | env:
39 | - CGO_ENABLED=0
40 | - GO111MODULE=on
41 | goos:
42 | - windows
43 | goarch:
44 | - '386'
45 | - amd64
46 | - arm
47 | - arm64
48 | goarm:
49 | - '7'
50 | mod_timestamp: "{{ .CommitTimestamp }}"
51 | flags:
52 | - -trimpath
53 | ldflags:
54 | - -s -w -X github.com/ProtocolScience/AstralGocq/internal/base.Version=v{{.Version}}
55 |
56 | checksum:
57 | name_template: "{{ .ProjectName }}_checksums.txt"
58 | changelog:
59 | sort: asc
60 | filters:
61 | exclude:
62 | - "^docs:"
63 | - "^test:"
64 | - fix typo
65 | - Merge pull request
66 | - Merge branch
67 | - Merge remote-tracking
68 | - go mod tidy
69 |
70 | archives:
71 | - id: binary
72 | builds:
73 | - win
74 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
75 | format_overrides:
76 | - goos: windows
77 | format: binary
78 | - id: nowin
79 | builds:
80 | - nowin
81 | - win
82 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
83 | format_overrides:
84 | - goos: windows
85 | format: zip
86 |
87 | nfpms:
88 | - license: AGPL 3.0
89 | homepage: https://go-cqhttp.org
90 | file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
91 | formats:
92 | - deb
93 | - rpm
94 | maintainer: Mrs4s
95 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to go-cqhttp
2 |
3 | 想要成为 go-cqhttp 的 Contributor? Awesome!
4 |
5 | 这个页面提供了一些 Tips ,可能对您的开发提供一些帮助.
6 |
7 | ## 开发环境准备
8 |
9 | go-cqhttp 使用了 `golangci-lint` 检查可能的问题,规范代码风格,为了减少不必要的麻烦,
10 | 我们推荐在开发环境中安装 `golangci-lint` 工具.
11 |
12 | ```shell
13 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
14 | ```
15 |
16 | 在提交代码前运行 `golangci-lint` 检查你的代码:
17 |
18 | ```shell
19 | golangci-lint run
20 | ```
21 |
22 | **注意**: `golangci-lint` 需要 `diff` 工具,在 windows 环境中,你可能需要使用 `Git Bash` 运行。
23 |
24 | ## Pull requests
25 |
26 | 首先,为了方便项目管理,请将您的 PR 推送至**dev**分支。
27 |
28 | ### 检查 issue 列表
29 |
30 | 不管你是已经明确了要提交什么代码,还是正在寻找一个想法,你都应该先到 issue 列表看一下。
31 | 如果在 issue 中找到了感兴趣的,请在 issue 表明正在对这个 issue 进行开发。
32 |
33 | ### 项目结构
34 |
35 | 下面是 go-cqhttp 项目结构的简单介绍.
36 |
37 |
38 |
39 | coolq |
40 |
41 | 包含与 MiraiGo 交互部分, CQ码解析等部分
42 | |
43 |
44 |
45 | server |
46 |
47 | 包含 http,ws 通信的实现部分
48 | |
49 |
50 |
51 | global |
52 |
53 | 一个实用的工具包
54 | |
55 |
56 |
57 | docs |
58 |
59 | 使用教程与文档
60 | |
61 |
62 |
63 |
64 | ## 社区准则
65 | 为了让社区保持强大,不断发展,我们向整个社区提出了一些通用准则:
66 |
67 | **友善**:对社区成员要礼貌,尊重和礼貌。 请不要在社区中发布任何有关种族歧视、性别歧视、
68 | 地域歧视、人格侮辱等言论。
69 |
70 | **鼓励参与**:在社区中讲礼貌的每个人都受到欢迎,无论他们的贡献程度如何,
71 | 我们鼓励一切人参与(不一定需要提交代码) `go-cqhttp` 的开发。
72 |
73 | **紧贴主题**:请避免主题外的讨论。当您更新或回复时, 可能会给大量人员发送邮件,
74 | 请牢记,没有人喜欢垃圾邮件。
75 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.20-alpine AS builder
2 |
3 | RUN go env -w GO111MODULE=auto \
4 | && go env -w CGO_ENABLED=0
5 | # && go env -w GOPROXY=https://goproxy.cn,direct
6 |
7 | WORKDIR /build
8 |
9 | COPY ./ .
10 |
11 | RUN set -ex \
12 | && cd /build \
13 | && go build -ldflags "-s -w -extldflags '-static'" -o cqhttp
14 |
15 | FROM alpine:latest
16 |
17 | COPY docker-entrypoint.sh /docker-entrypoint.sh
18 |
19 | RUN chmod +x /docker-entrypoint.sh && \
20 | apk add --no-cache --update \
21 | ffmpeg \
22 | coreutils \
23 | shadow \
24 | su-exec \
25 | tzdata && \
26 | rm -rf /var/cache/apk/* && \
27 | mkdir -p /app && \
28 | mkdir -p /data && \
29 | mkdir -p /config && \
30 | useradd -d /config -s /bin/sh abc && \
31 | chown -R abc /config && \
32 | chown -R abc /data
33 |
34 | ENV TZ="Asia/Shanghai"
35 | ENV UID=99
36 | ENV GID=100
37 | ENV UMASK=002
38 |
39 | COPY --from=builder /build/cqhttp /app/
40 |
41 | WORKDIR /data
42 |
43 | VOLUME [ "/data" ]
44 |
45 | ENTRYPOINT [ "/docker-entrypoint.sh" ]
46 | CMD [ "/app/cqhttp" ]
47 |
--------------------------------------------------------------------------------
/cmd/api-generator/supported.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "html/template"
4 |
5 | func (g *generator) genSupported(routers []Router) {
6 | var v11, v12 []string // for onebot v12 get_supported_actions
7 | for _, router := range routers {
8 | if len(router.PathV11) > 0 {
9 | v11 = append(v11, router.PathV11...)
10 | }
11 | if len(router.PathV11) > 0 {
12 | v12 = append(v12, router.PathV12...)
13 | }
14 | if len(router.Path) > 0 {
15 | v11 = append(v11, router.Path...)
16 | v12 = append(v12, router.Path...)
17 | }
18 | }
19 |
20 | type S struct {
21 | V11 []string
22 | V12 []string
23 | }
24 |
25 | tmpl, err := template.New("").Parse(supportedTemplete)
26 | if err != nil {
27 | panic(err)
28 | }
29 | err = tmpl.Execute(g.out, &S{V11: v11, V12: v12})
30 | if err != nil {
31 | panic(err)
32 | }
33 | }
34 |
35 | const supportedTemplete = `
36 | var supportedV11 = []string{
37 | {{range .V11}} "{{.}}",
38 | {{end}}
39 | }
40 |
41 | var supportedV12 = []string{
42 | {{range .V12}} "{{.}}",
43 | {{end}}
44 | }`
45 |
--------------------------------------------------------------------------------
/cmd/gocq/login.go:
--------------------------------------------------------------------------------
1 | package gocq
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "image"
7 | "image/png"
8 | "os"
9 | "strings"
10 | "time"
11 |
12 | "github.com/ProtocolScience/AstralGo/client"
13 | "github.com/mattn/go-colorable"
14 | "github.com/pkg/errors"
15 | log "github.com/sirupsen/logrus"
16 | "gopkg.ilharper.com/x/isatty"
17 |
18 | "github.com/ProtocolScience/AstralGocq/global"
19 | )
20 |
21 | var console = bufio.NewReader(os.Stdin)
22 |
23 | func readLine() (str string) {
24 | str, _ = console.ReadString('\n')
25 | str = strings.TrimSpace(str)
26 | return
27 | }
28 |
29 | /*
30 | func readLineTimeout(t time.Duration) {
31 | r := make(chan string)
32 | go func() {
33 | select {
34 | case r <- readLine():
35 | case <-time.After(t):
36 | }
37 | }()
38 | select {
39 | case <-r:
40 | case <-time.After(t):
41 | }
42 | }*/
43 |
44 | func readIfTTY(de string) (str string) {
45 | if isatty.Isatty(os.Stdin.Fd()) {
46 | return readLine()
47 | }
48 | log.Warnf("未检测到输入终端,自动选择%s.", de)
49 | return de
50 | }
51 |
52 | var cli *client.QQClient
53 | var device *client.DeviceInfo
54 |
55 | // ErrSMSRequestError SMS请求出错
56 | var ErrSMSRequestError = errors.New("sms request error")
57 |
58 | func commonLogin() error {
59 | res, err := cli.Login()
60 | if err != nil {
61 | return err
62 | }
63 | return loginResponseProcessor(res)
64 | }
65 |
66 | func printQRCode(imgData []byte) {
67 | const (
68 | black = "\033[48;5;0m \033[0m"
69 | white = "\033[48;5;7m \033[0m"
70 | )
71 | img, err := png.Decode(bytes.NewReader(imgData))
72 | if err != nil {
73 | log.Panic(err)
74 | }
75 | data := img.(*image.Gray).Pix
76 | bound := img.Bounds().Max.X
77 | buf := make([]byte, 0, (bound*4+1)*(bound))
78 | i := 0
79 | for y := 0; y < bound; y++ {
80 | i = y * bound
81 | for x := 0; x < bound; x++ {
82 | if data[i] != 255 {
83 | buf = append(buf, white...)
84 | } else {
85 | buf = append(buf, black...)
86 | }
87 | i++
88 | }
89 | buf = append(buf, '\n')
90 | }
91 | _, _ = colorable.NewColorableStdout().Write(buf)
92 | }
93 |
94 | func qrcodeLogin() error {
95 | rsp, err := cli.FetchQRCodeCustomSize(1, 2, 1)
96 | if err != nil {
97 | return err
98 | }
99 | _ = os.WriteFile("qrcode.png", rsp.ImageData, 0o644)
100 | defer func() { _ = os.Remove("qrcode.png") }()
101 | if cli.Uin != 0 {
102 | log.Infof("请使用账号 %v 登录手机QQ扫描二维码 (qrcode.png) : ", cli.Uin)
103 | } else {
104 | log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
105 | }
106 | time.Sleep(time.Second)
107 | printQRCode(rsp.ImageData)
108 | s, err := cli.QueryQRCodeStatus(rsp.Sig)
109 | if err != nil {
110 | return err
111 | }
112 | prevState := s.State
113 | for {
114 | time.Sleep(time.Second)
115 | s, _ = cli.QueryQRCodeStatus(rsp.Sig)
116 | if s == nil {
117 | continue
118 | }
119 | if prevState == s.State {
120 | continue
121 | }
122 | prevState = s.State
123 | switch s.State {
124 | case client.QRCodeCanceled:
125 | log.Fatalf("扫码被用户取消.")
126 | case client.QRCodeTimeout:
127 | log.Fatalf("二维码过期")
128 | case client.QRCodeWaitingForConfirm:
129 | log.Infof("扫码成功, 请在手机端确认登录.")
130 | case client.QRCodeConfirmed:
131 | res, err := cli.QRCodeLogin(s.LoginInfo)
132 | if err != nil {
133 | return err
134 | }
135 | return loginResponseProcessor(res)
136 | case client.QRCodeImageFetch, client.QRCodeWaitingForScan:
137 | // ignore
138 | }
139 | }
140 | }
141 |
142 | func loginResponseProcessor(res *client.LoginResponse) error {
143 | var err error
144 | for {
145 | if err != nil {
146 | return err
147 | }
148 | if res.Success {
149 | return nil
150 | }
151 | var text string
152 | switch res.Error {
153 | case client.SliderNeededError:
154 | log.Warnf("登录需要滑条验证码, 请验证后重试.")
155 | ticket := getTicket(res.VerifyUrl)
156 | if ticket == "" {
157 | log.Infof("按 Enter 继续....")
158 | readLine()
159 | os.Exit(0)
160 | }
161 | res, err = cli.SubmitTicket(ticket)
162 | continue
163 | case client.NeedCaptcha:
164 | log.Warnf("登录需要验证码.")
165 | _ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644)
166 | log.Warnf("请输入验证码 (captcha.jpg): (Enter 提交)")
167 | text = readLine()
168 | global.DelFile("captcha.jpg")
169 | res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
170 | continue
171 | case client.SMSNeededError:
172 | log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
173 | readLine()
174 | if !cli.RequestSMS() {
175 | log.Warnf("发送验证码失败,可能是请求过于频繁.")
176 | return errors.WithStack(ErrSMSRequestError)
177 | }
178 | log.Warn("请输入短信验证码: (Enter 提交)")
179 | text = readLine()
180 | res, err = cli.SubmitSMS(text)
181 | continue
182 | case client.SMSOrVerifyNeededError:
183 | log.Warnf("账号已开启设备锁,请选择验证方式:")
184 | log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
185 | log.Warnf("2. 使用手机QQ扫码验证.")
186 | log.Warn("请输入(1 - 2):")
187 | text = readIfTTY("2")
188 | if strings.Contains(text, "1") {
189 | if !cli.RequestSMS() {
190 | log.Warnf("发送验证码失败,可能是请求过于频繁.")
191 | return errors.WithStack(ErrSMSRequestError)
192 | }
193 | log.Warn("请输入短信验证码: (Enter 提交)")
194 | text = readLine()
195 | res, err = cli.SubmitSMS(text)
196 | continue
197 | }
198 | fallthrough
199 | case client.UnsafeDeviceError:
200 | log.Warnf("账号已开启设备锁,请前往 -> %v", res.VerifyUrl)
201 | log.Infof("按 Enter 继续....")
202 | readLine()
203 | res, err = cli.PasswordLogin()
204 | if err != nil {
205 | return err
206 | }
207 | continue
208 | case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
209 | msg := res.ErrorMessage
210 | log.Warnf("登录失败: %v Code: %v", msg, res.Code)
211 | switch res.Code {
212 | case 235:
213 | log.Warnf("设备信息被封禁, 请删除 device.json 后重试.")
214 | case 237:
215 | log.Warnf("登录过于频繁, 请在手机QQ登录并根据提示完成认证后等一段时间重试")
216 | case 45:
217 | log.Warnf("你的账号被限制登录, 请配置 SignServer 后重试")
218 | }
219 | log.Infof("按 Enter 继续....")
220 | readLine()
221 | os.Exit(0)
222 | }
223 | }
224 | }
225 |
226 | func getTicket(u string) string {
227 | log.Warnf("请前往该地址验证 -> %v ", u)
228 | log.Warn("请输入ticket: (Enter 提交)")
229 | return readLine()
230 | }
231 |
--------------------------------------------------------------------------------
/coolq/api_v12.go:
--------------------------------------------------------------------------------
1 | package coolq
2 |
3 | import (
4 | "runtime"
5 |
6 | "github.com/tidwall/gjson"
7 |
8 | "github.com/ProtocolScience/AstralGocq/global"
9 | "github.com/ProtocolScience/AstralGocq/internal/base"
10 | )
11 |
12 | // CQGetVersion 获取版本信息 OneBotV12
13 | //
14 | // https://git.io/JtwUs
15 | // @route12(get_version)
16 | func (bot *CQBot) CQGetVersion() global.MSG {
17 | return OK(global.MSG{
18 | "impl": "go_cqhttp",
19 | "platform": "qq",
20 | "version": base.Version,
21 | "onebot_version": 12,
22 | "runtime_version": runtime.Version(),
23 | "runtime_os": runtime.GOOS,
24 | })
25 | }
26 |
27 | // CQSendMessageV12 发送消息
28 | //
29 | // @route12(send_message)
30 | // @rename(m->message)
31 | func (bot *CQBot) CQSendMessageV12(groupID, userID, detailType string, m gjson.Result) global.MSG { // nolint
32 | // TODO: implement
33 | return OK(nil)
34 | }
35 |
--------------------------------------------------------------------------------
/coolq/converter.go:
--------------------------------------------------------------------------------
1 | package coolq
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 |
7 | "github.com/ProtocolScience/AstralGo/client"
8 | "github.com/ProtocolScience/AstralGo/message"
9 | "github.com/ProtocolScience/AstralGo/topic"
10 | log "github.com/sirupsen/logrus"
11 |
12 | "github.com/ProtocolScience/AstralGocq/global"
13 | )
14 |
15 | func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) global.MSG {
16 | sex := "unknown"
17 | if m.Gender == 1 { // unknown = 0xff
18 | sex = "female"
19 | } else if m.Gender == 0 {
20 | sex = "male"
21 | }
22 | role := "member"
23 | switch m.Permission { // nolint:exhaustive
24 | case client.Owner:
25 | role = "owner"
26 | case client.Administrator:
27 | role = "admin"
28 | }
29 | return global.MSG{
30 | "group_id": groupID,
31 | "user_id": m.Uin,
32 | "nickname": m.Nickname,
33 | "card": m.CardName,
34 | "sex": sex,
35 | "age": 0,
36 | "area": "",
37 | "join_time": m.JoinTime,
38 | "last_sent_time": m.LastSpeakTime,
39 | "shut_up_timestamp": m.ShutUpTimestamp,
40 | "level": strconv.FormatInt(int64(m.Level), 10),
41 | "role": role,
42 | "unfriendly": false,
43 | "title": m.SpecialTitle,
44 | "title_expire_time": 0,
45 | "card_changeable": false,
46 | }
47 | }
48 |
49 | func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) {
50 | for _, mem := range m {
51 | r = append(r, global.MSG{
52 | "tiny_id": fU64(mem.TinyId),
53 | "title": mem.Title,
54 | "nickname": mem.Nickname,
55 | "role_id": fU64(mem.Role),
56 | "role_name": mem.RoleName,
57 | })
58 | }
59 | return
60 | }
61 |
62 | func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) *event {
63 | source := message.Source{
64 | SourceType: message.SourceGroup,
65 | PrimaryID: m.GroupCode,
66 | }
67 | cqm := toStringMessage(m.Elements, source)
68 | typ := "message/group/normal"
69 | if m.Sender.Uin == bot.Client.Uin {
70 | typ = "message_sent/group/normal"
71 | }
72 | gm := global.MSG{
73 | "anonymous": nil,
74 | "font": 0,
75 | "group_id": m.GroupCode,
76 | "message": ToFormattedMessage(m.Elements, source),
77 | "message_seq": m.Id,
78 | "raw_message": cqm,
79 | "sender": global.MSG{
80 | "age": 0,
81 | "area": "",
82 | "level": "",
83 | "sex": "unknown",
84 | "user_id": m.Sender.Uin,
85 | },
86 | "user_id": m.Sender.Uin,
87 | }
88 | if m.Sender.IsAnonymous() {
89 | gm["anonymous"] = global.MSG{
90 | "flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
91 | "id": m.Sender.Uin,
92 | "name": m.Sender.AnonymousInfo.AnonymousNick,
93 | }
94 | gm["sender"].(global.MSG)["nickname"] = "匿名消息"
95 | typ = "message/group/anonymous"
96 | } else {
97 | group := bot.Client.FindGroup(m.GroupCode)
98 | mem := group.FindMember(m.Sender.Uin)
99 | if mem == nil {
100 | log.Warnf("获取 %v 成员信息失败,尝试刷新成员列表", m.Sender.Uin)
101 | t, err := bot.Client.GetGroupMembers(group)
102 | if err != nil {
103 | log.Warnf("刷新群 %v 成员列表失败: %v", group.Uin, err)
104 | return nil
105 | }
106 | group.Members = t
107 | mem = group.FindMember(m.Sender.Uin)
108 | if mem == nil {
109 | return nil
110 | }
111 | }
112 | ms := gm["sender"].(global.MSG)
113 | role := "member"
114 | switch mem.Permission { // nolint:exhaustive
115 | case client.Owner:
116 | role = "owner"
117 | case client.Administrator:
118 | role = "admin"
119 | }
120 | ms["role"] = role
121 | ms["nickname"] = mem.Nickname
122 | ms["card"] = mem.CardName
123 | ms["title"] = mem.SpecialTitle
124 | }
125 | ev := bot.event(typ, gm)
126 | ev.Time = int64(m.Time)
127 | return ev
128 | }
129 |
130 | func convertChannelInfo(c *client.ChannelInfo) global.MSG {
131 | slowModes := make([]global.MSG, 0, len(c.Meta.SlowModes))
132 | for _, mode := range c.Meta.SlowModes {
133 | slowModes = append(slowModes, global.MSG{
134 | "slow_mode_key": mode.SlowModeKey,
135 | "slow_mode_text": mode.SlowModeText,
136 | "speak_frequency": mode.SpeakFrequency,
137 | "slow_mode_circle": mode.SlowModeCircle,
138 | })
139 | }
140 | return global.MSG{
141 | "channel_id": fU64(c.ChannelId),
142 | "channel_type": c.ChannelType,
143 | "channel_name": c.ChannelName,
144 | "owner_guild_id": fU64(c.Meta.GuildId),
145 | "creator_tiny_id": fU64(c.Meta.CreatorTinyId),
146 | "create_time": c.Meta.CreateTime,
147 | "current_slow_mode": c.Meta.CurrentSlowMode,
148 | "talk_permission": c.Meta.TalkPermission,
149 | "visible_type": c.Meta.VisibleType,
150 | "slow_modes": slowModes,
151 | }
152 | }
153 |
154 | func convertChannelFeedInfo(f *topic.Feed) global.MSG {
155 | m := global.MSG{
156 | "id": f.Id,
157 | "title": f.Title,
158 | "sub_title": f.SubTitle,
159 | "create_time": f.CreateTime,
160 | "guild_id": fU64(f.GuildId),
161 | "channel_id": fU64(f.ChannelId),
162 | "poster_info": global.MSG{
163 | "tiny_id": f.Poster.TinyIdStr,
164 | "nickname": f.Poster.Nickname,
165 | "icon_url": f.Poster.IconUrl,
166 | },
167 | "contents": FeedContentsToArrayMessage(f.Contents),
168 | }
169 | images := make([]global.MSG, 0, len(f.Images))
170 | videos := make([]global.MSG, 0, len(f.Videos))
171 | for _, image := range f.Images {
172 | images = append(images, global.MSG{
173 | "file_id": image.FileId,
174 | "pattern_id": image.PatternId,
175 | "url": image.Url,
176 | "width": image.Width,
177 | "height": image.Height,
178 | })
179 | }
180 | for _, video := range f.Videos {
181 | videos = append(videos, global.MSG{
182 | "file_id": video.FileId,
183 | "pattern_id": video.PatternId,
184 | "url": video.Url,
185 | "width": video.Width,
186 | "height": video.Height,
187 | })
188 | }
189 | m["resource"] = global.MSG{
190 | "images": images,
191 | "videos": videos,
192 | }
193 | return m
194 | }
195 |
196 | func convertReactions(reactions []*message.GuildMessageEmojiReaction) (r []global.MSG) {
197 | r = make([]global.MSG, len(reactions))
198 | for i, re := range reactions {
199 | r[i] = global.MSG{
200 | "emoji_id": re.EmojiId,
201 | "emoji_index": re.Face.Index,
202 | "emoji_type": re.EmojiType,
203 | "emoji_name": re.Face.Name,
204 | "count": re.Count,
205 | "clicked": re.Clicked,
206 | }
207 | }
208 | return
209 | }
210 |
211 | func toStringMessage(m []message.IMessageElement, source message.Source) string {
212 | elems := toElements(m, source)
213 | var sb strings.Builder
214 | for _, elem := range elems {
215 | elem.WriteCQCodeTo(&sb)
216 | }
217 | return sb.String()
218 | }
219 |
220 | func fU64(v uint64) string {
221 | return strconv.FormatUint(v, 10)
222 | }
223 |
--------------------------------------------------------------------------------
/coolq/doc.go:
--------------------------------------------------------------------------------
1 | // Package coolq 包含CQBot实例,CQ码处理,消息发送,消息处理等的相关函数与结构体
2 | package coolq
3 |
--------------------------------------------------------------------------------
/coolq/feed.go:
--------------------------------------------------------------------------------
1 | package coolq
2 |
3 | import (
4 | "github.com/ProtocolScience/AstralGo/topic"
5 |
6 | "github.com/ProtocolScience/AstralGocq/global"
7 | )
8 |
9 | // FeedContentsToArrayMessage 将话题频道帖子内容转换为 Array Message
10 | func FeedContentsToArrayMessage(contents []topic.IFeedRichContentElement) []global.MSG {
11 | r := make([]global.MSG, 0, len(contents))
12 | for _, e := range contents {
13 | var m global.MSG
14 | switch elem := e.(type) {
15 | case *topic.TextElement:
16 | m = global.MSG{
17 | "type": "text",
18 | "data": global.MSG{"text": elem.Content},
19 | }
20 | case *topic.AtElement:
21 | m = global.MSG{
22 | "type": "at",
23 | "data": global.MSG{"id": elem.Id, "qq": elem.Id},
24 | }
25 | case *topic.EmojiElement:
26 | m = global.MSG{
27 | "type": "face",
28 | "data": global.MSG{"id": elem.Id},
29 | }
30 | case *topic.ChannelQuoteElement:
31 | m = global.MSG{
32 | "type": "channel_quote",
33 | "data": global.MSG{
34 | "guild_id": fU64(elem.GuildId),
35 | "channel_id": fU64(elem.ChannelId),
36 | "display_text": elem.DisplayText,
37 | },
38 | }
39 | case *topic.UrlQuoteElement:
40 | m = global.MSG{
41 | "type": "url_quote",
42 | "data": global.MSG{
43 | "url": elem.Url,
44 | "display_text": elem.DisplayText,
45 | },
46 | }
47 | }
48 | if m != nil {
49 | r = append(r, m)
50 | }
51 | }
52 | return r
53 | }
54 |
--------------------------------------------------------------------------------
/db/database.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "hash/crc32"
6 |
7 | "github.com/ProtocolScience/AstralGocq/global"
8 | )
9 |
10 | type (
11 | // Database 数据库操作接口定义
12 | Database interface {
13 | // Open 初始化数据库
14 | Open() error
15 |
16 | // GetMessageByGlobalID 通过 GlobalID 来获取消息
17 | GetMessageByGlobalID(int32) (StoredMessage, error)
18 | // GetGroupMessageByGlobalID 通过 GlobalID 来获取群消息
19 | GetGroupMessageByGlobalID(int32) (*StoredGroupMessage, error)
20 | // GetPrivateMessageByGlobalID 通过 GlobalID 来获取私聊消息
21 | GetPrivateMessageByGlobalID(int32) (*StoredPrivateMessage, error)
22 | // GetGuildChannelMessageByID 通过 ID 来获取频道消息
23 | GetGuildChannelMessageByID(string) (*StoredGuildChannelMessage, error)
24 |
25 | // InsertGroupMessage 向数据库写入新的群消息
26 | InsertGroupMessage(*StoredGroupMessage) error
27 | // InsertPrivateMessage 向数据库写入新的私聊消息
28 | InsertPrivateMessage(*StoredPrivateMessage) error
29 | // InsertGuildChannelMessage 向数据库写入新的频道消息
30 | InsertGuildChannelMessage(*StoredGuildChannelMessage) error
31 | }
32 |
33 | StoredMessage interface {
34 | GetID() string
35 | GetType() string
36 | GetGlobalID() int32
37 | GetAttribute() *StoredMessageAttribute
38 | GetContent() []global.MSG
39 | }
40 |
41 | // StoredGroupMessage 持久化群消息
42 | StoredGroupMessage struct {
43 | ID string `bson:"_id" yaml:"-"`
44 | GlobalID int32 `bson:"globalId" yaml:"-"`
45 | Attribute *StoredMessageAttribute `bson:"attribute" yaml:"-"`
46 | SubType string `bson:"subType" yaml:"-"`
47 | QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
48 | GroupCode int64 `bson:"groupCode" yaml:"-"`
49 | AnonymousID string `bson:"anonymousId" yaml:"-"`
50 | Content []global.MSG `bson:"content" yaml:"content"`
51 | }
52 |
53 | // StoredPrivateMessage 持久化私聊消息
54 | StoredPrivateMessage struct {
55 | ID string `bson:"_id" yaml:"-"`
56 | GlobalID int32 `bson:"globalId" yaml:"-"`
57 | Attribute *StoredMessageAttribute `bson:"attribute" yaml:"-"`
58 | SubType string `bson:"subType" yaml:"-"`
59 | QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
60 | SessionUin int64 `bson:"sessionUin" yaml:"-"`
61 | TargetUin int64 `bson:"targetUin" yaml:"-"`
62 | Content []global.MSG `bson:"content" yaml:"content"`
63 | }
64 |
65 | // StoredGuildChannelMessage 持久化频道消息
66 | StoredGuildChannelMessage struct {
67 | ID string `bson:"_id" yaml:"-"`
68 | Attribute *StoredGuildMessageAttribute `bson:"attribute" yaml:"-"`
69 | GuildID uint64 `bson:"guildId" yaml:"-"`
70 | ChannelID uint64 `bson:"channelId" yaml:"-"`
71 | QuotedInfo *QuotedInfo `bson:"quotedInfo" yaml:"-"`
72 | Content []global.MSG `bson:"content" yaml:"content"`
73 | }
74 |
75 | // StoredMessageAttribute 持久化消息属性
76 | StoredMessageAttribute struct {
77 | MessageSeq int32 `bson:"messageSeq" yaml:"-"`
78 | InternalID int32 `bson:"internalId" yaml:"-"`
79 | SenderUin int64 `bson:"senderUin" yaml:"-"`
80 | SenderName string `bson:"senderName" yaml:"-"`
81 | Timestamp int64 `bson:"timestamp" yaml:"-"`
82 | }
83 |
84 | // StoredGuildMessageAttribute 持久化频道消息属性
85 | StoredGuildMessageAttribute struct {
86 | MessageSeq uint64 `bson:"messageSeq" yaml:"-"`
87 | InternalID uint64 `bson:"internalId" yaml:"-"`
88 | SenderTinyID uint64 `bson:"senderTinyId" yaml:"-"`
89 | SenderName string `bson:"senderName" yaml:"-"`
90 | Timestamp int64 `bson:"timestamp" yaml:"-"`
91 | }
92 |
93 | // QuotedInfo 引用回复
94 | QuotedInfo struct {
95 | PrevID string `bson:"prevId" yaml:"-"`
96 | PrevGlobalID int32 `bson:"prevGlobalId" yaml:"-"`
97 | QuotedContent []global.MSG `bson:"quotedContent" yaml:"quoted_content"`
98 | }
99 | )
100 |
101 | // ToGlobalID 构建`code`-`msgID`的字符串并返回其CRC32 Checksum的值
102 | func ToGlobalID(code int64, msgID int32) int32 {
103 | return int32(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d-%d", code, msgID))))
104 | }
105 |
106 | func (m *StoredGroupMessage) GetID() string { return m.ID }
107 | func (m *StoredGroupMessage) GetType() string { return "group" }
108 | func (m *StoredGroupMessage) GetGlobalID() int32 { return m.GlobalID }
109 | func (m *StoredGroupMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute }
110 | func (m *StoredGroupMessage) GetContent() []global.MSG { return m.Content }
111 |
112 | func (m *StoredPrivateMessage) GetID() string { return m.ID }
113 | func (m *StoredPrivateMessage) GetType() string { return "private" }
114 | func (m *StoredPrivateMessage) GetGlobalID() int32 { return m.GlobalID }
115 | func (m *StoredPrivateMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute }
116 | func (m *StoredPrivateMessage) GetContent() []global.MSG { return m.Content }
117 |
--------------------------------------------------------------------------------
/db/leveldb/const.go:
--------------------------------------------------------------------------------
1 | package leveldb
2 |
3 | const dataVersion = 1
4 |
5 | const (
6 | group = 0x0
7 | private = 0x1
8 | guildChannel = 0x2
9 | )
10 |
11 | type coder byte
12 |
13 | const (
14 | coderNil coder = iota
15 | coderInt
16 | coderUint
17 | coderInt32
18 | coderUint32
19 | coderInt64
20 | coderUint64
21 | coderString
22 | coderMSG // global.MSG
23 | coderArrayMSG // []global.MSG
24 | coderStruct // struct{}
25 | )
26 |
--------------------------------------------------------------------------------
/db/leveldb/leveldb.go:
--------------------------------------------------------------------------------
1 | package leveldb
2 |
3 | import (
4 | "path"
5 |
6 | "github.com/ProtocolScience/AstralGo/binary"
7 | "github.com/ProtocolScience/AstralGo/utils"
8 | "github.com/pkg/errors"
9 | "github.com/syndtr/goleveldb/leveldb"
10 | "github.com/syndtr/goleveldb/leveldb/opt"
11 | "gopkg.in/yaml.v3"
12 |
13 | "github.com/ProtocolScience/AstralGocq/db"
14 | )
15 |
16 | type database struct {
17 | db *leveldb.DB
18 | }
19 |
20 | // config leveldb 相关配置
21 | type config struct {
22 | Enable bool `yaml:"enable"`
23 | }
24 |
25 | func init() {
26 | db.Register("leveldb", func(node yaml.Node) db.Database {
27 | conf := new(config)
28 | _ = node.Decode(conf)
29 | if !conf.Enable {
30 | return nil
31 | }
32 | return &database{}
33 | })
34 | }
35 |
36 | func (ldb *database) Open() error {
37 | p := path.Join("data", "leveldb-v3")
38 | d, err := leveldb.OpenFile(p, &opt.Options{
39 | WriteBuffer: 32 * opt.KiB,
40 | })
41 | if err != nil {
42 | return errors.Wrap(err, "open leveldb error")
43 | }
44 | ldb.db = d
45 | return nil
46 | }
47 |
48 | func (ldb *database) GetMessageByGlobalID(id int32) (_ db.StoredMessage, err error) {
49 | v, err := ldb.db.Get(binary.ToBytes(id), nil)
50 | if err != nil || len(v) == 0 {
51 | return nil, errors.Wrap(err, "get value error")
52 | }
53 | defer func() {
54 | if r := recover(); r != nil {
55 | err = errors.Errorf("%v", r)
56 | }
57 | }()
58 | r, err := newReader(utils.B2S(v))
59 | if err != nil {
60 | return nil, err
61 | }
62 | switch r.uvarint() {
63 | case group:
64 | return r.readStoredGroupMessage(), nil
65 | case private:
66 | return r.readStoredPrivateMessage(), nil
67 | default:
68 | return nil, errors.New("unknown message flag")
69 | }
70 | }
71 |
72 | func (ldb *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) {
73 | i, err := ldb.GetMessageByGlobalID(id)
74 | if err != nil {
75 | return nil, err
76 | }
77 | g, ok := i.(*db.StoredGroupMessage)
78 | if !ok {
79 | return nil, errors.New("message type error")
80 | }
81 | return g, nil
82 | }
83 |
84 | func (ldb *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) {
85 | i, err := ldb.GetMessageByGlobalID(id)
86 | if err != nil {
87 | return nil, err
88 | }
89 | p, ok := i.(*db.StoredPrivateMessage)
90 | if !ok {
91 | return nil, errors.New("message type error")
92 | }
93 | return p, nil
94 | }
95 |
96 | func (ldb *database) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) {
97 | v, err := ldb.db.Get([]byte(id), nil)
98 | if err != nil {
99 | return nil, errors.Wrap(err, "get value error")
100 | }
101 | defer func() {
102 | if r := recover(); r != nil {
103 | err = errors.Errorf("%v", r)
104 | }
105 | }()
106 | r, err := newReader(utils.B2S(v))
107 | if err != nil {
108 | return nil, err
109 | }
110 | switch r.uvarint() {
111 | case guildChannel:
112 | return r.readStoredGuildChannelMessage(), nil
113 | default:
114 | return nil, errors.New("unknown message flag")
115 | }
116 | }
117 |
118 | func (ldb *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
119 | w := newWriter()
120 | w.uvarint(group)
121 | w.writeStoredGroupMessage(msg)
122 | err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil)
123 | return errors.Wrap(err, "put data error")
124 | }
125 |
126 | func (ldb *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
127 | w := newWriter()
128 | w.uvarint(private)
129 | w.writeStoredPrivateMessage(msg)
130 | err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil)
131 | return errors.Wrap(err, "put data error")
132 | }
133 |
134 | func (ldb *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
135 | w := newWriter()
136 | w.uvarint(guildChannel)
137 | w.writeStoredGuildChannelMessage(msg)
138 | err := ldb.db.Put(utils.S2B(msg.ID), w.bytes(), nil)
139 | return errors.Wrap(err, "put data error")
140 | }
141 |
--------------------------------------------------------------------------------
/db/leveldb/reader.go:
--------------------------------------------------------------------------------
1 | package leveldb
2 |
3 | import (
4 | "encoding/binary"
5 | "io"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/pkg/errors"
10 |
11 | "github.com/ProtocolScience/AstralGocq/global"
12 | )
13 |
14 | type intReader struct {
15 | data string
16 | *strings.Reader
17 | }
18 |
19 | func newIntReader(s string) intReader {
20 | return intReader{
21 | data: s,
22 | Reader: strings.NewReader(s),
23 | }
24 | }
25 |
26 | func (r *intReader) varint() int64 {
27 | i, _ := binary.ReadVarint(r)
28 | return i
29 | }
30 |
31 | func (r *intReader) uvarint() uint64 {
32 | i, _ := binary.ReadUvarint(r)
33 | return i
34 | }
35 |
36 | // reader implements the index read.
37 | // data format is the same as the writer's
38 | type reader struct {
39 | data intReader
40 | strings intReader
41 | stringIndex map[uint64]string
42 | }
43 |
44 | func (r *reader) coder() coder { o, _ := r.data.ReadByte(); return coder(o) }
45 | func (r *reader) varint() int64 { return r.data.varint() }
46 | func (r *reader) uvarint() uint64 { return r.data.uvarint() }
47 | func (r *reader) int32() int32 { return int32(r.varint()) }
48 | func (r *reader) int64() int64 { return r.varint() }
49 | func (r *reader) uint64() uint64 { return r.uvarint() }
50 |
51 | // func (r *reader) uint32() uint32 { return uint32(r.uvarint()) }
52 | // func (r *reader) int() int { return int(r.varint()) }
53 | // func (r *reader) uint() uint { return uint(r.uvarint()) }
54 |
55 | func (r *reader) string() string {
56 | off := r.data.uvarint()
57 | if s, ok := r.stringIndex[off]; ok {
58 | return s
59 | }
60 | _, _ = r.strings.Seek(int64(off), io.SeekStart)
61 | l := int64(r.strings.uvarint())
62 | whence, _ := r.strings.Seek(0, io.SeekCurrent)
63 | s := r.strings.data[whence : whence+l]
64 | r.stringIndex[off] = s
65 | return s
66 | }
67 |
68 | func (r *reader) msg() global.MSG {
69 | length := r.uvarint()
70 | msg := make(global.MSG, length)
71 | for i := uint64(0); i < length; i++ {
72 | s := r.string()
73 | msg[s] = r.obj()
74 | }
75 | return msg
76 | }
77 |
78 | func (r *reader) arrayMsg() []global.MSG {
79 | length := r.uvarint()
80 | msgs := make([]global.MSG, length)
81 | for i := range msgs {
82 | msgs[i] = r.msg()
83 | }
84 | return msgs
85 | }
86 |
87 | func (r *reader) obj() any {
88 | switch coder := r.coder(); coder {
89 | case coderNil:
90 | return nil
91 | case coderInt:
92 | return int(r.varint())
93 | case coderUint:
94 | return uint(r.uvarint())
95 | case coderInt32:
96 | return int32(r.varint())
97 | case coderUint32:
98 | return uint32(r.uvarint())
99 | case coderInt64:
100 | return r.varint()
101 | case coderUint64:
102 | return r.uvarint()
103 | case coderString:
104 | return r.string()
105 | case coderMSG:
106 | return r.msg()
107 | case coderArrayMSG:
108 | return r.arrayMsg()
109 | default:
110 | panic("db/leveldb: invalid coder " + strconv.Itoa(int(coder)))
111 | }
112 | }
113 |
114 | func newReader(data string) (*reader, error) {
115 | in := newIntReader(data)
116 | v := in.uvarint()
117 | if v != dataVersion {
118 | return nil, errors.Errorf("db/leveldb: invalid data version %d", v)
119 | }
120 | sl := int64(in.uvarint())
121 | dl := int64(in.uvarint())
122 | whence, _ := in.Seek(0, io.SeekCurrent)
123 | sData := data[whence : whence+sl]
124 | dData := data[whence+sl : whence+sl+dl]
125 | r := reader{
126 | data: newIntReader(dData),
127 | strings: newIntReader(sData),
128 | stringIndex: make(map[uint64]string),
129 | }
130 | return &r, nil
131 | }
132 |
--------------------------------------------------------------------------------
/db/leveldb/structs.go:
--------------------------------------------------------------------------------
1 | package leveldb
2 |
3 | import "github.com/ProtocolScience/AstralGocq/db"
4 |
5 | func (w *writer) writeStoredGroupMessage(x *db.StoredGroupMessage) {
6 | if x == nil {
7 | w.nil()
8 | return
9 | }
10 | w.coder(coderStruct)
11 | w.string(x.ID)
12 | w.int32(x.GlobalID)
13 | w.writeStoredMessageAttribute(x.Attribute)
14 | w.string(x.SubType)
15 | w.writeQuotedInfo(x.QuotedInfo)
16 | w.int64(x.GroupCode)
17 | w.string(x.AnonymousID)
18 | w.arrayMsg(x.Content)
19 | }
20 |
21 | func (r *reader) readStoredGroupMessage() *db.StoredGroupMessage {
22 | coder := r.coder()
23 | if coder == coderNil {
24 | return nil
25 | }
26 | x := &db.StoredGroupMessage{}
27 | x.ID = r.string()
28 | x.GlobalID = r.int32()
29 | x.Attribute = r.readStoredMessageAttribute()
30 | x.SubType = r.string()
31 | x.QuotedInfo = r.readQuotedInfo()
32 | x.GroupCode = r.int64()
33 | x.AnonymousID = r.string()
34 | x.Content = r.arrayMsg()
35 | return x
36 | }
37 |
38 | func (w *writer) writeStoredPrivateMessage(x *db.StoredPrivateMessage) {
39 | if x == nil {
40 | w.nil()
41 | return
42 | }
43 | w.coder(coderStruct)
44 | w.string(x.ID)
45 | w.int32(x.GlobalID)
46 | w.writeStoredMessageAttribute(x.Attribute)
47 | w.string(x.SubType)
48 | w.writeQuotedInfo(x.QuotedInfo)
49 | w.int64(x.SessionUin)
50 | w.int64(x.TargetUin)
51 | w.arrayMsg(x.Content)
52 | }
53 |
54 | func (r *reader) readStoredPrivateMessage() *db.StoredPrivateMessage {
55 | coder := r.coder()
56 | if coder == coderNil {
57 | return nil
58 | }
59 | x := &db.StoredPrivateMessage{}
60 | x.ID = r.string()
61 | x.GlobalID = r.int32()
62 | x.Attribute = r.readStoredMessageAttribute()
63 | x.SubType = r.string()
64 | x.QuotedInfo = r.readQuotedInfo()
65 | x.SessionUin = r.int64()
66 | x.TargetUin = r.int64()
67 | x.Content = r.arrayMsg()
68 | return x
69 | }
70 |
71 | func (w *writer) writeStoredGuildChannelMessage(x *db.StoredGuildChannelMessage) {
72 | if x == nil {
73 | w.nil()
74 | return
75 | }
76 | w.coder(coderStruct)
77 | w.string(x.ID)
78 | w.writeStoredGuildMessageAttribute(x.Attribute)
79 | w.uint64(x.GuildID)
80 | w.uint64(x.ChannelID)
81 | w.writeQuotedInfo(x.QuotedInfo)
82 | w.arrayMsg(x.Content)
83 | }
84 |
85 | func (r *reader) readStoredGuildChannelMessage() *db.StoredGuildChannelMessage {
86 | coder := r.coder()
87 | if coder == coderNil {
88 | return nil
89 | }
90 | x := &db.StoredGuildChannelMessage{}
91 | x.ID = r.string()
92 | x.Attribute = r.readStoredGuildMessageAttribute()
93 | x.GuildID = r.uint64()
94 | x.ChannelID = r.uint64()
95 | x.QuotedInfo = r.readQuotedInfo()
96 | x.Content = r.arrayMsg()
97 | return x
98 | }
99 |
100 | func (w *writer) writeStoredMessageAttribute(x *db.StoredMessageAttribute) {
101 | if x == nil {
102 | w.nil()
103 | return
104 | }
105 | w.coder(coderStruct)
106 | w.int32(x.MessageSeq)
107 | w.int32(x.InternalID)
108 | w.int64(x.SenderUin)
109 | w.string(x.SenderName)
110 | w.int64(x.Timestamp)
111 | }
112 |
113 | func (r *reader) readStoredMessageAttribute() *db.StoredMessageAttribute {
114 | coder := r.coder()
115 | if coder == coderNil {
116 | return nil
117 | }
118 | x := &db.StoredMessageAttribute{}
119 | x.MessageSeq = r.int32()
120 | x.InternalID = r.int32()
121 | x.SenderUin = r.int64()
122 | x.SenderName = r.string()
123 | x.Timestamp = r.int64()
124 | return x
125 | }
126 |
127 | func (w *writer) writeStoredGuildMessageAttribute(x *db.StoredGuildMessageAttribute) {
128 | if x == nil {
129 | w.nil()
130 | return
131 | }
132 | w.coder(coderStruct)
133 | w.uint64(x.MessageSeq)
134 | w.uint64(x.InternalID)
135 | w.uint64(x.SenderTinyID)
136 | w.string(x.SenderName)
137 | w.int64(x.Timestamp)
138 | }
139 |
140 | func (r *reader) readStoredGuildMessageAttribute() *db.StoredGuildMessageAttribute {
141 | coder := r.coder()
142 | if coder == coderNil {
143 | return nil
144 | }
145 | x := &db.StoredGuildMessageAttribute{}
146 | x.MessageSeq = r.uint64()
147 | x.InternalID = r.uint64()
148 | x.SenderTinyID = r.uint64()
149 | x.SenderName = r.string()
150 | x.Timestamp = r.int64()
151 | return x
152 | }
153 |
154 | func (w *writer) writeQuotedInfo(x *db.QuotedInfo) {
155 | if x == nil {
156 | w.nil()
157 | return
158 | }
159 | w.coder(coderStruct)
160 | w.string(x.PrevID)
161 | w.int32(x.PrevGlobalID)
162 | w.arrayMsg(x.QuotedContent)
163 | }
164 |
165 | func (r *reader) readQuotedInfo() *db.QuotedInfo {
166 | coder := r.coder()
167 | if coder == coderNil {
168 | return nil
169 | }
170 | x := &db.QuotedInfo{}
171 | x.PrevID = r.string()
172 | x.PrevGlobalID = r.int32()
173 | x.QuotedContent = r.arrayMsg()
174 | return x
175 | }
176 |
--------------------------------------------------------------------------------
/db/leveldb/writer.go:
--------------------------------------------------------------------------------
1 | package leveldb
2 |
3 | import (
4 | "bytes"
5 |
6 | "github.com/ProtocolScience/AstralGocq/global"
7 | )
8 |
9 | type intWriter struct {
10 | bytes.Buffer
11 | }
12 |
13 | func (w *intWriter) varint(x int64) {
14 | w.uvarint(uint64(x)<<1 ^ uint64(x>>63))
15 | }
16 |
17 | func (w *intWriter) uvarint(x uint64) {
18 | for x >= 0x80 {
19 | w.WriteByte(byte(x) | 0x80)
20 | x >>= 7
21 | }
22 | w.WriteByte(byte(x))
23 | }
24 |
25 | // writer implements the index write.
26 | //
27 | // data format(use uvarint to encode integers):
28 | //
29 | // - version
30 | // - string data length
31 | // - index data length
32 | // - string data
33 | // - index data
34 | //
35 | // for string data part, each string is encoded as:
36 | //
37 | // - string length
38 | // - string
39 | //
40 | // for index data part, each object value is encoded as:
41 | //
42 | // - coder
43 | // - value
44 | //
45 | // * coder is the identifier of value's type.
46 | // * specially for string, it's value is the offset in string data part.
47 | type writer struct {
48 | data intWriter
49 | strings intWriter
50 | stringIndex map[string]uint64
51 | }
52 |
53 | func newWriter() *writer {
54 | return &writer{
55 | stringIndex: make(map[string]uint64),
56 | }
57 | }
58 |
59 | func (w *writer) coder(o coder) { w.data.WriteByte(byte(o)) }
60 | func (w *writer) varint(x int64) { w.data.varint(x) }
61 | func (w *writer) uvarint(x uint64) { w.data.uvarint(x) }
62 | func (w *writer) nil() { w.coder(coderNil) }
63 | func (w *writer) int(i int) { w.varint(int64(i)) }
64 | func (w *writer) uint(i uint) { w.uvarint(uint64(i)) }
65 | func (w *writer) int32(i int32) { w.varint(int64(i)) }
66 | func (w *writer) uint32(i uint32) { w.uvarint(uint64(i)) }
67 | func (w *writer) int64(i int64) { w.varint(i) }
68 | func (w *writer) uint64(i uint64) { w.uvarint(i) }
69 |
70 | func (w *writer) string(s string) {
71 | off, ok := w.stringIndex[s]
72 | if !ok {
73 | // not found write to string data part
74 | // | string length | string |
75 | off = uint64(w.strings.Len())
76 | w.strings.uvarint(uint64(len(s)))
77 | _, _ = w.strings.WriteString(s)
78 | w.stringIndex[s] = off
79 | }
80 | // write offset to index data part
81 | w.uvarint(off)
82 | }
83 |
84 | func (w *writer) msg(m global.MSG) {
85 | w.uvarint(uint64(len(m)))
86 | for s, obj := range m {
87 | w.string(s)
88 | w.obj(obj)
89 | }
90 | }
91 |
92 | func (w *writer) arrayMsg(a []global.MSG) {
93 | w.uvarint(uint64(len(a)))
94 | for _, v := range a {
95 | w.msg(v)
96 | }
97 | }
98 |
99 | func (w *writer) obj(o any) {
100 | switch x := o.(type) {
101 | case nil:
102 | w.nil()
103 | case int:
104 | w.coder(coderInt)
105 | w.int(x)
106 | case int32:
107 | w.coder(coderInt32)
108 | w.int32(x)
109 | case int64:
110 | w.coder(coderInt64)
111 | w.int64(x)
112 | case uint:
113 | w.coder(coderUint)
114 | w.uint(x)
115 | case uint32:
116 | w.coder(coderUint32)
117 | w.uint32(x)
118 | case uint64:
119 | w.coder(coderUint64)
120 | w.uint64(x)
121 | case string:
122 | w.coder(coderString)
123 | w.string(x)
124 | case global.MSG:
125 | w.coder(coderMSG)
126 | w.msg(x)
127 | case []global.MSG:
128 | w.coder(coderArrayMSG)
129 | w.arrayMsg(x)
130 | default:
131 | panic("unsupported type")
132 | }
133 | }
134 |
135 | func (w *writer) bytes() []byte {
136 | var out intWriter
137 | out.uvarint(dataVersion)
138 | out.uvarint(uint64(w.strings.Len()))
139 | out.uvarint(uint64(w.data.Len()))
140 | _, _ = w.strings.WriteTo(&out)
141 | _, _ = w.data.WriteTo(&out)
142 | return out.Bytes()
143 | }
144 |
--------------------------------------------------------------------------------
/db/mongodb/mongodb.go:
--------------------------------------------------------------------------------
1 | package mongodb
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/pkg/errors"
7 | "go.mongodb.org/mongo-driver/bson"
8 | "go.mongodb.org/mongo-driver/mongo"
9 | "go.mongodb.org/mongo-driver/mongo/options"
10 | "gopkg.in/yaml.v3"
11 |
12 | "github.com/ProtocolScience/AstralGocq/db"
13 | )
14 |
15 | type database struct {
16 | uri string
17 | db string
18 | mongo *mongo.Database
19 | }
20 |
21 | // config mongodb 相关配置
22 | type config struct {
23 | Enable bool `yaml:"enable"`
24 | URI string `yaml:"uri"`
25 | Database string `yaml:"database"`
26 | }
27 |
28 | const (
29 | MongoGroupMessageCollection = "group-messages"
30 | MongoPrivateMessageCollection = "private-messages"
31 | MongoGuildChannelMessageCollection = "guild-channel-messages"
32 | )
33 |
34 | func init() {
35 | db.Register("database", func(node yaml.Node) db.Database {
36 | conf := new(config)
37 | _ = node.Decode(conf)
38 | if conf.Database == "" {
39 | conf.Database = "gocq-database"
40 | }
41 | if !conf.Enable {
42 | return nil
43 | }
44 | return &database{uri: conf.URI, db: conf.Database}
45 | })
46 | }
47 |
48 | func (m *database) Open() error {
49 | cli, err := mongo.Connect(context.Background(), options.Client().ApplyURI(m.uri))
50 | if err != nil {
51 | return errors.Wrap(err, "open mongo connection error")
52 | }
53 | m.mongo = cli.Database(m.db)
54 | return nil
55 | }
56 |
57 | func (m *database) GetMessageByGlobalID(id int32) (db.StoredMessage, error) {
58 | if r, err := m.GetGroupMessageByGlobalID(id); err == nil {
59 | return r, nil
60 | }
61 | return m.GetPrivateMessageByGlobalID(id)
62 | }
63 |
64 | func (m *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) {
65 | coll := m.mongo.Collection(MongoGroupMessageCollection)
66 | var ret db.StoredGroupMessage
67 | if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil {
68 | return nil, errors.Wrap(err, "query error")
69 | }
70 | return &ret, nil
71 | }
72 |
73 | func (m *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) {
74 | coll := m.mongo.Collection(MongoPrivateMessageCollection)
75 | var ret db.StoredPrivateMessage
76 | if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil {
77 | return nil, errors.Wrap(err, "query error")
78 | }
79 | return &ret, nil
80 | }
81 |
82 | func (m *database) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) {
83 | coll := m.mongo.Collection(MongoGuildChannelMessageCollection)
84 | var ret db.StoredGuildChannelMessage
85 | if err := coll.FindOne(context.Background(), bson.D{{"_id", id}}).Decode(&ret); err != nil {
86 | return nil, errors.Wrap(err, "query error")
87 | }
88 | return &ret, nil
89 | }
90 |
91 | func (m *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
92 | coll := m.mongo.Collection(MongoGroupMessageCollection)
93 | _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
94 | return errors.Wrap(err, "insert error")
95 | }
96 |
97 | func (m *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error {
98 | coll := m.mongo.Collection(MongoPrivateMessageCollection)
99 | _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
100 | return errors.Wrap(err, "insert error")
101 | }
102 |
103 | func (m *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error {
104 | coll := m.mongo.Collection(MongoGuildChannelMessageCollection)
105 | _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true))
106 | return errors.Wrap(err, "insert error")
107 | }
108 |
--------------------------------------------------------------------------------
/db/multidb.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "gopkg.in/yaml.v3"
6 |
7 | "github.com/ProtocolScience/AstralGocq/internal/base"
8 | )
9 |
10 | // backends 多数据库支持, 后端支持
11 | // 写入会对所有 Backend 进行写入
12 | // 读取只会读取第一个库
13 | var backends []Database
14 |
15 | // drivers 多数据库启动
16 | var drivers = make(map[string]func(node yaml.Node) Database)
17 |
18 | // DatabaseDisabledError 没有可用的db
19 | var DatabaseDisabledError = errors.New("database disabled")
20 |
21 | // Register 添加数据库后端
22 | func Register(name string, init func(yaml.Node) Database) {
23 | if _, ok := drivers[name]; ok {
24 | panic("database driver conflict: " + name)
25 | }
26 | drivers[name] = init
27 | }
28 |
29 | // Init 加载所有后端配置文件
30 | func Init() {
31 | backends = make([]Database, 0, len(drivers))
32 | for name, init := range drivers {
33 | if n, ok := base.Database[name]; ok {
34 | db := init(n)
35 | if db != nil {
36 | backends = append(backends, db)
37 | }
38 | }
39 | }
40 | }
41 |
42 | func Open() error {
43 | for _, b := range backends {
44 | if err := b.Open(); err != nil {
45 | return errors.Wrap(err, "open backend error")
46 | }
47 | }
48 | base.Database = nil
49 | return nil
50 | }
51 |
52 | func GetMessageByGlobalID(id int32) (StoredMessage, error) {
53 | if len(backends) == 0 {
54 | return nil, DatabaseDisabledError
55 | }
56 | return backends[0].GetMessageByGlobalID(id)
57 | }
58 |
59 | func GetGroupMessageByGlobalID(id int32) (*StoredGroupMessage, error) {
60 | if len(backends) == 0 {
61 | return nil, DatabaseDisabledError
62 | }
63 | return backends[0].GetGroupMessageByGlobalID(id)
64 | }
65 |
66 | func GetPrivateMessageByGlobalID(id int32) (*StoredPrivateMessage, error) {
67 | if len(backends) == 0 {
68 | return nil, DatabaseDisabledError
69 | }
70 | return backends[0].GetPrivateMessageByGlobalID(id)
71 | }
72 |
73 | func GetGuildChannelMessageByID(id string) (*StoredGuildChannelMessage, error) {
74 | if len(backends) == 0 {
75 | return nil, DatabaseDisabledError
76 | }
77 | return backends[0].GetGuildChannelMessageByID(id)
78 | }
79 |
80 | func InsertGroupMessage(m *StoredGroupMessage) error {
81 | for _, b := range backends {
82 | if err := b.InsertGroupMessage(m); err != nil {
83 | return errors.Wrap(err, "insert message to backend error")
84 | }
85 | }
86 | return nil
87 | }
88 |
89 | func InsertPrivateMessage(m *StoredPrivateMessage) error {
90 | for _, b := range backends {
91 | if err := b.InsertPrivateMessage(m); err != nil {
92 | return errors.Wrap(err, "insert message to backend error")
93 | }
94 | }
95 | return nil
96 | }
97 |
98 | func InsertGuildChannelMessage(m *StoredGuildChannelMessage) error {
99 | for _, b := range backends {
100 | if err := b.InsertGuildChannelMessage(m); err != nil {
101 | return errors.Wrap(err, "insert message to backend error")
102 | }
103 | }
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/db/sqlite3/model.go:
--------------------------------------------------------------------------------
1 | package sqlite3
2 |
3 | const (
4 | Sqlite3GroupMessageTableName = "grpmsg"
5 | Sqlite3MessageAttributeTableName = "msgattr"
6 | Sqlite3GuildMessageAttributeTableName = "gmsgattr"
7 | Sqlite3QuotedInfoTableName = "quoinf"
8 | Sqlite3PrivateMessageTableName = "privmsg"
9 | Sqlite3GuildChannelMessageTableName = "guildmsg"
10 | Sqlite3UinInfoTableName = "uininf"
11 | Sqlite3TinyInfoTableName = "tinyinf"
12 | )
13 |
14 | // StoredMessageAttribute 持久化消息属性
15 | type StoredMessageAttribute struct {
16 | ID int64 // ID is the crc64 of 字段s below
17 | MessageSeq int32
18 | InternalID int32
19 | SenderUin int64 // SenderUin is fk to UinInfo
20 | Timestamp int64
21 | }
22 |
23 | // StoredGuildMessageAttribute 持久化频道消息属性
24 | type StoredGuildMessageAttribute struct {
25 | ID int64 // ID is the crc64 of 字段s below
26 | MessageSeq int64
27 | InternalID int64
28 | SenderTinyID int64 // SenderTinyID is fk to TinyInfo
29 | Timestamp int64
30 | }
31 |
32 | // QuotedInfo 引用回复
33 | type QuotedInfo struct {
34 | ID int64 // ID is the crc64 of 字段s below
35 | PrevID string
36 | PrevGlobalID int32
37 | QuotedContent string // QuotedContent is json of original content
38 | }
39 |
40 | // UinInfo QQ 与 昵称
41 | type UinInfo struct {
42 | Uin int64
43 | Name string
44 | }
45 |
46 | // TinyInfo Tiny 与 昵称
47 | type TinyInfo struct {
48 | ID int64
49 | Name string
50 | }
51 |
52 | // StoredGroupMessage 持久化群消息
53 | type StoredGroupMessage struct {
54 | GlobalID int32
55 | ID string
56 | AttributeID int64
57 | SubType string
58 | QuotedInfoID int64
59 | GroupCode int64
60 | AnonymousID string
61 | Content string // Content is json of original content
62 | }
63 |
64 | // StoredPrivateMessage 持久化私聊消息
65 | type StoredPrivateMessage struct {
66 | GlobalID int32
67 | ID string
68 | AttributeID int64
69 | SubType string
70 | QuotedInfoID int64
71 | SessionUin int64
72 | TargetUin int64
73 | Content string // Content is json of original content
74 | }
75 |
76 | // StoredGuildChannelMessage 持久化频道消息
77 | type StoredGuildChannelMessage struct {
78 | ID string
79 | AttributeID int64
80 | GuildID int64
81 | ChannelID int64
82 | QuotedInfoID int64
83 | Content string // Content is json of original content
84 | }
85 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | USER=abc
4 |
5 | echo "---Setup Timezone to ${TZ}---"
6 | echo "${TZ}" > /etc/timezone
7 | echo "---Checking if UID: ${UID} matches user---"
8 | usermod -o -u ${UID} ${USER}
9 | echo "---Checking if GID: ${GID} matches user---"
10 | groupmod -o -g ${GID} ${USER} > /dev/null 2>&1 ||:
11 | usermod -g ${GID} ${USER}
12 | echo "---Setting umask to ${UMASK}---"
13 | umask ${UMASK}
14 |
15 | echo "---Taking ownership of data...---"
16 | chown -R ${UID}:${GID} /app /data
17 | chmod +x /app/cqhttp
18 |
19 | echo "Starting..."
20 | su-exec ${USER} /app/cqhttp "$@"
21 |
--------------------------------------------------------------------------------
/docs/EventFilter.md:
--------------------------------------------------------------------------------
1 | # 事件过滤器
2 |
3 | 在配置文件填写对应通信方式的 `middlewares.filter` 即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则不会启用事件过滤器。
4 | 事件过滤器会处理所有事件(包括心跳事件在内的元事件),请谨慎使用!!
5 |
6 | 注意: 与客户端建立连接的握手事件**不会**经过事件过滤器
7 |
8 | > 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
9 |
10 | ## 示例
11 |
12 | 这节首先给出一些示例,演示过滤器的基本用法,下一节将给出具体语法说明。
13 |
14 | ### 过滤所有事件
15 |
16 | ```json
17 | {
18 | ".not": {}
19 | }
20 | ```
21 |
22 | ### 只上报以「!!」开头的消息
23 |
24 | ```json
25 | {
26 | "raw_message": {
27 | ".regex": "^!!"
28 | }
29 | }
30 | ```
31 |
32 | ### 只上报群组的非匿名消息
33 |
34 | ```json
35 | {
36 | "message_type": "group",
37 | "anonymous": {
38 | ".eq": null
39 | }
40 | }
41 | ```
42 |
43 | ### 只上报私聊或特定群组的非匿名消息
44 |
45 | ```json
46 | {
47 | ".or": [
48 | {
49 | "message_type": "private"
50 | },
51 | {
52 | "message_type": "group",
53 | "group_id": {
54 | ".in": [
55 | 123456
56 | ]
57 | },
58 | "anonymous": {
59 | ".eq": null
60 | }
61 | }
62 | ]
63 | }
64 | ```
65 |
66 | ### 只上报群组 11111、22222、33333 中不是用户 12345 发送的消息,以及用户 66666 发送的所有消息
67 |
68 | ```json
69 | {
70 | ".or": [
71 | {
72 | "group_id": {
73 | ".in": [11111, 22222, 33333]
74 | },
75 | "user_id": {
76 | ".neq": 12345
77 | }
78 | },
79 | {
80 | "user_id": 66666
81 | }
82 | ]
83 | }
84 | ```
85 |
86 | ### 一个更复杂的例子
87 |
88 | ```json
89 | {
90 | ".or": [
91 | {
92 | "message_type": "private",
93 | "user_id": {
94 | ".not": {
95 | ".in": [11111, 22222, 33333]
96 | },
97 | ".neq": 44444
98 | }
99 | },
100 | {
101 | "message_type": {
102 | ".regex": "group|discuss"
103 | },
104 | ".or": [
105 | {
106 | "group_id": 12345
107 | },
108 | {
109 | "raw_message": {
110 | ".contains": "通知"
111 | }
112 | }
113 | ]
114 | }
115 | ]
116 | }
117 | ```
118 |
119 | ## 进阶指南
120 |
121 | 1. 对于嵌套的值,可以使用 `.` 进行简化,如
122 |
123 | ```json
124 | {
125 | "sender": {
126 | "sex": "male"
127 | }
128 | }
129 | ```
130 |
131 | 与下面的配置文件作用相同
132 |
133 | ```json
134 | {
135 | "sender.sex": "male"
136 | }
137 | ```
138 |
139 | 2. 对于数组,可以使用数字索引,如
140 | ```json
141 | {
142 | "message.0.type": "text"
143 | }
144 | ```
145 |
146 | 更多进阶语法请参考[GJSON语法](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)
147 |
148 | ## 语法说明
149 |
150 | 过滤规则最外层是一个 JSON 对象,其中的键,如果以 `.`(点号)开头,则表示运算符,其值为运算符的参数,如果不以 `.` 开头,则表示对事件数据对象中相应键的过滤。过滤规则中任何一个对象,只有在它的所有项都匹配的情况下,才会让事件通过(等价于一个 `and` 运算);其中,不以 `.` 开头的键,若其值不是对象,则只有在这个值和事件数据相应值相等的情况下,才会通过(等价于一个 `eq` 运算符)。
151 |
152 | 下面列出所有运算符(「要求的参数类型」是指运算符的键所对应的值的类型,「可作用于的类型」是指在过滤时事件对象相应值的类型):
153 |
154 | | 运算符 | 要求的参数类型 | 可作用于的类型 |
155 | | ----------- | -------------------------- | ----------------------------------------------------- |
156 | | `.not` | object | 任何 |
157 | | `.and` | object | 若参数中全为运算符,则任何;若不全为运算符,则 object |
158 | | `.or` | array(数组元素为 object) | 任何 |
159 | | `.eq` | 任何 | 任何 |
160 | | `.neq` | 任何 | 任何 |
161 | | `.in` | string/array | 若参数为 string,则 string;若参数为 array,则任何 |
162 | | `.contains` | string | string |
163 | | `.regex` | string | string |
164 |
165 |
166 | ## 过滤时的事件数据对象
167 |
168 | 过滤器在go-cqhttp构建好事件数据后运行,各事件的数据字段见[OneBot标准]( https://github.com/botuniverse/onebot-11/blob/master/event/README.md )。
169 |
170 | 这里有几点需要注意:
171 |
172 | - `message` 字段在运行过滤器时和上报信息类型相同(见 [消息格式]( https://github.com/botuniverse/onebot-11/blob/master/message/array.md ))
173 | - `raw_message` 字段为未经**CQ码**处理的原始消息字符串,这意味着其中可能会出现形如 `[CQ:face,id=123]` 的 CQ 码
174 |
--------------------------------------------------------------------------------
/docs/QA.md:
--------------------------------------------------------------------------------
1 | # 常见问题
2 |
3 | > 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
4 |
5 | ### Q: 为什么挂一段时间后就会出现 `消息发送失败,账号可能被风控`?
6 |
7 | ### A: 如果你刚开始使用 go-cqhttp 建议挂机3-7天,即可解除风控
8 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 文档
2 |
3 | > 文档目前依旧保留以便往前兼容
4 | \
5 | 下面的文档更易读以及人性化, 强烈建议您查看下面提供的文档
6 |
7 | 目前文档已移动到位于 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs) 的仓库
8 |
9 | 您可以在以下其中任意一个链接查看:
10 |
11 | -
12 | -
13 |
--------------------------------------------------------------------------------
/docs/adminApi.md:
--------------------------------------------------------------------------------
1 | # 管理 API
2 |
3 | > 支持跨域
4 |
5 | ## 公共参数
6 |
7 | 参数:
8 |
9 | | 参数名 | 类型 | 说明 |
10 | | ------------ | ------ | --------------------------- |
11 | | access_token | string | 校验口令,config.hjson中配置 |
12 |
13 |
14 |
15 | ## admin/do_restart
16 |
17 | ### 热重启
18 |
19 | > 热重启
20 |
21 | > ps: 目前不支持ws部分的修改生效
22 |
23 | method:`POST/GET`
24 |
25 | 参数:
26 |
27 | | 参数名 | 类型 | 说明 |
28 | | ------ | ---- | ---- |
29 | | 无 | | |
30 |
31 | 返回:
32 |
33 | ```json
34 | {"data": {}, "retcode": 0, "status": "ok"}
35 | ```
36 |
37 |
38 | ### admin/get_web_write
39 |
40 | > 拉取验证码/设备锁
41 |
42 | method: `GET`
43 |
44 |
45 | 参数:
46 |
47 | | 参数名 | 类型 | 说明 |
48 | | ------ | ---- | ---- |
49 | | 无 | | |
50 |
51 | 返回:
52 |
53 | ```json
54 | {"data": {"ispic": true,"picbase64":"xxxxx"}, "retcode": 0, "status": "ok"}
55 | ```
56 | | 参数名 | 类型 | 说明 |
57 | | -------- | ------ | --------------------------------------------------- |
58 | | ispic | bool | 是否是验证码类型 true是,false为不是(比如设备锁 |
59 | | picbas64 | string | 验证码的base64编码内容,加上头,放入img标签即可显示 |
60 |
61 | ### admin/do_web_write
62 |
63 | > web输入验证码/设备锁确认
64 |
65 | method: `POST` formdata
66 |
67 |
68 | 参数:
69 |
70 | | 参数名 | 类型 | 说明 |
71 | | ------ | ------ | ---------- |
72 | | input | string | 输入的内容 |
73 |
74 | 返回:
75 |
76 | ```json
77 | {"data": {}, "retcode": 0, "status": "ok"}
78 | ```
79 |
80 |
81 | ### admin/do_restart_docker
82 |
83 | > 冷重启
84 |
85 | > 注意:此api 会直接结束掉进程,需要依赖docker/supervisor等进程管理工具来自动拉起
86 |
87 | method: `POST`
88 |
89 |
90 | 参数:
91 |
92 | | 参数名 | 类型 | 说明 |
93 | | ------ | ---- | ---- |
94 | | 无 | | |
95 |
96 | 返回:
97 |
98 | ```json
99 | {"data": {}, "retcode": 0, "status": "ok"}
100 | ```
101 |
102 | ### admin/do_process_restart
103 |
104 | > 冷重启
105 |
106 | method: `POST`
107 |
108 |
109 | 参数:
110 |
111 | | 参数名 | 类型 | 说明 |
112 | | ------ | ---- | ---- |
113 | | 无 | | |
114 |
115 | 返回:
116 |
117 | ```json
118 | {"data": {}, "retcode": 0, "status": "ok"}
119 | ```
120 |
121 | ### admin/do_config_base
122 |
123 | > 基础配置
124 |
125 | method: `POST` formdata
126 |
127 |
128 | 参数:
129 |
130 | | 参数名 | 类型 | 说明 |
131 | | ------------ | ------ | ------------------------------------- |
132 | | uin | string | qq号 |
133 | | password | string | qq密码 |
134 | | enable_db | string | 是否启动数据库,填 'true' 或者 'false' |
135 | | access_token | string | 授权 token |
136 |
137 | 返回:
138 |
139 | ```json
140 | {"data": {}, "retcode": 0, "status": "ok"}
141 | ```
142 |
143 |
144 | ### admin/do_config_http
145 |
146 | > http服务配置
147 |
148 | method: `POST` formdata
149 |
150 | 参数:
151 |
152 | | 参数名 | 类型 | 说明 |
153 | | ----------- | ------ | --------------------------------------------- |
154 | | port | string | 服务端口 |
155 | | host | string | 服务监听地址 |
156 | | enable | string | 是否启用 ,填 'true' 或者 'false' |
157 | | timeout | string | http请求超时时间 |
158 | | post_url | string | post上报地址 不需要就填空字符串,或者不填 |
159 | | post_secret | string | post上报的secret 不需要就填空字符串,或者不填 |
160 |
161 | 返回:
162 |
163 | ```json
164 | {"data": {}, "retcode": 0, "status": "ok"}
165 | ```
166 |
167 |
168 | ### admin/do_config_ws
169 |
170 | > 正向ws设置
171 |
172 | method: `POST` formdata
173 |
174 | 参数:
175 |
176 | | 参数名 | 类型 | 说明 |
177 | | ------ | ------ | -------------------------------- |
178 | | port | string | 服务端口 |
179 | | host | string | 服务监听地址 |
180 | | enable | string | 是否启用 ,填 'true' 或者 'false' |
181 |
182 |
183 | 返回:
184 |
185 | ```json
186 | {"data": {}, "retcode": 0, "status": "ok"}
187 | ```
188 |
189 | ### admin/do_config_reverse
190 |
191 | > 反向ws配置
192 |
193 | method: `POST` formdata
194 |
195 | 参数:
196 |
197 | | 参数名 | 类型 | 说明 |
198 | | ------ | ------ | -------------------------------- |
199 | | port | string | 服务端口 |
200 | | host | string | 服务监听地址 |
201 | | enable | string | 是否启用 ,填 'true' 或者 'false' |
202 |
203 |
204 | 返回:
205 |
206 | ```json
207 | {"data": {}, "retcode": 0, "status": "ok"}
208 | ```
209 |
210 | ### admin/do_config_json
211 |
212 | > 直接修改 config.hjson配置
213 |
214 | method: `POST` formdata
215 |
216 | 参数:
217 |
218 | | 参数名 | 类型 | 说明 |
219 | | ------ | ------ | ----------------------------------- |
220 | | json | string | 完整的config.hjson的配合,json字符串 |
221 |
222 |
223 | 返回:
224 |
225 | ```json
226 | {"data": {}, "retcode": 0, "status": "ok"}
227 | ```
228 |
229 | ### admin/get_config_json
230 |
231 | > 获取当前 config.hjson配置
232 |
233 | method: `GET`
234 |
235 | 参数:
236 |
237 | | 参数名 | 类型 | 说明 |
238 | | ------ | ---- | ---- |
239 | | 无 | | |
240 |
241 |
242 | 返回:
243 |
244 | ```json
245 | {"data": {"config":"xxxx"}, "retcode": 0, "status": "ok"}
246 | ```
247 |
248 | | 参数名 | 类型 | 说明 |
249 | | ------ | ------ | ----------------------------------- |
250 | | config | string | 完整的config.hjson的配合,json字符串 |
251 |
252 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | # 配置
2 |
3 | > 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
4 |
5 | go-cqhttp 包含 `config.yml` 和 `device.json` 两个配置文件, 其中 `config.yml` 为运行配置 `device.json` 为虚拟设备信息.
6 |
7 | ## 配置信息
8 |
9 | go-cqhttp 的配置文件采用 YAML , 在使用之前希望你能了解 YAML 的语法([教程](https://www.runoob.com/w3cnote/yaml-intro.html))
10 |
11 | 默认生成的配置文件如下所示:
12 |
13 | ````yaml
14 | # go-cqhttp 默认配置文件
15 |
16 | account: # 账号相关
17 | uin: 1233456 # QQ账号
18 | password: '' # 密码为空时使用扫码登录
19 | encrypt: false # 是否开启密码加密
20 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
21 | relogin: # 重连设置
22 | delay: 3 # 首次重连延迟, 单位秒
23 | interval: 3 # 重连间隔
24 | max-times: 0 # 最大重连次数, 0为无限制
25 |
26 | # 是否使用服务器下发的新地址进行重连
27 | # 注意, 此设置可能导致在海外服务器上连接情况更差
28 | use-sso-address: true
29 |
30 | heartbeat:
31 | # 心跳频率, 单位秒
32 | # -1 为关闭心跳
33 | interval: 5
34 |
35 | message:
36 | # 上报数据类型
37 | # 可选: string,array
38 | post-format: string
39 | # 是否忽略无效的CQ码, 如果为假将原样发送
40 | ignore-invalid-cqcode: false
41 | # 是否强制分片发送消息
42 | # 分片发送将会带来更快的速度
43 | # 但是兼容性会有些问题
44 | force-fragment: false
45 | # 是否将url分片发送
46 | fix-url: false
47 | # 下载图片等请求网络代理
48 | proxy-rewrite: ''
49 | # 是否上报自身消息
50 | report-self-message: false
51 | # 移除服务端的Reply附带的At
52 | remove-reply-at: false
53 | # 为Reply附加更多信息
54 | extra-reply-data: false
55 | # 跳过 Mime 扫描, 忽略错误数据
56 | skip-mime-scan: false
57 |
58 | output:
59 | # 日志等级 trace,debug,info,warn,error
60 | log-level: warn
61 | # 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留.
62 | log-aging: 15
63 | # 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写
64 | log-force-new: true
65 | # 是否启用 DEBUG
66 | debug: false # 开启调试模式
67 |
68 | # 默认中间件锚点
69 | default-middlewares: &default
70 | # 访问密钥, 强烈推荐在公网的服务器设置
71 | access-token: ''
72 | # 事件过滤器文件目录
73 | filter: ''
74 | # API限速设置
75 | # 该设置为全局生效
76 | # 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
77 | # 目前该限速设置为令牌桶算法, 请参考:
78 | # https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
79 | rate-limit:
80 | enabled: false # 是否启用限速
81 | frequency: 1 # 令牌回复频率, 单位秒
82 | bucket: 1 # 令牌桶大小
83 |
84 | # 连接服务列表
85 | servers:
86 | # HTTP 通信设置
87 | - http:
88 | # 服务端监听地址
89 | # 如需指定监听ipv4, 可使用 `address: tcp4://0.0.0.0:5700` (ipv6同理)
90 | address: 0.0.0.0:5700
91 | # 反向HTTP超时时间, 单位秒
92 | # 最小值为5,小于5将会忽略本项设置
93 | timeout: 5
94 | middlewares:
95 | <<: *default # 引用默认中间件
96 | # 反向HTTP POST地址列表
97 | post:
98 | #- url: '' # 地址
99 | # secret: '' # 密钥
100 | #- url: 127.0.0.1:5701 # 地址
101 | # secret: '' # 密钥
102 |
103 | # 正向WS设置
104 | - ws:
105 | # 正向WS服务器监听地址
106 | # 如需指定监听ipv4, 可使用 `address: tcp4://0.0.0.0:6700` (ipv6同理)
107 | address: 0.0.0.0:6700
108 | middlewares:
109 | <<: *default # 引用默认中间件
110 |
111 | - ws-reverse:
112 | # 反向WS Universal 地址
113 | # 注意 设置了此项地址后下面两项将会被忽略
114 | universal: ws://your_websocket_universal.server
115 | # 反向WS API 地址
116 | api: ws://your_websocket_api.server
117 | # 反向WS Event 地址
118 | event: ws://your_websocket_event.server
119 | # 重连间隔 单位毫秒
120 | reconnect-interval: 3000
121 | middlewares:
122 | <<: *default # 引用默认中间件
123 | # pprof 性能分析服务器, 一般情况下不需要启用.
124 | # 如果遇到性能问题请上传报告给开发者处理
125 | # 注意: pprof服务不支持中间件、不支持鉴权. 请不要开放到公网
126 | - pprof:
127 | # pprof服务器监听地址
128 | host: 127.0.0.1
129 | # pprof服务器监听端口
130 | port: 7700
131 |
132 | # LambdaServer 配置
133 | - lambda:
134 | type: scf # 可用 scf,aws (aws未经过测试)
135 | middlewares:
136 | <<: *default # 引用默认中间件
137 |
138 | # 可添加更多
139 | #- ws-reverse:
140 | #- ws:
141 | #- http:
142 |
143 | database: # 数据库相关设置
144 | leveldb:
145 | # 是否启用内置leveldb数据库
146 | # 启用将会增加10-20MB的内存占用和一定的磁盘空间
147 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
148 | enable: true
149 | ````
150 |
151 | > 注1: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
152 | > 解密后密码的哈希将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
153 | > 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥
154 |
155 | > 注2: 对于不需要的通信方式,你可以使用注释将其停用(推荐),或者添加配置 `disabled: true` 将其关闭
156 |
157 | > 注3: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
158 |
159 | > 注4:关闭心跳服务可能引起断线,请谨慎关闭
160 |
161 | > 注5:关于MIME扫描, 详见[MIME](file.md#MIME)
162 |
163 | ### 环境变量
164 |
165 | go-cqhttp 配置文件可以使用占位符来读取**环境变量**的值。
166 |
167 | ```yaml
168 | account: # 账号相关
169 | uin: ${CQ_UIN} # 读取环境变量 CQ_UIN
170 | password: ${CQ_PWD:123456} # 当 CQ_PWD 为空时使用默认值 123456
171 | ```
172 |
173 | ## 在线状态
174 |
175 | | 状态 | 值 |
176 | | -----|----|
177 | | 在线 | 0 |
178 | | 离开 | 1 |
179 | | 隐身 | 2 |
180 | | 忙 | 3 |
181 | | 听歌中 | 4 |
182 | | 星座运势 | 5 |
183 | | 今日天气 | 6 |
184 | | 遇见春天 | 7 |
185 | | Timi中 | 8 |
186 | | 吃鸡中 | 9 |
187 | | 恋爱中 | 10 |
188 | | 汪汪汪 | 11 |
189 | | 干饭中 | 12 |
190 | | 学习中 | 13 |
191 | | 熬夜中 | 14 |
192 | | 打球中 | 15 |
193 | | 信号弱 | 16 |
194 | | 在线学习 | 17 |
195 | | 游戏中 | 18 |
196 | | 度假中 | 19 |
197 | | 追剧中 | 20 |
198 | | 健身中 | 21 |
199 |
200 | ## 设备信息
201 |
202 | 默认生成的设备信息如下所示:
203 |
204 | ``` json
205 | {
206 | "protocol": 0,
207 | "display": "xxx",
208 | "finger_print": "xxx",
209 | "boot_id": "xxx",
210 | "proc_version": "xxx",
211 | "imei": "xxx"
212 | }
213 | ```
214 |
215 | 在大部分情况下 我们只需要关心 `protocol` 字段:
216 |
217 | | 值 | 类型 | 限制 |
218 | | --- | ------------- | ---------------------------------------------------------------- |
219 | | 0 | iPad | 无 |
220 | | 1 | Android Phone | 无 |
221 | | 2 | Android Watch | 无法接收 `notify` 事件、无法接收口令红包、无法接收撤回消息 |
222 | | 3 | MacOS | 无 |
223 | | 4 | 企点 | 只能登录企点账号或企点子账号 |
224 |
225 | > 注意, 根据协议的不同, 各类消息有所限制
226 |
227 | ## 自定义服务器IP
228 |
229 | > 某些海外服务器使用默认地址可能会存在链路问题,此功能可以指定 go-cqhttp 连接哪些地址以达到最优化.
230 |
231 | 将文件 `address.txt` 创建到 `go-cqhttp` 工作目录, 并键入 `IP:PORT` 以换行符为分割即可.
232 |
233 | 示例:
234 |
235 | ````
236 | 1.1.1.1:53
237 | 1.1.2.2:8899
238 | ````
239 |
240 | ## 云函数部署
241 |
242 | 使用CustomRuntime进行部署, bootstrap 文件在 `scripts/bootstrap` 中已给出。
243 | 在部署前,请在本地完成登录,并将 `config.yml` , `device.json` ,`bootstrap` 和 `go-cqhttp`
244 | 一起打包。
245 |
246 | 在触发器中创建一个API网关触发器,并启用集成响应,创建完成后即可通过api网关访问go-cqhttp(建议配置 AccessToken)。
247 |
248 | > scripts/bootstrap 中使用的工作路径为 /tmp, 这个目录最大能容下500M文件, 如需长期使用,
249 | > 请挂载文件存储(CFS).
250 |
--------------------------------------------------------------------------------
/docs/file.md:
--------------------------------------------------------------------------------
1 | # 文件
2 |
3 | go-cqhttp 默认生成的文件树如下所示:
4 |
5 | ```
6 | .
7 | ├── go-cqhttp
8 | ├── config.yml
9 | ├── device.json
10 | ├── logs
11 | │ └── xx-xx-xx.log
12 | └── data
13 | ├── images
14 | │ └── xxxx.image
15 | └── levleldb
16 | ```
17 |
18 | | 文件 | 用途 |
19 | | ------------ | -------------------- |
20 | | go-cqhttp | go-cqhttp 可执行文件 |
21 | | config.yml | 运行配置文件 |
22 | | device.json | 虚拟设备配置文件 |
23 | | logs | 日志存放目录 |
24 | | data | 数据目录 |
25 | | data/leveldb | 数据库目录 |
26 | | data/images | 图片缓存目录 |
27 | | data/voices | 语音缓存目录 |
28 | | data/videos | 视频缓存目录 |
29 | | data/cache | 发送图片缓存目录 |
30 |
31 | ## 图片缓存文件
32 |
33 | 出于性能考虑,go-cqhttp 并不会将图片源文件下载到本地,而是生成一个可以和 QQ 服务器对应的缓存文件 (.image),该缓存文件结构如下:
34 |
35 | | 偏移 | 类型 | 说明 |
36 | | --------------- | -------- | -------------------- |
37 | | 0x00 | [16]byte | 图片源文件 MD5 HASH |
38 | | 0x10 | uint32 | 图片源文件大小 |
39 | | 0x14 | string | 图片原名(QQ内部ID) |
40 | | 0x14 + 原名长度 | string | 图片下载链接 |
41 |
42 | # MIME
43 |
44 | 启用MINE检查可以及时发现媒体资源格式错误引起的上传失败(通常表现为,请求网页图片,但服务端返回404.html)
45 |
46 | 在配置文件中设置 `skip-mine-scan: false`后 ,go-cqhttp 会在上传媒体资源(视频暂不支持)前对MIME进行检查,
47 | 详细允许类型如下所示:
48 |
49 | 图片:
50 | > image/bmp
51 | > image/gif
52 | > image/jpeg
53 | > image/png
54 | > image/webp
55 |
56 | 语音:
57 | > audio/aac
58 | > audio/aiff
59 | > audio/amr
60 | > audio/ape
61 | > audio/flac
62 | > audio/midi
63 | > audio/mp4
64 | > audio/mpeg
65 | > audio/ogg
66 | > audio/wav
67 | > audio/x-m4a
68 |
69 |
--------------------------------------------------------------------------------
/docs/quick_start.md:
--------------------------------------------------------------------------------
1 | # 开始
2 |
3 | 欢迎来到 go-cqhttp 文档 目前还在咕
4 |
5 | > 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.
6 |
7 | # 基础教程
8 | ## 下载
9 | 从[release](https://github.com/ProtocolScience/AstralGocq/releases)界面下载最新版本的go-cqhttp
10 |
11 | - Windows下32位文件为 `go-cqhttp-v*-windows-386.zip`
12 | - Windows下64位文件为 `go-cqhttp-v*-windows-amd64.zip`
13 | - Windows下arm用(如使用高通CPU的笔记本)文件为 `go-cqhttp-v*-windows-arm.zip`
14 | - Linux下32位文件为 `go-cqhttp-v*-linux-386.tar.gz`
15 | - Linux下64位文件为 `go-cqhttp-v*-linux-amd64.tar.gz`
16 | - Linux下arm用(如树莓派)文件为 `go-cqhttp-v*-linux-arm.tar.gz`
17 | - MD5文件为 `*.md5` ,用于校验文件完整性
18 | - 如果没有你所使用的系统版本或者希望自己构建,请移步[进阶指南-如何自己构建](#如何自己构建)
19 |
20 | ## 解压
21 |
22 | - Windows下请使用自己熟悉的解压软件自行解压
23 | - Linux下在命令行中输入 `tar -xzvf [文件名]`
24 |
25 | ## 使用
26 |
27 | ### Windows
28 |
29 | #### 标准方法
30 |
31 | 1. 双击`go-cqhttp.exe`此时将提示
32 | ```
33 | [WARNING]: 尝试加载配置文件 config.hjson 失败: 文件不存在
34 | [INFO]: 默认配置文件已生成,请编辑 config.hjson 后重启程序.
35 | ```
36 | 2. 参照[config.md](https://github.com/ProtocolScience/AstralGocq/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数
37 | 3. 再次双击`go-cqhttp.exe`
38 | ```
39 | [INFO]: 登录成功 欢迎使用: balabala
40 | ```
41 |
42 | 如出现需要认证的信息,请自行认证设备。
43 |
44 | 此时,基础配置完成
45 |
46 | #### 懒人法
47 |
48 | 1. [下载包含Windows.bat的zip](https://github.com/fkx4-p/go-cqhttp-lazy/archive/master.zip)
49 | 2. 解压
50 | 3. 将`Windows.bat`复制/剪切到**go-cqhttp**文件夹
51 | 4. 双击运行
52 |
53 | 效果如下
54 |
55 | ```
56 | QQ account:
57 | [QQ账号]
58 | QQ password:
59 | [QQ密码]
60 | enable http?(Y/n)
61 | [是否开启http(y/n),默认开启]
62 | enable ws?(Y/n)
63 | [是否开启websocket(y/n),默认开启]
64 | 请按任意键继续. . .
65 | ```
66 |
67 | 5. 双击`go-cqhttp.exe`
68 | ```
69 | [INFO]: 登录成功 欢迎使用: balabala
70 | ```
71 |
72 | 如出现需要认证的信息,请自行认证设备。
73 |
74 | 此时,基础配置完成
75 |
76 | ### Linux
77 |
78 | #### 标准方法
79 |
80 | 1. 打开一个命令行/ssh
81 | 2. `cd`到解压目录
82 | 3. 输入 `./go-cqhttp`,`Enter`运行 ,此时将提示
83 | ```
84 | [WARNING]: 尝试加载配置文件 config.hjson 失败: 文件不存在
85 | [INFO]: 默认配置文件已生成,请编辑 config.hjson 后重启程序.
86 | ```
87 |
88 | 4. 参照[config.md](https://github.com/ProtocolScience/AstralGocq/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数
89 | 5. 再次输入 `./go-cqhttp`,`Enter`运行
90 | ```
91 | [INFO]: 登录成功 欢迎使用: balabala
92 | ```
93 |
94 | 如出现需要认证的信息,请自行认证设备。
95 |
96 | 此时,基础配置完成
97 |
98 | #### 懒人法
99 |
100 | 暂时咕咕咕了
101 |
102 | ## 验证http是否成功配置
103 |
104 | 此时,如果在本地开启的服务器,可以在浏览器输入`http://127.0.0.1:5700/send_private_msg?user_id=[接收者qq号]&message=[发送的信息]`来发送一条测试信息
105 |
106 | 如果出现`{"data":{"message_id":balabala},"retcode":0,"status":"ok"}`则证明已经成功配置HTTP
107 |
108 | *注:请 连 中括号 也替换掉,就像这样:*`http://127.0.0.1:5700/send_private_msg?user_id=10001&message=ffeecoishp`
109 |
110 | # 进阶指南
111 |
112 | ## 跳过启动的五秒延时
113 |
114 | 使用命令行参数 `faststart`即可跳过启动的五秒钟延时,例如
115 |
116 | ```
117 | .\go-cqhttp.exe faststart
118 | ```
119 |
120 | ## 如何自己构建
121 |
122 | 1. [下载源码](https://github.com/ProtocolScience/AstralGocq/archive/master.zip)并解压 || 使用`git clone https://github.com/ProtocolScience/AstralGocq.git`来拉取
123 |
124 | 2. [下载golang binary release](https://golang.google.cn/dl/)并安装或者[自己构建golang](https://golang.google.cn/doc/install/source)
125 |
126 | 3. 在`cmd`或Linux命令行中,`cd`到目录中
127 |
128 | 4. 输入`go build -ldflags "-s -w -extldflags '-static'"`,`Enter`运行
129 |
130 | *注:可以使用*`go env -w GOPROXY=https://goproxy.cn,direct`*来加速国内依赖安装速度*
131 |
132 | ## 更新
133 |
134 | ### 方法一
135 |
136 | 从[release](https://github.com/ProtocolScience/AstralGocq/releases)界面下载最新版本的go-cqhttp
137 | 并替换之前的版本
138 |
139 | ### 方法二
140 |
141 | 使用更新参数,在命令行中打开go-cqhttp所在目录
142 | #### windows
143 | 输入指令
144 | `go-cqhttp.exe update`
145 |
146 | 如果在国内连接github下载速度可能很慢,可以使用镜像源下载
147 |
148 | `go-cqhttp.exe update https://github.rc1844.workers.dev`
149 |
150 | 几个可用的镜像源
151 | - `https://hub.fastgit.org`
152 | - `https://github.com.cnpmjs.org`
153 | - `https://github.bajins.com`
154 | - `https://github.rc1844.workers.dev`
155 |
156 | #### linux
157 | 方法与windows基本一致,将 `go-cqhttp.exe` 替换为 `./go-cqhttp`即可
158 |
--------------------------------------------------------------------------------
/docs/slider.md:
--------------------------------------------------------------------------------
1 | # 滑块验证码
2 |
3 | > 该文档已过期, 最新版本下可直接使用手机扫描二维码通过验证.
4 |
5 | 由于TX最新的限制, 所有协议在陌生设备/IP登录时都有可能被要求通过滑块验证码, 否则将会出现 `当前上网环境异常` 的错误. 目前我们准备了两个临时方案应对该验证码.
6 |
7 | > 如果您有一台运行Windows的PC/Server 并且不会抓包操作, 我们建议直接使用方案B
8 |
9 | ## 方案A: 自行抓包
10 |
11 | 由于滑块验证码和QQ本体的协议独立, 我们无法直接处理并提交. 需要在浏览器通过后抓包并获取 `Ticket` 提交.
12 |
13 | 该方案为具体的抓包教程, 如果您已经知道如何在浏览器中抓包. 可以略过接下来的文档并直接抓取 `cap_union_new_verify` 的返回值, 提取 `Ticket` 并在命令行提交.
14 |
15 | 首先获取滑块验证码的地址, 并在浏览器中打开. 这里以 *Microsoft Edge* 浏览器为例, *Chrome* 同理.
16 |
17 | 
18 |
19 | 首先选择 `1` 并提取链接在浏览器中打开
20 |
21 | 
22 |
23 | 
24 |
25 | 此时不要滑动验证码, 首先按下 `F12` (键盘右上角退格键上方) 打开 *开发者工具*
26 |
27 | 
28 |
29 | 点击 `Network` 选项卡 (在某些浏览器它可能叫做 `网络`)
30 |
31 | 
32 |
33 | 点开 `Filter` (箭头) 按钮以确定您能看到下面的工具栏, 勾选 `Preserve log`(红框)
34 |
35 | 此时可以滑动并通过验证码
36 |
37 | 
38 |
39 | 回到 *开发者工具*, 我们可以看到已经有了一个请求.
40 |
41 | 
42 |
43 | 此时如果有多个请求, 请不要慌张. 看到上面的 `Filter` 没? 此时在 `Filter` 输入框中输入 `cap_union_new`, 就应该只剩一个请求了.
44 |
45 | 然后点击该请求. 点开 `Preview` 选项卡 (箭头):
46 |
47 | 
48 |
49 | 此时就能看到一个标准的 `JSON`, 复制 `ticket` 字段并回到 `go-cqhttp` 粘贴. 即可通过滑块验证.
50 |
51 | 如果您看到这里还是不会如何操作, 没关系! 我们还准备了方案B.
52 |
53 | ## 方案B: 使用专用工具
54 |
55 | 此方案需要您有一台可以操作的 `Windows` 电脑.
56 |
57 | 首先下载工具: [蓝奏云](https://wws.lanzous.com/i2vn0jrofte) [Google Drive](https://drive.google.com/file/d/1peMDHqgP8AgWBVp5vP-cfhcGrb2ksSrE/view?usp=sharing)
58 |
59 | 解压并打开工具:
60 |
61 | 
62 |
63 | 打开 `go-cqhttp` 并选择 `2`:
64 |
65 | 
66 |
67 | 复制 `ID` 并前往工具粘贴:
68 |
69 | 
70 |
71 | 
72 |
73 | 点击 `OK` 并处理滑块, 完成即可登录成功. (OK可能反应稍微慢点, 请不要多次点击)
74 |
75 | 
76 |
77 |
--------------------------------------------------------------------------------
/global/all_test.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestVersionNameCompare(t *testing.T) {
11 | tests := [...]struct {
12 | current string
13 | remote string
14 | expected bool
15 | }{
16 | // Normal Tests:
17 | {"v0.9.29-fix2", "v0.9.29-fix2", false},
18 | {"v0.9.29-fix1", "v0.9.29-fix2", true},
19 | {"v0.9.29-fix2", "v0.9.29-fix1", false},
20 | {"v0.9.29-fix2", "v0.9.30", true},
21 | {"v1.0.0-alpha", "v1.0.0-alpha2", true},
22 | {"v1.0.0-alpha2", "v1.0.0-beta1", true},
23 | {"v1.0.0", "v1.0.0-beta1", false},
24 | {"v1.0.0-alpha", "v1.0.0", true},
25 | {"v1.0.0", "v1.0.0", false},
26 | {"v1.0.0-alpha", "v1.0.0-rc1", true},
27 |
28 | // Issue Fixes:
29 | {"v1.0.0-beta1", "v0.9.40-fix5", false}, // issue #877
30 | }
31 | for i := 0; i < len(tests); i++ {
32 | t.Run("test case "+strconv.Itoa(i), func(t *testing.T) {
33 | assert.Equal(t, tests[i].expected, VersionNameCompare(tests[i].current, tests[i].remote))
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/global/buffer.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "bytes"
5 |
6 | "github.com/ProtocolScience/AstralGo/binary" // 和 MiraiGo 共用同一 buffer 池
7 | )
8 |
9 | // NewBuffer 从池中获取新 bytes.Buffer
10 | func NewBuffer() *bytes.Buffer {
11 | return (*bytes.Buffer)(binary.SelectWriter())
12 | }
13 |
14 | // PutBuffer 将 Buffer放入池中
15 | func PutBuffer(buf *bytes.Buffer) {
16 | binary.PutWriter((*binary.Writer)(buf))
17 | }
18 |
--------------------------------------------------------------------------------
/global/codec.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "io"
7 | "os"
8 | "os/exec"
9 | "path"
10 |
11 | "github.com/pkg/errors"
12 |
13 | "github.com/ProtocolScience/AstralGocq/internal/base"
14 | )
15 |
16 | // GetSilkFileDuration 读 Silk 文件的真实音频时间长度
17 | func GetSilkFileDuration(resource io.Reader, frameMs int64) (int64, error) {
18 | if frameMs <= 0 {
19 | frameMs = 20
20 | }
21 |
22 | readByte := func(reader io.Reader) (byte, error) {
23 | buf := make([]byte, 1)
24 | _, err := reader.Read(buf)
25 | if err != nil {
26 | return 0, err
27 | }
28 | return buf[0], nil
29 | }
30 |
31 | readUnsignedShort := func(reader io.Reader) (int, error) {
32 | firstByte, err := readByte(reader)
33 | if err != nil {
34 | return 0, err
35 | }
36 | secondByte, err := readByte(reader)
37 | if err != nil {
38 | return 0, err
39 | }
40 | return int(firstByte) | (int(secondByte) << 8), nil
41 | }
42 |
43 | var tencentVersion bool
44 | firstByte, err := readByte(resource)
45 | if err != nil {
46 | return 0, err
47 | }
48 | if firstByte == 0x02 {
49 | tencentVersion = true
50 | }
51 |
52 | // Skip version-specific bytes
53 | for i := 0; i < 8; i++ {
54 | _, err := readByte(resource)
55 | if err != nil {
56 | return 0, err
57 | }
58 | }
59 |
60 | if tencentVersion {
61 | _, err := readByte(resource)
62 | if err != nil {
63 | return 0, err
64 | }
65 | }
66 |
67 | var packetCount int64
68 | for {
69 | size, err := readUnsignedShort(resource)
70 | if err != nil {
71 | if errors.Is(err, io.EOF) {
72 | break
73 | }
74 | return 0, err
75 | }
76 |
77 | if !tencentVersion && size == 0xffff {
78 | break
79 | }
80 |
81 | packetCount++
82 |
83 | // Skip current packet size
84 | for i := 0; i < size; i++ {
85 | _, err := readByte(resource)
86 | if err != nil {
87 | return 0, err
88 | }
89 | }
90 | }
91 |
92 | // Return total play time (seconds)
93 | return (packetCount * frameMs) / 1000, nil
94 | }
95 |
96 | // EncoderSilk 将音频编码为Silk
97 | func EncoderSilk(data []byte) ([]byte, error) {
98 | h := md5.New()
99 | _, err := h.Write(data)
100 | if err != nil {
101 | return nil, errors.Wrap(err, "calc md5 failed")
102 | }
103 | tempName := hex.EncodeToString(h.Sum(nil))
104 | if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) {
105 | return os.ReadFile(silkPath)
106 | }
107 | slk, err := base.EncodeSilk(data, tempName)
108 | if err != nil {
109 | return nil, errors.Wrap(err, "encode silk failed")
110 | }
111 | return slk, nil
112 | }
113 |
114 | // EncodeMP4 将给定视频文件编码为MP4
115 | func EncodeMP4(src string, dst string) error { // -y 覆盖文件
116 | cmd1 := exec.Command("ffmpeg", "-i", src, "-y", "-c", "copy", "-map", "0", dst)
117 | if errors.Is(cmd1.Err, exec.ErrDot) {
118 | cmd1.Err = nil
119 | }
120 | err := cmd1.Run()
121 | if err != nil {
122 | cmd2 := exec.Command("ffmpeg", "-i", src, "-y", "-c:v", "h264", "-c:a", "mp3", dst)
123 | if errors.Is(cmd2.Err, exec.ErrDot) {
124 | cmd2.Err = nil
125 | }
126 | return errors.Wrap(cmd2.Run(), "convert mp4 failed")
127 | }
128 | return err
129 | }
130 |
131 | // ExtractCover 获取给定视频文件的Cover
132 | func ExtractCover(src string, target string) error {
133 | cmd := exec.Command("ffmpeg", "-i", src, "-y", "-ss", "0", "-frames:v", "1", target)
134 | if errors.Is(cmd.Err, exec.ErrDot) {
135 | cmd.Err = nil
136 | }
137 | return errors.Wrap(cmd.Run(), "extract video cover failed")
138 | }
139 |
--------------------------------------------------------------------------------
/global/doc.go:
--------------------------------------------------------------------------------
1 | // Package global 包含文件下载,视频音频编码,本地文件缓存处理,消息过滤器,调用速率限制,gocq主配置等的相关函数与结构体
2 | package global
3 |
--------------------------------------------------------------------------------
/global/fs.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "encoding/hex"
7 | "errors"
8 | "net/netip"
9 | "net/url"
10 | "os"
11 | "path"
12 | "runtime"
13 | "strings"
14 |
15 | "github.com/ProtocolScience/AstralGo/utils"
16 | b14 "github.com/fumiama/go-base16384"
17 | "github.com/segmentio/asm/base64"
18 | log "github.com/sirupsen/logrus"
19 |
20 | "github.com/ProtocolScience/AstralGocq/internal/download"
21 | )
22 |
23 | const (
24 | // ImagePath go-cqhttp使用的图片缓存目录
25 | ImagePath = "data/images"
26 | // VoicePath go-cqhttp使用的语音缓存目录
27 | VoicePath = "data/voices"
28 | // VideoPath go-cqhttp使用的视频缓存目录
29 | VideoPath = "data/videos"
30 | // VersionsPath go-cqhttp使用的版本信息目录
31 | VersionsPath = "data/versions"
32 | // CachePath go-cqhttp使用的缓存目录
33 | CachePath = "data/cache"
34 | // DumpsPath go-cqhttp使用错误转储目录
35 | DumpsPath = "dumps"
36 | // HeaderAmr AMR文件头
37 | HeaderAmr = "#!AMR"
38 | // HeaderSilk Silkv3文件头
39 | HeaderSilk = "\x02#!SILK_V3"
40 | )
41 |
42 | // PathExists 判断给定path是否存在
43 | func PathExists(path string) bool {
44 | _, err := os.Stat(path)
45 | return err == nil || errors.Is(err, os.ErrExist)
46 | }
47 |
48 | // ReadAllText 读取给定path对应文件,无法读取时返回空值
49 | func ReadAllText(path string) string {
50 | b, err := os.ReadFile(path)
51 | if err != nil {
52 | log.Error(err)
53 | return ""
54 | }
55 | return string(b)
56 | }
57 |
58 | // WriteAllText 将给定text写入给定path
59 | func WriteAllText(path, text string) error {
60 | return os.WriteFile(path, utils.S2B(text), 0o644)
61 | }
62 |
63 | // Check 检测err是否为nil
64 | func Check(err error, deleteSession bool) {
65 | if err != nil {
66 | if deleteSession && PathExists("session.token") {
67 | _ = os.Remove("session.token")
68 | }
69 | log.Fatalf("遇到错误: %v", err)
70 | }
71 | }
72 |
73 | // IsAMRorSILK 判断给定文件是否为Amr或Silk格式
74 | func IsAMRorSILK(b []byte) bool {
75 | return bytes.HasPrefix(b, []byte(HeaderAmr)) || bytes.HasPrefix(b, []byte(HeaderSilk))
76 | }
77 |
78 | // FindFile 从给定的File寻找文件,并返回文件byte数组。File是一个合法的URL。p为文件寻找位置。
79 | // 对于HTTP/HTTPS形式的URL,Cache为"1"或空时表示启用缓存
80 | func FindFile(file, cache, p string) (data []byte, err error) {
81 | data, err = nil, os.ErrNotExist
82 | switch {
83 | case strings.HasPrefix(file, "http"): // https also has prefix http
84 | hash := md5.Sum([]byte(file))
85 | cacheFile := path.Join(CachePath, hex.EncodeToString(hash[:])+".cache")
86 | if (cache == "" || cache == "1") && PathExists(cacheFile) {
87 | return os.ReadFile(cacheFile)
88 | }
89 | err = download.Request{URL: file}.WriteToFile(cacheFile)
90 | if err != nil {
91 | return nil, err
92 | }
93 | return os.ReadFile(cacheFile)
94 | case strings.HasPrefix(file, "base64"):
95 | data, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(file, "base64://"))
96 | if err != nil {
97 | return nil, err
98 | }
99 | case strings.HasPrefix(file, "base16384"):
100 | data, err = b14.UTF82UTF16BE(utils.S2B(strings.TrimPrefix(file, "base16384://")))
101 | if err != nil {
102 | return nil, err
103 | }
104 | data = b14.Decode(data)
105 | case strings.HasPrefix(file, "file"):
106 | var fu *url.URL
107 | fu, err = url.Parse(file)
108 | if err != nil {
109 | return nil, err
110 | }
111 | if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` {
112 | fu.Path = fu.Path[1:]
113 | }
114 | data, err = os.ReadFile(fu.Path)
115 | if err != nil {
116 | return nil, err
117 | }
118 | case PathExists(path.Join(p, file)):
119 | data, err = os.ReadFile(path.Join(p, file))
120 | if err != nil {
121 | return nil, err
122 | }
123 | }
124 | return
125 | }
126 |
127 | // DelFile 删除一个给定path,并返回删除结果
128 | func DelFile(path string) bool {
129 | err := os.Remove(path)
130 | if err != nil {
131 | // 删除失败
132 | log.Error(err)
133 | return false
134 | }
135 | // 删除成功
136 | log.Info(path + "删除成功")
137 | return true
138 | }
139 |
140 | // ReadAddrFile 从给定path中读取合法的IP地址与端口,每个IP地址以换行符"\n"作为分隔
141 | func ReadAddrFile(path string) []netip.AddrPort {
142 | d, err := os.ReadFile(path)
143 | if err != nil {
144 | return nil
145 | }
146 | str := string(d)
147 | lines := strings.Split(str, "\n")
148 | var ret []netip.AddrPort
149 | for _, l := range lines {
150 | addr, err := netip.ParseAddrPort(l)
151 | if err == nil {
152 | ret = append(ret, addr)
153 | }
154 | }
155 | return ret
156 | }
157 |
--------------------------------------------------------------------------------
/global/log_hook.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "path/filepath"
8 | "reflect"
9 | "strings"
10 | "sync"
11 |
12 | "github.com/mattn/go-colorable"
13 | "github.com/sirupsen/logrus"
14 | )
15 |
16 | // LocalHook logrus本地钩子
17 | type LocalHook struct {
18 | lock *sync.Mutex
19 | levels []logrus.Level // hook级别
20 | formatter logrus.Formatter // 格式
21 | path string // 写入path
22 | writer io.Writer // io
23 | }
24 |
25 | // Levels ref: logrus/hooks.go impl Hook interface
26 | func (hook *LocalHook) Levels() []logrus.Level {
27 | if len(hook.levels) == 0 {
28 | return logrus.AllLevels
29 | }
30 | return hook.levels
31 | }
32 |
33 | func (hook *LocalHook) ioWrite(entry *logrus.Entry) error {
34 | log, err := hook.formatter.Format(entry)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | _, err = hook.writer.Write(log)
40 | if err != nil {
41 | return err
42 | }
43 | return nil
44 | }
45 |
46 | func (hook *LocalHook) pathWrite(entry *logrus.Entry) error {
47 | dir := filepath.Dir(hook.path)
48 | if err := os.MkdirAll(dir, os.ModePerm); err != nil {
49 | return err
50 | }
51 |
52 | fd, err := os.OpenFile(hook.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
53 | if err != nil {
54 | return err
55 | }
56 | defer fd.Close()
57 |
58 | log, err := hook.formatter.Format(entry)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | _, err = fd.Write(log)
64 | return err
65 | }
66 |
67 | // Fire ref: logrus/hooks.go impl Hook interface
68 | func (hook *LocalHook) Fire(entry *logrus.Entry) error {
69 | hook.lock.Lock()
70 | defer hook.lock.Unlock()
71 |
72 | if hook.writer != nil {
73 | return hook.ioWrite(entry)
74 | }
75 |
76 | if hook.path != "" {
77 | return hook.pathWrite(entry)
78 | }
79 |
80 | return nil
81 | }
82 |
83 | // SetFormatter 设置日志格式
84 | func (hook *LocalHook) SetFormatter(consoleFormatter, fileFormatter logrus.Formatter) {
85 | hook.lock.Lock()
86 | defer hook.lock.Unlock()
87 |
88 | // 支持处理windows平台的console色彩
89 | logrus.SetOutput(colorable.NewColorableStdout())
90 | // 用于在console写出
91 | logrus.SetFormatter(consoleFormatter)
92 | // 用于写入文件
93 | hook.formatter = fileFormatter
94 | }
95 |
96 | // SetWriter 设置Writer
97 | func (hook *LocalHook) SetWriter(writer io.Writer) {
98 | hook.lock.Lock()
99 | defer hook.lock.Unlock()
100 | hook.writer = writer
101 | }
102 |
103 | // SetPath 设置日志写入路径
104 | func (hook *LocalHook) SetPath(path string) {
105 | hook.lock.Lock()
106 | defer hook.lock.Unlock()
107 | hook.path = path
108 | }
109 |
110 | // NewLocalHook 初始化本地日志钩子实现
111 | func NewLocalHook(args any, consoleFormatter, fileFormatter logrus.Formatter, levels ...logrus.Level) *LocalHook {
112 | hook := &LocalHook{
113 | lock: new(sync.Mutex),
114 | }
115 | hook.SetFormatter(consoleFormatter, fileFormatter)
116 | hook.levels = append(hook.levels, levels...)
117 |
118 | switch arg := args.(type) {
119 | case string:
120 | hook.SetPath(arg)
121 | case io.Writer:
122 | hook.SetWriter(arg)
123 | default:
124 | panic(fmt.Sprintf("unsupported type: %v", reflect.TypeOf(args)))
125 | }
126 |
127 | return hook
128 | }
129 |
130 | // GetLogLevel 获取日志等级
131 | //
132 | // 可能的值有
133 | //
134 | // "trace","debug","info","warn","warn","error"
135 | func GetLogLevel(level string) []logrus.Level {
136 | switch level {
137 | case "trace":
138 | return []logrus.Level{
139 | logrus.TraceLevel, logrus.DebugLevel,
140 | logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel,
141 | logrus.FatalLevel, logrus.PanicLevel,
142 | }
143 | case "debug":
144 | return []logrus.Level{
145 | logrus.DebugLevel, logrus.InfoLevel,
146 | logrus.WarnLevel, logrus.ErrorLevel,
147 | logrus.FatalLevel, logrus.PanicLevel,
148 | }
149 | case "info":
150 | return []logrus.Level{
151 | logrus.InfoLevel, logrus.WarnLevel,
152 | logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel,
153 | }
154 | case "warn":
155 | return []logrus.Level{
156 | logrus.WarnLevel, logrus.ErrorLevel,
157 | logrus.FatalLevel, logrus.PanicLevel,
158 | }
159 | case "error":
160 | return []logrus.Level{
161 | logrus.ErrorLevel, logrus.FatalLevel,
162 | logrus.PanicLevel,
163 | }
164 | default:
165 | return []logrus.Level{
166 | logrus.InfoLevel, logrus.WarnLevel,
167 | logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel,
168 | }
169 | }
170 | }
171 |
172 | // LogFormat specialize for go-cqhttp
173 | type LogFormat struct {
174 | EnableColor bool
175 | }
176 |
177 | // Format implements logrus.Formatter
178 | func (f LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
179 | buf := NewBuffer()
180 | defer PutBuffer(buf)
181 |
182 | if f.EnableColor {
183 | buf.WriteString(GetLogLevelColorCode(entry.Level))
184 | }
185 |
186 | buf.WriteByte('[')
187 | buf.WriteString(entry.Time.Format("2006-01-02 15:04:05"))
188 | buf.WriteString("] [")
189 | buf.WriteString(strings.ToUpper(entry.Level.String()))
190 | buf.WriteString("]: ")
191 | buf.WriteString(entry.Message)
192 | buf.WriteString(" \n")
193 |
194 | if f.EnableColor {
195 | buf.WriteString(colorReset)
196 | }
197 |
198 | ret := make([]byte, len(buf.Bytes()))
199 | copy(ret, buf.Bytes()) // copy buffer
200 | return ret, nil
201 | }
202 |
203 | const (
204 | colorCodePanic = "\x1b[1;31m" // color.Style{color.Bold, color.Red}.String()
205 | colorCodeFatal = "\x1b[1;31m" // color.Style{color.Bold, color.Red}.String()
206 | colorCodeError = "\x1b[31m" // color.Style{color.Red}.String()
207 | colorCodeWarn = "\x1b[33m" // color.Style{color.Yellow}.String()
208 | colorCodeInfo = "\x1b[37m" // color.Style{color.White}.String()
209 | colorCodeDebug = "\x1b[32m" // color.Style{color.Green}.String()
210 | colorCodeTrace = "\x1b[36m" // color.Style{color.Cyan}.String()
211 | colorReset = "\x1b[0m"
212 | )
213 |
214 | // GetLogLevelColorCode 获取日志等级对应色彩code
215 | func GetLogLevelColorCode(level logrus.Level) string {
216 | switch level {
217 | case logrus.PanicLevel:
218 | return colorCodePanic
219 | case logrus.FatalLevel:
220 | return colorCodeFatal
221 | case logrus.ErrorLevel:
222 | return colorCodeError
223 | case logrus.WarnLevel:
224 | return colorCodeWarn
225 | case logrus.InfoLevel:
226 | return colorCodeInfo
227 | case logrus.DebugLevel:
228 | return colorCodeDebug
229 | case logrus.TraceLevel:
230 | return colorCodeTrace
231 |
232 | default:
233 | return colorCodeInfo
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/global/net.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tidwall/gjson"
7 |
8 | "github.com/ProtocolScience/AstralGocq/internal/download"
9 | )
10 |
11 | // QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
12 | func QQMusicSongInfo(id string) (gjson.Result, error) {
13 | d, err := download.Request{URL: `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`}.JSON()
14 | if err != nil {
15 | return gjson.Result{}, err
16 | }
17 | return d.Get("songinfo.data"), nil
18 | }
19 |
20 | // NeteaseMusicSongInfo 通过给定id在wdd音乐上查找曲目信息
21 | func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
22 | d, err := download.Request{URL: fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id)}.JSON()
23 | if err != nil {
24 | return gjson.Result{}, err
25 | }
26 | return d.Get("songs.0"), nil
27 | }
28 |
--------------------------------------------------------------------------------
/global/param.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "regexp"
5 | "strconv"
6 |
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | // MSG 消息Map
11 | type MSG = map[string]any
12 |
13 | // VersionNameCompare 检查版本名是否需要更新, 仅适用于 go-cqhttp 的版本命名规则
14 | //
15 | // 例: v0.9.29-fix2 == v0.9.29-fix2 -> false
16 | //
17 | // v0.9.29-fix1 < v0.9.29-fix2 -> true
18 | //
19 | // v0.9.29-fix2 > v0.9.29-fix1 -> false
20 | //
21 | // v0.9.29-fix2 < v0.9.30 -> true
22 | //
23 | // v1.0.0-alpha2 < v1.0.0-beta1 -> true
24 | //
25 | // v1.0.0 > v1.0.0-beta1 -> false
26 | func VersionNameCompare(current, remote string) bool {
27 | defer func() { // 应该不会panic, 为了保险还是加个
28 | if err := recover(); err != nil {
29 | log.Warn("检查更新失败!")
30 | }
31 | }()
32 | sp := regexp.MustCompile(`v(\d+)\.(\d+)\.(\d+)-?(.+)?`)
33 | cur := sp.FindStringSubmatch(current)
34 | re := sp.FindStringSubmatch(remote)
35 | for i := 1; i <= 3; i++ {
36 | curSub, _ := strconv.Atoi(cur[i])
37 | reSub, _ := strconv.Atoi(re[i])
38 | if curSub != reSub {
39 | return curSub < reSub
40 | }
41 | }
42 | if cur[4] == "" || re[4] == "" {
43 | return re[4] == "" && cur[4] != re[4]
44 | }
45 | return cur[4] < re[4]
46 | }
47 |
--------------------------------------------------------------------------------
/global/signal.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "sync"
9 | "time"
10 |
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | var (
15 | mainStopCh chan struct{}
16 | mainOnce sync.Once
17 |
18 | dumpMutex sync.Mutex
19 | )
20 |
21 | func dumpStack() {
22 | dumpMutex.Lock()
23 | defer dumpMutex.Unlock()
24 |
25 | log.Info("开始 dump 当前 goroutine stack 信息")
26 |
27 | buf := make([]byte, 1024)
28 | for {
29 | n := runtime.Stack(buf, true)
30 | if n < len(buf) {
31 | buf = buf[:n]
32 | break
33 | }
34 | buf = make([]byte, 2*len(buf))
35 | }
36 |
37 | fileName := fmt.Sprintf("%s.%d.stacks.%d.log", filepath.Base(os.Args[0]), os.Getpid(), time.Now().Unix())
38 | fd, err := os.Create(fileName)
39 | if err != nil {
40 | log.Errorf("保存 stackdump 到文件时出现错误: %v", err)
41 | log.Warnf("无法保存 stackdump. 将直接打印\n %s", buf)
42 | return
43 | }
44 | defer fd.Close()
45 | _, err = fd.Write(buf)
46 | if err != nil {
47 | log.Errorf("写入 stackdump 失败: %v", err)
48 | log.Warnf("无法保存 stackdump. 将直接打印\n %s", buf)
49 | return
50 | }
51 | log.Infof("stackdump 已保存至 %s", fileName)
52 | }
53 |
--------------------------------------------------------------------------------
/global/signal_unix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package global
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "sync"
10 | "syscall"
11 | )
12 |
13 | // SetupMainSignalHandler is for main to use at last
14 | func SetupMainSignalHandler() <-chan struct{} {
15 | mainOnce.Do(func() {
16 | mainStopCh = make(chan struct{})
17 | mc := make(chan os.Signal, 4)
18 | closeOnce := sync.Once{}
19 | signal.Notify(mc, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGUSR1)
20 | go func() {
21 | for {
22 | switch <-mc {
23 | case os.Interrupt, syscall.SIGTERM:
24 | closeOnce.Do(func() {
25 | close(mainStopCh)
26 | })
27 | case syscall.SIGQUIT, syscall.SIGUSR1:
28 | dumpStack()
29 | }
30 | }
31 | }()
32 | })
33 | return mainStopCh
34 | }
35 |
--------------------------------------------------------------------------------
/global/signal_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package global
5 |
6 | import (
7 | "errors"
8 | "fmt"
9 | "net"
10 | "os"
11 | "os/signal"
12 | "strings"
13 | "sync"
14 | "syscall"
15 | "time"
16 |
17 | "github.com/Microsoft/go-winio"
18 | log "github.com/sirupsen/logrus"
19 | )
20 |
21 | var validTasks = map[string]func(){
22 | "dumpstack": dumpStack,
23 | }
24 |
25 | // SetupMainSignalHandler is for main to use at last
26 | func SetupMainSignalHandler() <-chan struct{} {
27 | mainOnce.Do(func() {
28 | // for stack trace collecting on windows
29 | pipeName := fmt.Sprintf(`\\.\pipe\go-cqhttp-%d`, os.Getpid())
30 | pipe, err := winio.ListenPipe(pipeName, &winio.PipeConfig{})
31 | if err != nil {
32 | log.Errorf("创建 named pipe 失败. 将无法使用 dumpstack 功能: %v", err)
33 | } else {
34 | maxTaskLen := 0
35 | for t := range validTasks {
36 | if l := len(t); l > maxTaskLen {
37 | maxTaskLen = l
38 | }
39 | }
40 | go func() {
41 | for {
42 | c, err := pipe.Accept()
43 | if err != nil {
44 | if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), "closed") {
45 | return
46 | }
47 | log.Errorf("accept named pipe 失败: %v", err)
48 | continue
49 | }
50 | go func() {
51 | defer c.Close()
52 | _ = c.SetReadDeadline(time.Now().Add(5 * time.Second))
53 | buf := make([]byte, maxTaskLen)
54 | n, err := c.Read(buf)
55 | if err != nil {
56 | log.Errorf("读取 named pipe 失败: %v", err)
57 | return
58 | }
59 | cmd := string(buf[:n])
60 | if task, ok := validTasks[cmd]; ok {
61 | task()
62 | return
63 | }
64 | log.Warnf("named pipe 读取到未知指令: %q", cmd)
65 | }()
66 | }
67 | }()
68 | }
69 | // setup the main stop channel
70 | mainStopCh = make(chan struct{})
71 | mc := make(chan os.Signal, 2)
72 | closeOnce := sync.Once{}
73 | signal.Notify(mc, os.Interrupt, syscall.SIGTERM)
74 | go func() {
75 | for {
76 | switch <-mc {
77 | case os.Interrupt, syscall.SIGTERM:
78 | closeOnce.Do(func() {
79 | close(mainStopCh)
80 | if pipe != nil {
81 | _ = pipe.Close()
82 | }
83 | })
84 | }
85 | }
86 | }()
87 | })
88 | return mainStopCh
89 | }
90 |
--------------------------------------------------------------------------------
/global/terminal/doc.go:
--------------------------------------------------------------------------------
1 | // Package terminal 包含用于检测在windows下是否通过双击运行go-cqhttp, 禁用快速编辑, 启用VT100的函数
2 | package terminal
3 |
--------------------------------------------------------------------------------
/global/terminal/double_click.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package terminal
4 |
5 | // RunningByDoubleClick 检查是否通过双击直接运行,非Windows系统永远返回false
6 | func RunningByDoubleClick() bool {
7 | return false
8 | }
9 |
10 | // NoMoreDoubleClick 提示用户不要双击运行,非Windows系统永远返回nil
11 | func NoMoreDoubleClick() error {
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/global/terminal/double_click_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "unsafe"
7 |
8 | "golang.org/x/sys/windows"
9 |
10 | "github.com/pkg/errors"
11 | )
12 |
13 | // RunningByDoubleClick 检查是否通过双击直接运行
14 | func RunningByDoubleClick() bool {
15 | kernel32 := windows.NewLazySystemDLL("kernel32.dll")
16 | lp := kernel32.NewProc("GetConsoleProcessList")
17 | if lp != nil {
18 | var ids [2]uint32
19 | var maxCount uint32 = 2
20 | ret, _, _ := lp.Call(uintptr(unsafe.Pointer(&ids)), uintptr(maxCount))
21 | if ret > 1 {
22 | return false
23 | }
24 | }
25 | return true
26 | }
27 |
28 | // NoMoreDoubleClick 提示用户不要双击运行,并生成安全启动脚本
29 | func NoMoreDoubleClick() error {
30 | toHighDPI()
31 | r := boxW(getConsoleWindows(), "请勿通过双击直接运行本程序, 这将导致一些非预料的后果.\n请在shell中运行./go-cqhttp.exe\n点击确认将释出安全启动脚本,点击取消则关闭程序", "警告", 0x00000030|0x00000001)
32 | if r == 2 {
33 | return nil
34 | }
35 | r = boxW(0, "点击确认将覆盖go-cqhttp.bat,点击取消则关闭程序", "警告", 0x00000030|0x00000001)
36 | if r == 2 {
37 | return nil
38 | }
39 | f, err := os.OpenFile("go-cqhttp.bat", os.O_CREATE|os.O_RDWR, 0o666)
40 | if err != nil {
41 | return err
42 | }
43 | if err != nil {
44 | return errors.Errorf("打开go-cqhttp.bat失败: %v", err)
45 | }
46 | _ = f.Truncate(0)
47 |
48 | ex, _ := os.Executable()
49 | exPath := filepath.Base(ex)
50 | _, err = f.WriteString("%Created by go-cqhttp. DO NOT EDIT ME!%\nstart cmd /K \"" + exPath + "\"")
51 | if err != nil {
52 | return errors.Errorf("写入go-cqhttp.bat失败: %v", err)
53 | }
54 | f.Close()
55 | boxW(0, "安全启动脚本已生成,请双击go-cqhttp.bat启动", "提示", 0x00000040|0x00000000)
56 | return nil
57 | }
58 |
59 | // BoxW of Win32 API. Check https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw for more detail.
60 | func boxW(hwnd uintptr, caption, title string, flags uint) int {
61 | captionPtr, _ := windows.UTF16PtrFromString(caption)
62 | titlePtr, _ := windows.UTF16PtrFromString(title)
63 | u32 := windows.NewLazySystemDLL("user32.dll")
64 | ret, _, _ := u32.NewProc("MessageBoxW").Call(
65 | hwnd,
66 | uintptr(unsafe.Pointer(captionPtr)),
67 | uintptr(unsafe.Pointer(titlePtr)),
68 | uintptr(flags))
69 |
70 | return int(ret)
71 | }
72 |
73 | // GetConsoleWindows retrieves the window handle used by the console associated with the calling process.
74 | func getConsoleWindows() (hWnd uintptr) {
75 | hWnd, _, _ = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetConsoleWindow").Call()
76 | return
77 | }
78 |
79 | // toHighDPI tries to raise DPI awareness context to DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED
80 | func toHighDPI() {
81 | systemAware := ^uintptr(2) + 1
82 | unawareGDIScaled := ^uintptr(5) + 1
83 | u32 := windows.NewLazySystemDLL("user32.dll")
84 | proc := u32.NewProc("SetThreadDpiAwarenessContext")
85 | if proc.Find() != nil {
86 | return
87 | }
88 | for i := unawareGDIScaled; i <= systemAware; i++ {
89 | _, _, _ = u32.NewProc("SetThreadDpiAwarenessContext").Call(i)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/global/terminal/quick_edit.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package terminal
4 |
5 | // RestoreInputMode 还原输入模式,非Windows系统永远返回nil
6 | func RestoreInputMode() error {
7 | return nil
8 | }
9 |
10 | // DisableQuickEdit 禁用快速编辑,非Windows系统永远返回nil
11 | func DisableQuickEdit() error {
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/global/terminal/quick_edit_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 |
6 | "golang.org/x/sys/windows"
7 | )
8 |
9 | var inputmode uint32
10 |
11 | // RestoreInputMode 还原输入模式
12 | func RestoreInputMode() error {
13 | if inputmode == 0 {
14 | return nil
15 | }
16 | stdin := windows.Handle(os.Stdin.Fd())
17 | return windows.SetConsoleMode(stdin, inputmode)
18 | }
19 |
20 | // DisableQuickEdit 禁用快速编辑
21 | func DisableQuickEdit() error {
22 | stdin := windows.Handle(os.Stdin.Fd())
23 |
24 | var mode uint32
25 | err := windows.GetConsoleMode(stdin, &mode)
26 | if err != nil {
27 | return err
28 | }
29 | inputmode = mode
30 |
31 | mode &^= windows.ENABLE_QUICK_EDIT_MODE // 禁用快速编辑模式
32 | mode |= windows.ENABLE_EXTENDED_FLAGS // 启用扩展标志
33 |
34 | mode &^= windows.ENABLE_MOUSE_INPUT // 禁用鼠标输入
35 | mode |= windows.ENABLE_PROCESSED_INPUT // 启用控制输入
36 |
37 | mode &^= windows.ENABLE_INSERT_MODE // 禁用插入模式
38 | mode |= windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT // 启用输入回显&逐行输入
39 |
40 | mode &^= windows.ENABLE_WINDOW_INPUT // 禁用窗口输入
41 | mode &^= windows.ENABLE_VIRTUAL_TERMINAL_INPUT // 禁用虚拟终端输入
42 |
43 | return windows.SetConsoleMode(stdin, mode)
44 | }
45 |
--------------------------------------------------------------------------------
/global/terminal/title.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package terminal
4 |
5 | import (
6 | "fmt"
7 | "time"
8 |
9 | "github.com/ProtocolScience/AstralGocq/internal/base"
10 | )
11 |
12 | // SetTitle 设置标题为 go-cqhttp `版本` `版权`
13 | func SetTitle() {
14 | fmt.Printf("\033]0;go-cqhttp "+base.Version+" © 2020 - %d Mrs4s"+"\007", time.Now().Year())
15 | }
16 |
--------------------------------------------------------------------------------
/global/terminal/title_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "fmt"
5 | "syscall"
6 | "time"
7 | "unsafe"
8 |
9 | "golang.org/x/sys/windows"
10 |
11 | "github.com/ProtocolScience/AstralGocq/internal/base"
12 | )
13 |
14 | func setConsoleTitle(title string) error {
15 | p0, err := syscall.UTF16PtrFromString(title)
16 | if err != nil {
17 | return err
18 | }
19 | r1, _, err := windows.NewLazySystemDLL("kernel32.dll").NewProc("SetConsoleTitleW").Call(uintptr(unsafe.Pointer(p0)))
20 | if r1 == 0 {
21 | return err
22 | }
23 | return nil
24 | }
25 |
26 | // SetTitle 设置标题为 go-cqhttp `版本` `版权`
27 | func SetTitle() {
28 | _ = setConsoleTitle(fmt.Sprintf("go-cqhttp "+base.Version+" © 2020 - %d Mrs4s", time.Now().Year()))
29 | }
30 |
--------------------------------------------------------------------------------
/global/terminal/vt100.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package terminal
4 |
5 | // EnableVT100 启用颜色、控制字符,非Windows系统永远返回nil
6 | func EnableVT100() error {
7 | return nil
8 | }
9 |
--------------------------------------------------------------------------------
/global/terminal/vt100_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 |
6 | "golang.org/x/sys/windows"
7 | )
8 |
9 | // EnableVT100 启用颜色、控制字符
10 | func EnableVT100() error {
11 | stdout := windows.Handle(os.Stdout.Fd())
12 |
13 | var mode uint32
14 | err := windows.GetConsoleMode(stdout, &mode)
15 | if err != nil {
16 | return err
17 | }
18 |
19 | mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING // 启用虚拟终端处理
20 | mode |= windows.ENABLE_PROCESSED_OUTPUT // 启用处理后的输出
21 |
22 | return windows.SetConsoleMode(stdout, mode)
23 | }
24 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ProtocolScience/AstralGocq
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/FloatTech/sqlite v1.6.3
7 | github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a
8 | github.com/ProtocolScience/AstralGo v0.0.0-20250410021903-6f58986fa78e
9 | github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d
10 | github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e
11 | github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5
12 | github.com/fumiama/go-base16384 v1.7.0
13 | github.com/fumiama/go-hide-param v0.1.4
14 | github.com/google/uuid v1.3.0
15 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
16 | github.com/mattn/go-colorable v0.1.13
17 | github.com/pkg/errors v0.9.1
18 | github.com/segmentio/asm v1.2.0
19 | github.com/sirupsen/logrus v1.9.3
20 | github.com/stretchr/testify v1.8.1
21 | github.com/syndtr/goleveldb v1.0.0
22 | github.com/tidwall/gjson v1.15.0
23 | github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60
24 | go.mongodb.org/mongo-driver v1.12.0
25 | golang.org/x/crypto v0.17.0
26 | golang.org/x/image v0.10.0
27 | golang.org/x/sys v0.15.0
28 | golang.org/x/term v0.15.0
29 | golang.org/x/time v0.3.0
30 | gopkg.ilharper.com/x/isatty v1.1.1
31 | gopkg.in/yaml.v3 v3.0.1
32 | )
33 |
34 | require (
35 | github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b // indirect
36 | github.com/davecgh/go-spew v1.1.1 // indirect
37 | github.com/fumiama/imgsz v0.0.2 // indirect
38 | github.com/golang/snappy v0.0.4 // indirect
39 | github.com/jonboulle/clockwork v0.3.0 // indirect
40 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
41 | github.com/klauspost/compress v1.13.6 // indirect
42 | github.com/kr/pretty v0.3.1 // indirect
43 | github.com/lestrrat-go/strftime v1.0.6 // indirect
44 | github.com/mattn/go-isatty v0.0.16 // indirect
45 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
46 | github.com/pierrec/lz4/v4 v4.1.15 // indirect
47 | github.com/pmezard/go-difflib v1.0.0 // indirect
48 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
49 | github.com/tidwall/match v1.1.1 // indirect
50 | github.com/tidwall/pretty v1.2.0 // indirect
51 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect
52 | github.com/xdg-go/scram v1.1.2 // indirect
53 | github.com/xdg-go/stringprep v1.0.4 // indirect
54 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
55 | golang.org/x/mod v0.12.0 // indirect
56 | golang.org/x/sync v0.3.0 // indirect
57 | golang.org/x/text v0.14.0 // indirect
58 | golang.org/x/tools v0.11.0 // indirect
59 | lukechampine.com/uint128 v1.2.0 // indirect
60 | modernc.org/cc/v3 v3.40.0 // indirect
61 | modernc.org/ccgo/v3 v3.16.13 // indirect
62 | modernc.org/libc v1.21.5 // indirect
63 | modernc.org/mathutil v1.5.0 // indirect
64 | modernc.org/memory v1.4.0 // indirect
65 | modernc.org/opt v0.1.3 // indirect
66 | modernc.org/sqlite v1.20.0 // indirect
67 | modernc.org/strutil v1.1.3 // indirect
68 | modernc.org/token v1.0.1 // indirect
69 | )
70 |
--------------------------------------------------------------------------------
/internal/base/feature.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | )
6 |
7 | // silk encode features
8 | var (
9 | EncodeSilk = encodeSilk // 编码 SilkV3 音频
10 | ResampleSilk = resampleSilk // 将silk重新编码为 24000 bit rate
11 | )
12 |
13 | func encodeSilk(_ []byte, _ string) ([]byte, error) {
14 | return nil, errors.New("not supported now")
15 | }
16 |
17 | func resampleSilk(data []byte) []byte {
18 | return data
19 | }
20 |
--------------------------------------------------------------------------------
/internal/base/flag.go:
--------------------------------------------------------------------------------
1 | // Package base provides base config for go-cqhttp
2 | package base
3 |
4 | import (
5 | "flag"
6 | "fmt"
7 | "os"
8 | "time"
9 |
10 | log "github.com/sirupsen/logrus"
11 | "gopkg.in/yaml.v3"
12 |
13 | "github.com/ProtocolScience/AstralGocq/modules/config"
14 | )
15 |
16 | // command flags
17 | var (
18 | LittleC string // config file
19 | LittleD bool // daemon
20 | LittleH bool // Help
21 | LittleWD string // working directory
22 | )
23 |
24 | // config file flags
25 | var (
26 | Debug bool // 是否开启 debug 模式
27 | RemoveReplyAt bool // 是否删除reply后的at
28 | ExtraReplyData bool // 是否上报额外reply信息
29 | IgnoreInvalidCQCode bool // 是否忽略无效CQ码
30 | SplitURL bool // 是否分割URL
31 | ForceFragmented bool // 是否启用强制分片
32 | SkipMimeScan bool // 是否跳过Mime扫描
33 | ConvertWebpImage bool // 是否转换Webp图片
34 | ReportSelfMessage bool // 是否上报自身消息
35 | UseSSOAddress bool // 是否使用服务器下发的新地址进行重连
36 | LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志
37 | LogColorful bool // 是否启用日志颜色
38 | FastStart bool // 是否为快速启动
39 | AllowTempSession bool // 是否允许发送临时会话信息
40 | // UpdateProtocol bool // 是否更新协议
41 | SignServers []config.SignServer // 使用特定的服务器进行签名
42 | HTTPTimeout int // download 超时时间
43 | SignServerTimeout int // 签名服务器超时时间
44 |
45 | PostFormat string // 上报格式 string or array
46 | Proxy string // 存储 proxy_rewrite,用于设置代理
47 | PasswordHash [16]byte // 存储QQ密码哈希供登录使用
48 | AccountToken []byte // 存储 AccountToken 供登录使用
49 | Account *config.Account // 账户配置
50 | Reconnect *config.Reconnect // 重连配置
51 | LogLevel string // 日志等级
52 | LogAging = time.Hour * 24 * 365 // 日志时效
53 | HeartbeatInterval = time.Second * 5 // 心跳间隔
54 |
55 | Servers []map[string]yaml.Node // 连接服务列表
56 | Database map[string]yaml.Node // 数据库列表
57 | )
58 |
59 | // Parse parse flags
60 | func Parse() {
61 | flag.StringVar(&LittleC, "c", "config.yml", "configuration filename")
62 | flag.BoolVar(&LittleD, "d", false, "running as a daemon")
63 | flag.BoolVar(&LittleH, "h", false, "this Help")
64 | flag.StringVar(&LittleWD, "w", "", "cover the working directory")
65 | d := flag.Bool("D", false, "debug mode")
66 | flag.BoolVar(&FastStart, "faststart", false, "skip waiting 5 seconds")
67 | // flag.BoolVar(&UpdateProtocol, "update-protocol", false, "update protocol")
68 | flag.Parse()
69 |
70 | if *d {
71 | Debug = true
72 | }
73 | }
74 |
75 | // Init read config from yml file
76 | func Init() {
77 | conf := config.Parse(LittleC)
78 | { // bool config
79 | if conf.Output.Debug {
80 | Debug = true
81 | }
82 | IgnoreInvalidCQCode = conf.Message.IgnoreInvalidCQCode
83 | SplitURL = conf.Message.FixURL
84 | RemoveReplyAt = conf.Message.RemoveReplyAt
85 | ExtraReplyData = conf.Message.ExtraReplyData
86 | ForceFragmented = conf.Message.ForceFragment
87 | SkipMimeScan = conf.Message.SkipMimeScan
88 | ConvertWebpImage = conf.Message.ConvertWebpImage
89 | ReportSelfMessage = conf.Message.ReportSelfMessage
90 | UseSSOAddress = conf.Account.UseSSOAddress
91 | AllowTempSession = conf.Account.AllowTempSession
92 | SignServers = conf.Account.SignServers
93 | HTTPTimeout = conf.Message.HTTPTimeout
94 | SignServerTimeout = int(conf.Account.SignServerTimeout)
95 | }
96 | { // others
97 | Proxy = conf.Message.ProxyRewrite
98 | Account = conf.Account
99 | Reconnect = conf.Account.ReLogin
100 | Servers = conf.Servers
101 | Database = conf.Database
102 | LogLevel = conf.Output.LogLevel
103 | LogColorful = conf.Output.LogColorful == nil || *conf.Output.LogColorful
104 | if conf.Message.PostFormat != "string" && conf.Message.PostFormat != "array" {
105 | log.Warnf("post-format 配置错误, 将自动使用 string")
106 | PostFormat = "string"
107 | } else {
108 | PostFormat = conf.Message.PostFormat
109 | }
110 | if conf.Output.LogAging > 0 {
111 | LogAging = time.Hour * 24 * time.Duration(conf.Output.LogAging)
112 | }
113 | if conf.Heartbeat.Interval > 0 {
114 | HeartbeatInterval = time.Second * time.Duration(conf.Heartbeat.Interval)
115 | }
116 | if conf.Heartbeat.Disabled || conf.Heartbeat.Interval < 0 {
117 | HeartbeatInterval = 0
118 | }
119 | }
120 | }
121 |
122 | // Help cli命令行-h的帮助提示
123 | func Help() {
124 | fmt.Printf(`go-cqhttp service
125 | version: %s
126 | Usage:
127 | server [OPTIONS]
128 | Options:
129 | `, Version)
130 |
131 | flag.PrintDefaults()
132 | os.Exit(0)
133 | }
134 |
--------------------------------------------------------------------------------
/internal/base/version.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import "runtime/debug"
4 |
5 | // Version go-cqhttp的版本信息,在编译时使用ldflags进行覆盖
6 | var Version = "unknown"
7 |
8 | func init() {
9 | if Version != "unknown" {
10 | return
11 | }
12 | info, ok := debug.ReadBuildInfo()
13 | if ok {
14 | Version = info.Main.Version
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/internal/cache/cache.go:
--------------------------------------------------------------------------------
1 | // Package cache impl the cache for gocq
2 | package cache
3 |
4 | import (
5 | pb "github.com/ProtocolScience/AstralGo/client/pb/database"
6 | "github.com/RomiChan/protobuf/proto"
7 | log "github.com/sirupsen/logrus"
8 | "github.com/syndtr/goleveldb/leveldb"
9 | "github.com/syndtr/goleveldb/leveldb/opt"
10 | "sync"
11 | "time"
12 | )
13 |
14 | // Media Cache DBs
15 | var (
16 | Media Cache
17 | )
18 |
19 | // Cache wraps the btree.DB for concurrent safe
20 | type Cache struct {
21 | ldb *leveldb.DB
22 | clean sync.Mutex
23 | lastCleanTime uint32
24 | }
25 |
26 | // ScanExpiredData 扫描并删除过期数据,防止gocq的数据库越来越肥硕
27 | func (c *Cache) ScanExpiredData() {
28 | // 创建迭代器
29 | c.clean.Lock()
30 | curTime := uint32(time.Now().Unix())
31 | if curTime-c.lastCleanTime > 3600 {
32 | c.lastCleanTime = curTime
33 | iter := c.ldb.NewIterator(nil, nil)
34 | for iter.Next() {
35 | value := iter.Value()
36 | result := pb.DatabaseRecord{}
37 | if proto.Unmarshal(value, &result) == nil {
38 | if curTime > result.Register.ExpiredTime {
39 | _ = c.ldb.Delete(iter.Key(), nil)
40 | }
41 | }
42 | }
43 | iter.Release()
44 | }
45 | defer c.clean.Unlock()
46 | }
47 |
48 | // Insert 添加媒体缓存
49 | func (c *Cache) Insert(md5 []byte, record *pb.DatabaseRecord) {
50 | c.ScanExpiredData()
51 | record.Register = &pb.DataRegister{
52 | ExpiredTime: uint32(time.Now().Unix()) + 2592000,
53 | }
54 | data, _ := proto.Marshal(record)
55 | _ = c.ldb.Put(md5, data, nil)
56 | }
57 |
58 | // Get 获取缓存信息
59 | func (c *Cache) Get(md5 []byte) *pb.DatabaseRecord {
60 | c.ScanExpiredData()
61 | got, _ := c.ldb.Get(md5, nil)
62 | result := pb.DatabaseRecord{}
63 | err := proto.Unmarshal(got, &result)
64 | if err != nil {
65 | return nil
66 | }
67 | c.Insert(md5, &result) //update time
68 | return &result
69 | }
70 |
71 | // Delete 删除指定缓存
72 | func (c *Cache) Delete(md5 []byte) {
73 | _ = c.ldb.Delete(md5, nil)
74 | }
75 |
76 | // Init 初始化 Cache
77 | func Init() {
78 | open := func(typ, path string, cache *Cache) {
79 | ldb, err := leveldb.OpenFile(path, &opt.Options{
80 | WriteBuffer: 4 * opt.KiB,
81 | })
82 | if err != nil {
83 | log.Fatalf("open cache %s db failed: %v", typ, err)
84 | }
85 | cache.ldb = ldb
86 | }
87 | open("database", "data/database", &Media)
88 | }
89 |
--------------------------------------------------------------------------------
/internal/mime/mime.go:
--------------------------------------------------------------------------------
1 | // Package mime 提供MIME检查功能
2 | package mime
3 |
4 | import (
5 | "io"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/ProtocolScience/AstralGocq/internal/base"
10 | )
11 |
12 | const limit = 4 * 1024
13 |
14 | func scan(r io.ReadSeeker) string {
15 | _, _ = r.Seek(0, io.SeekStart)
16 | defer r.Seek(0, io.SeekStart)
17 | in := make([]byte, limit)
18 | _, _ = r.Read(in)
19 | return http.DetectContentType(in)
20 | }
21 |
22 | // CheckImage 判断给定流是否为合法图片
23 | // 返回 是否合法, 实际Mime
24 | // 判断后会自动将 Stream Seek 至 0
25 | func CheckImage(r io.ReadSeeker) (t string, ok bool) {
26 | if base.SkipMimeScan {
27 | return "", true
28 | }
29 | if r == nil {
30 | return "image/nil-stream", false
31 | }
32 | t = scan(r)
33 | switch t {
34 | case "image/bmp", "image/gif", "image/jpeg", "image/png", "image/webp":
35 | ok = true
36 | }
37 | return
38 | }
39 |
40 | // CheckAudio 判断给定流是否为合法音频
41 | func CheckAudio(r io.ReadSeeker) (string, bool) {
42 | if base.SkipMimeScan {
43 | return "", true
44 | }
45 | t := scan(r)
46 | // std mime type detection is not full supported for audio
47 | if strings.Contains(t, "text") || strings.Contains(t, "image") {
48 | return t, false
49 | }
50 | return t, true
51 | }
52 |
--------------------------------------------------------------------------------
/internal/msg/element.go:
--------------------------------------------------------------------------------
1 | // Package msg 提供了go-cqhttp消息中间表示,CQ码处理等等
2 | package msg
3 |
4 | import (
5 | "bytes"
6 | "strings"
7 | "unicode/utf8"
8 |
9 | "github.com/ProtocolScience/AstralGo/binary"
10 | )
11 |
12 | // @@@ CQ码转义处理 @@@
13 |
14 | // EscapeText 将字符串raw中部分字符转义
15 | //
16 | // - & -> &
17 | // - [ -> [
18 | // - ] -> ]
19 | func EscapeText(s string) string {
20 | count := strings.Count(s, "&")
21 | count += strings.Count(s, "[")
22 | count += strings.Count(s, "]")
23 | if count == 0 {
24 | return s
25 | }
26 |
27 | // Apply replacements to buffer.
28 | var b strings.Builder
29 | b.Grow(len(s) + count*4)
30 | start := 0
31 | for i := 0; i < count; i++ {
32 | j := start
33 | for index, r := range s[start:] {
34 | if r == '&' || r == '[' || r == ']' {
35 | j += index
36 | break
37 | }
38 | }
39 | b.WriteString(s[start:j])
40 | switch s[j] {
41 | case '&':
42 | b.WriteString("&")
43 | case '[':
44 | b.WriteString("[")
45 | case ']':
46 | b.WriteString("]")
47 | }
48 | start = j + 1
49 | }
50 | b.WriteString(s[start:])
51 | return b.String()
52 | }
53 |
54 | // EscapeValue 将字符串value中部分字符转义
55 | //
56 | // - , -> ,
57 | // - & -> &
58 | // - [ -> [
59 | // - ] -> ]
60 | func EscapeValue(value string) string {
61 | ret := EscapeText(value)
62 | return strings.ReplaceAll(ret, ",", ",")
63 | }
64 |
65 | // UnescapeText 将字符串content中部分字符反转义
66 | //
67 | // - & -> &
68 | // - [ -> [
69 | // - ] -> ]
70 | func UnescapeText(content string) string {
71 | ret := content
72 | ret = strings.ReplaceAll(ret, "[", "[")
73 | ret = strings.ReplaceAll(ret, "]", "]")
74 | ret = strings.ReplaceAll(ret, "&", "&")
75 | return ret
76 | }
77 |
78 | // UnescapeValue 将字符串content中部分字符反转义
79 | //
80 | // - , -> ,
81 | // - & -> &
82 | // - [ -> [
83 | // - ] -> ]
84 | func UnescapeValue(content string) string {
85 | ret := strings.ReplaceAll(content, ",", ",")
86 | return UnescapeText(ret)
87 | }
88 |
89 | // @@@ 消息中间表示 @@@
90 |
91 | // Pair key value pair
92 | type Pair struct {
93 | K string
94 | V string
95 | }
96 |
97 | // Element single message
98 | type Element struct {
99 | Type string
100 | Data []Pair
101 | }
102 |
103 | // Get 获取指定值
104 | func (e *Element) Get(k string) string {
105 | for _, datum := range e.Data {
106 | if datum.K == k {
107 | return datum.V
108 | }
109 | }
110 | return ""
111 | }
112 |
113 | // CQCode convert element to cqcode
114 | func (e *Element) CQCode() string {
115 | buf := strings.Builder{}
116 | e.WriteCQCodeTo(&buf)
117 | return buf.String()
118 | }
119 |
120 | // WriteCQCodeTo write element's cqcode into sb
121 | func (e *Element) WriteCQCodeTo(sb *strings.Builder) {
122 | if e.Type == "text" {
123 | sb.WriteString(EscapeText(e.Data[0].V)) // must be {"text": value}
124 | return
125 | }
126 | sb.WriteString("[CQ:")
127 | sb.WriteString(e.Type)
128 | for _, data := range e.Data {
129 | sb.WriteByte(',')
130 | sb.WriteString(data.K)
131 | sb.WriteByte('=')
132 | sb.WriteString(EscapeValue(data.V))
133 | }
134 | sb.WriteByte(']')
135 | }
136 |
137 | // MarshalJSON see encoding/json.Marshaler
138 | func (e *Element) MarshalJSON() ([]byte, error) {
139 | return binary.NewWriterF(func(w *binary.Writer) {
140 | buf := (*bytes.Buffer)(w)
141 | // fmt.Fprintf(buf, `{"type":"%s","data":{`, e.Type)
142 | buf.WriteString(`{"type":"`)
143 | buf.WriteString(e.Type)
144 | buf.WriteString(`","data":{`)
145 | for i, data := range e.Data {
146 | if i != 0 {
147 | buf.WriteByte(',')
148 | }
149 | // fmt.Fprintf(buf, `"%s":%q`, data.K, data.V)
150 | buf.WriteByte('"')
151 | buf.WriteString(data.K)
152 | buf.WriteString(`":`)
153 | buf.WriteString(QuoteJSON(data.V))
154 | }
155 | buf.WriteString(`}}`)
156 | }), nil
157 | }
158 |
159 | const hex = "0123456789abcdef"
160 |
161 | // QuoteJSON 按JSON转义为字符加上双引号
162 | func QuoteJSON(s string) string {
163 | i, j := 0, 0
164 | var b strings.Builder
165 | b.WriteByte('"')
166 | for j < len(s) {
167 | c := s[j]
168 |
169 | if c >= 0x20 && c <= 0x7f && c != '\\' && c != '"' {
170 | // fast path: most of the time, printable ascii characters are used
171 | j++
172 | continue
173 | }
174 |
175 | switch c {
176 | case '\\', '"', '\n', '\r', '\t':
177 | b.WriteString(s[i:j])
178 | b.WriteByte('\\')
179 | switch c {
180 | case '\n':
181 | c = 'n'
182 | case '\r':
183 | c = 'r'
184 | case '\t':
185 | c = 't'
186 | }
187 | b.WriteByte(c)
188 | j++
189 | i = j
190 | continue
191 |
192 | case '<', '>', '&':
193 | b.WriteString(s[i:j])
194 | b.WriteString(`\u00`)
195 | b.WriteByte(hex[c>>4])
196 | b.WriteByte(hex[c&0xF])
197 | j++
198 | i = j
199 | continue
200 | }
201 |
202 | // This encodes bytes < 0x20 except for \t, \n and \r.
203 | if c < 0x20 {
204 | b.WriteString(s[i:j])
205 | b.WriteString(`\u00`)
206 | b.WriteByte(hex[c>>4])
207 | b.WriteByte(hex[c&0xF])
208 | j++
209 | i = j
210 | continue
211 | }
212 |
213 | r, size := utf8.DecodeRuneInString(s[j:])
214 |
215 | if r == utf8.RuneError && size == 1 {
216 | b.WriteString(s[i:j])
217 | b.WriteString(`\ufffd`)
218 | j += size
219 | i = j
220 | continue
221 | }
222 |
223 | switch r {
224 | case '\u2028', '\u2029':
225 | // U+2028 is LINE SEPARATOR.
226 | // U+2029 is PARAGRAPH SEPARATOR.
227 | // They are both technically valid characters in JSON strings,
228 | // but don't work in JSONP, which has to be evaluated as JavaScript,
229 | // and can lead to security holes there. It is valid JSON to
230 | // escape them, so we do so unconditionally.
231 | // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
232 | b.WriteString(s[i:j])
233 | b.WriteString(`\u202`)
234 | b.WriteByte(hex[r&0xF])
235 | j += size
236 | i = j
237 | continue
238 | }
239 |
240 | j += size
241 | }
242 |
243 | b.WriteString(s[i:])
244 | b.WriteByte('"')
245 | return b.String()
246 | }
247 |
--------------------------------------------------------------------------------
/internal/msg/element_test.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | )
7 |
8 | func jsonMarshal(s string) string {
9 | b, err := json.Marshal(s)
10 | if err != nil {
11 | panic(err)
12 | }
13 | return string(b)
14 | }
15 |
16 | func TestQuoteJSON(t *testing.T) {
17 | testcase := []string{
18 | "\u0005", // issue 1773
19 | "\v",
20 | }
21 |
22 | for _, input := range testcase {
23 | got := QuoteJSON(input)
24 | expected := jsonMarshal(input)
25 | if got != expected {
26 | t.Errorf("want %v but got %v", expected, got)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/internal/msg/local.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/ProtocolScience/AstralGo/message"
7 | )
8 |
9 | // Poke 拍一拍
10 | type Poke struct {
11 | Target int64
12 | }
13 |
14 | // Type 获取元素类型ID
15 | func (e *Poke) Type() message.ElementType {
16 | // Make message.IMessageElement Happy
17 | return message.At
18 | }
19 |
20 | // LocalImage 本地图片
21 | type LocalImage struct {
22 | Stream io.ReadSeeker
23 | File string
24 | URL string
25 |
26 | Flash bool
27 | EffectID int32
28 | }
29 |
30 | // Type implements the message.IMessageElement.
31 | func (e *LocalImage) Type() message.ElementType {
32 | return message.Image
33 | }
34 |
35 | // LocalVideo 本地视频
36 | type LocalVideo struct {
37 | File string
38 | Thumb io.ReadSeeker
39 | }
40 |
41 | // Type impl message.IMessageElement
42 | func (e *LocalVideo) Type() message.ElementType {
43 | return message.Video
44 | }
45 |
46 | // LocalVoice 本地语音
47 | type LocalVoice struct {
48 | Name string
49 | Md5 []byte
50 | Size int32
51 | URL string
52 | Data []byte
53 | During int32
54 | }
55 |
56 | // Type implements the message.IMessageElement.
57 | func (e *LocalVoice) Type() message.ElementType {
58 | return message.Voice
59 | }
60 |
--------------------------------------------------------------------------------
/internal/msg/parse.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "github.com/tidwall/gjson"
5 | )
6 |
7 | // ParseObject 将消息JSON对象转为消息元素数组
8 | func ParseObject(m gjson.Result) (r []Element) {
9 | convert := func(e gjson.Result) {
10 | var elem Element
11 | elem.Type = e.Get("type").Str
12 | e.Get("data").ForEach(func(key, value gjson.Result) bool {
13 | pair := Pair{K: key.Str, V: value.String()}
14 | elem.Data = append(elem.Data, pair)
15 | return true
16 | })
17 | r = append(r, elem)
18 | }
19 |
20 | if m.IsArray() {
21 | m.ForEach(func(_, e gjson.Result) bool {
22 | convert(e)
23 | return true
24 | })
25 | }
26 | if m.IsObject() {
27 | convert(m)
28 | }
29 | return
30 | }
31 |
32 | func text(txt string) Element {
33 | return Element{
34 | Type: "text",
35 | Data: []Pair{
36 | {
37 | K: "text",
38 | V: txt,
39 | },
40 | },
41 | }
42 | }
43 |
44 | // ParseString 将字符串(CQ码)转为消息元素数组
45 | func ParseString(raw string) (r []Element) {
46 | var elem Element
47 | for raw != "" {
48 | i := 0
49 | for i < len(raw) && !(raw[i] == '[' && i+4 < len(raw) && raw[i:i+4] == "[CQ:") {
50 | i++
51 | }
52 | if i > 0 {
53 | r = append(r, text(UnescapeText(raw[:i])))
54 | }
55 |
56 | if i+4 > len(raw) {
57 | return
58 | }
59 | raw = raw[i+4:] // skip "[CQ:"
60 | i = 0
61 | for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
62 | i++
63 | }
64 | if i+1 > len(raw) {
65 | return
66 | }
67 | elem.Type = raw[:i]
68 | elem.Data = nil // reset data
69 | raw = raw[i:]
70 | i = 0
71 | for {
72 | if raw[0] == ']' {
73 | r = append(r, elem)
74 | raw = raw[1:]
75 | break
76 | }
77 | raw = raw[1:]
78 |
79 | for i < len(raw) && raw[i] != '=' {
80 | i++
81 | }
82 | if i+1 > len(raw) {
83 | return
84 | }
85 | key := raw[:i]
86 | raw = raw[i+1:] // skip "="
87 | i = 0
88 | for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
89 | i++
90 | }
91 |
92 | if i+1 > len(raw) {
93 | return
94 | }
95 | elem.Data = append(elem.Data, Pair{
96 | K: key,
97 | V: UnescapeValue(raw[:i]),
98 | })
99 | raw = raw[i:]
100 | i = 0
101 | }
102 | }
103 | return
104 | }
105 |
--------------------------------------------------------------------------------
/internal/msg/parse_test.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/ProtocolScience/AstralGo/utils"
9 | "github.com/stretchr/testify/assert"
10 | "github.com/tidwall/gjson"
11 | )
12 |
13 | func TestParseString(_ *testing.T) {
14 | // TODO: add more text
15 | for _, v := range ParseString(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`) {
16 | fmt.Println(v)
17 | }
18 | }
19 |
20 | var (
21 | bench = `asdfqwerqwerqwer[CQ:face,id=115,text=111]asdfasdfasdfasdfasdfasdfasd[CQ:face,id=217]] 123 [`
22 | 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":" ["}}]`)
23 | )
24 |
25 | func BenchmarkParseString(b *testing.B) {
26 | for i := 0; i < b.N; i++ {
27 | ParseString(bench)
28 | }
29 | b.SetBytes(int64(len(bench)))
30 | }
31 |
32 | func BenchmarkParseObject(b *testing.B) {
33 | for i := 0; i < b.N; i++ {
34 | ParseObject(benchArray)
35 | }
36 | b.SetBytes(int64(len(benchArray.Raw)))
37 | }
38 |
39 | const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&987654321[]&`
40 |
41 | func BenchmarkCQCodeEscapeText(b *testing.B) {
42 | for i := 0; i < b.N; i++ {
43 | ret := bText
44 | EscapeText(ret)
45 | }
46 | }
47 |
48 | func TestCQCodeEscapeText(t *testing.T) {
49 | for i := 0; i < 200; i++ {
50 | rs := utils.RandomStringRange(3000, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890[]&")
51 | ret := rs
52 | ret = strings.ReplaceAll(ret, "&", "&")
53 | ret = strings.ReplaceAll(ret, "[", "[")
54 | ret = strings.ReplaceAll(ret, "]", "]")
55 | assert.Equal(t, ret, EscapeText(rs))
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/internal/param/param.go:
--------------------------------------------------------------------------------
1 | // Package param provide some util for param parse
2 | package param
3 |
4 | import (
5 | "math"
6 | "regexp"
7 | "strings"
8 | "sync"
9 |
10 | "github.com/tidwall/gjson"
11 | )
12 |
13 | // EnsureBool 判断给定的p是否可表示为合法Bool类型,否则返回defaultVal
14 | //
15 | // 支持的合法类型有
16 | //
17 | // type bool
18 | //
19 | // type gjson.True or gjson.False
20 | //
21 | // type string "true","yes","1" or "false","no","0" (case insensitive)
22 | func EnsureBool(p any, defaultVal bool) bool {
23 | var str string
24 | if b, ok := p.(bool); ok {
25 | return b
26 | }
27 | if j, ok := p.(gjson.Result); ok {
28 | if !j.Exists() {
29 | return defaultVal
30 | }
31 | switch j.Type { // nolint: exhaustive
32 | case gjson.True:
33 | return true
34 | case gjson.False:
35 | return false
36 | case gjson.String:
37 | str = j.Str
38 | default:
39 | return defaultVal
40 | }
41 | } else if s, ok := p.(string); ok {
42 | str = s
43 | }
44 | str = strings.ToLower(str)
45 | switch str {
46 | case "true", "yes", "1":
47 | return true
48 | case "false", "no", "0":
49 | return false
50 | default:
51 | return defaultVal
52 | }
53 | }
54 |
55 | var (
56 | // once lazy compile the reg
57 | once sync.Once
58 | // reg is splitURL regex pattern.
59 | reg *regexp.Regexp
60 | )
61 |
62 | // SplitURL 将给定URL字符串分割为两部分,用于URL预处理防止风控
63 | func SplitURL(s string) []string {
64 | once.Do(func() { // lazy init.
65 | reg = regexp.MustCompile(`(?i)[a-z\d][-a-z\d]{0,62}(\.[a-z\d][-a-z\d]{0,62})+\.?`)
66 | })
67 | idx := reg.FindAllStringIndex(s, -1)
68 | if len(idx) == 0 {
69 | return []string{s}
70 | }
71 | var result []string
72 | last := 0
73 | for i := 0; i < len(idx); i++ {
74 | if len(idx[i]) != 2 {
75 | continue
76 | }
77 | m := int(math.Abs(float64(idx[i][0]-idx[i][1]))/1.5) + idx[i][0]
78 | result = append(result, s[last:m])
79 | last = m
80 | }
81 | result = append(result, s[last:])
82 | return result
83 | }
84 |
--------------------------------------------------------------------------------
/internal/selfdiagnosis/diagnoses.go:
--------------------------------------------------------------------------------
1 | // Package selfdiagnosis 自我诊断相关
2 | package selfdiagnosis
3 |
4 | import (
5 | "github.com/ProtocolScience/AstralGo/client"
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | // NetworkDiagnosis 诊断网络状态并输出结果
10 | func NetworkDiagnosis(c *client.QQClient) {
11 | log.Infof("开始诊断网络情况")
12 | qualityInfo := c.ConnectionQualityTest()
13 | log.Debugf("聊天服务器连接延迟: %vms", qualityInfo.ChatServerLatency)
14 | log.Debugf("聊天服务器丢包率: %v%%", qualityInfo.ChatServerPacketLoss*10)
15 | log.Debugf("长消息服务器连接延迟: %vms", qualityInfo.LongMessageServerLatency)
16 | log.Debugf("长消息服务器响应延迟: %vms", qualityInfo.LongMessageServerResponseLatency)
17 | log.Debugf("媒体服务器连接延迟: %vms", qualityInfo.SrvServerLatency)
18 | log.Debugf("媒体服务器丢包率: %v%%", qualityInfo.SrvServerPacketLoss*10)
19 |
20 | const (
21 | chatServerErrorMessage = "可能出现消息丢失/延迟或频繁掉线等情况, 请检查本地网络状态."
22 | longMessageServerErrorMessage = "可能导致无法接收/发送长消息的情况, 请检查本地网络状态."
23 | mediaServerErrorMessage = "可能导致无法上传/下载媒体文件, 无法上传群共享, 无法发送消息等情况, 请检查本地网络状态."
24 | )
25 |
26 | if qualityInfo.ChatServerLatency > 1000 {
27 | if qualityInfo.ChatServerLatency == 9999 {
28 | log.Errorf("错误: 聊天服务器延迟测试失败, %v", chatServerErrorMessage)
29 | } else {
30 | log.Warnf("警告: 聊天服务器延迟为 %vms,大于 1000ms, %v", qualityInfo.ChatServerLatency, chatServerErrorMessage)
31 | }
32 | }
33 |
34 | if qualityInfo.ChatServerPacketLoss > 0 {
35 | log.Warnf("警告: 本地连接聊天服务器丢包率为 %v%%, %v", qualityInfo.ChatServerPacketLoss*10, chatServerErrorMessage)
36 | }
37 |
38 | if qualityInfo.LongMessageServerLatency > 1000 {
39 | if qualityInfo.LongMessageServerLatency == 9999 {
40 | log.Errorf("错误: 长消息服务器延迟测试失败, %v 如果您使用的腾讯云服务器, 请修改DNS到114.114.114.114", longMessageServerErrorMessage)
41 | } else {
42 | log.Warnf("警告: 长消息延迟为 %vms, 大于 1000ms, %v", qualityInfo.LongMessageServerLatency, longMessageServerErrorMessage)
43 | }
44 | }
45 |
46 | if qualityInfo.LongMessageServerResponseLatency > 2000 {
47 | if qualityInfo.LongMessageServerResponseLatency == 9999 {
48 | log.Errorf("错误: 长消息服务器响应延迟测试失败, %v 如果您使用的腾讯云服务器, 请修改DNS到114.114.114.114", longMessageServerErrorMessage)
49 | } else {
50 | log.Warnf("警告: 长消息响应延迟为 %vms, 大于 1000ms, %v", qualityInfo.LongMessageServerResponseLatency, longMessageServerErrorMessage)
51 | }
52 | }
53 |
54 | if qualityInfo.SrvServerLatency > 1000 {
55 | if qualityInfo.SrvServerPacketLoss == 9999 {
56 | log.Errorf("错误: 媒体服务器延迟测试失败, %v", mediaServerErrorMessage)
57 | } else {
58 | log.Warnf("警告: 媒体服务器延迟为 %vms,大于 1000ms, %v", qualityInfo.SrvServerLatency, mediaServerErrorMessage)
59 | }
60 | }
61 |
62 | if qualityInfo.SrvServerPacketLoss > 0 {
63 | log.Warnf("警告: 本地连接媒体服务器丢包率为 %v%%, %v", qualityInfo.SrvServerPacketLoss*10, mediaServerErrorMessage)
64 | }
65 |
66 | if qualityInfo.ChatServerLatency > 1000 || qualityInfo.ChatServerPacketLoss > 0 || qualityInfo.LongMessageServerLatency > 1000 || qualityInfo.SrvServerLatency > 1000 || qualityInfo.SrvServerPacketLoss > 0 {
67 | log.Infof("网络诊断完成. 发现问题, 请检查日志.")
68 | } else {
69 | log.Infof("网络诊断完成. 未发现问题")
70 | }
71 | }
72 |
73 | // DNSDiagnosis 诊断DNS状态并输出结果
74 | func DNSDiagnosis() {
75 | // todo
76 | }
77 |
78 | // EnvironmentDiagnosis 诊断本地环境状态并输出结果
79 | func EnvironmentDiagnosis() {
80 | // todo
81 | }
82 |
--------------------------------------------------------------------------------
/internal/selfupdate/update.go:
--------------------------------------------------------------------------------
1 | // Package selfupdate 版本升级检查和自更新
2 | package selfupdate
3 |
4 | import (
5 | "bufio"
6 | "bytes"
7 | "encoding/hex"
8 | "fmt"
9 | "hash"
10 | "io"
11 | "math"
12 | "os"
13 | "path/filepath"
14 | "runtime"
15 | "strings"
16 |
17 | "github.com/sirupsen/logrus"
18 |
19 | "github.com/ProtocolScience/AstralGocq/global"
20 | "github.com/ProtocolScience/AstralGocq/internal/base"
21 | "github.com/ProtocolScience/AstralGocq/internal/download"
22 | )
23 |
24 | func readLine() (str string) {
25 | console := bufio.NewReader(os.Stdin)
26 | str, _ = console.ReadString('\n')
27 | str = strings.TrimSpace(str)
28 | return
29 | }
30 |
31 | func lastVersion() (string, error) {
32 | r, err := download.Request{URL: "https://api.github.com/repos/Mrs4s/go-cqhttp/releases/latest"}.JSON()
33 | if err != nil {
34 | return "", err
35 | }
36 | return r.Get("tag_name").Str, nil
37 | }
38 |
39 | // CheckUpdate 检查更新
40 | func CheckUpdate() {
41 | logrus.Infof("正在检查更新.")
42 | if base.Version == "(devel)" {
43 | logrus.Warnf("检查更新失败: 使用的 Actions 测试版或自编译版本.")
44 | return
45 | }
46 | latest, err := lastVersion()
47 | if err != nil {
48 | logrus.Warnf("检查更新失败: %v", err)
49 | return
50 | }
51 | if global.VersionNameCompare(base.Version, latest) {
52 | logrus.Infof("当前有更新的 go-cqhttp 可供更新, 请前往 https://github.com/ProtocolScience/AstralGocq/releases 下载.")
53 | logrus.Infof("当前版本: %v 最新版本: %v", base.Version, latest)
54 | return
55 | }
56 | logrus.Infof("检查更新完成. 当前已运行最新版本.")
57 | }
58 |
59 | func binaryName() string {
60 | goarch := runtime.GOARCH
61 | if goarch == "arm" {
62 | goarch += "v7"
63 | }
64 | ext := "tar.gz"
65 | if runtime.GOOS == "windows" {
66 | ext = "zip"
67 | }
68 | return fmt.Sprintf("go-cqhttp_%v_%v.%v", runtime.GOOS, goarch, ext)
69 | }
70 |
71 | func checksum(github, version string) []byte {
72 | sumURL := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp_checksums.txt", github, version)
73 | sum, err := download.Request{URL: sumURL}.Bytes()
74 | if err != nil {
75 | return nil
76 | }
77 |
78 | rd := bufio.NewReader(bytes.NewReader(sum))
79 | for {
80 | str, err := rd.ReadString('\n')
81 | if err != nil {
82 | break
83 | }
84 | str = strings.TrimSpace(str)
85 | if strings.HasSuffix(str, binaryName()) {
86 | sum, _ := hex.DecodeString(strings.TrimSuffix(str, " "+binaryName()))
87 | return sum
88 | }
89 | }
90 | return nil
91 | }
92 |
93 | func wait() {
94 | logrus.Info("按 Enter 继续....")
95 | readLine()
96 | os.Exit(0)
97 | }
98 |
99 | // SelfUpdate 自更新
100 | func SelfUpdate(github string) {
101 | if github == "" {
102 | github = "https://github.com"
103 | }
104 |
105 | logrus.Infof("正在检查更新.")
106 | latest, err := lastVersion()
107 | if err != nil {
108 | logrus.Warnf("获取最新版本失败: %v", err)
109 | wait()
110 | }
111 | url := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/%v", github, latest, binaryName())
112 | if base.Version == latest {
113 | logrus.Info("当前版本已经是最新版本!")
114 | wait()
115 | }
116 | logrus.Info("当前最新版本为 ", latest)
117 | logrus.Warn("是否更新(y/N): ")
118 | r := strings.TrimSpace(readLine())
119 | if r != "y" && r != "Y" {
120 | logrus.Warn("已取消更新!")
121 | wait()
122 | }
123 | logrus.Info("正在更新,请稍等...")
124 | sum := checksum(github, latest)
125 | if sum != nil {
126 | err = update(url, sum)
127 | if err != nil {
128 | logrus.Error("更新失败: ", err)
129 | } else {
130 | logrus.Info("更新成功!")
131 | }
132 | } else {
133 | logrus.Error("checksum 失败!")
134 | }
135 | wait()
136 | }
137 |
138 | // writeSumCounter 写入量计算实例
139 | type writeSumCounter struct {
140 | total uint64
141 | hash hash.Hash
142 | }
143 |
144 | // Write 方法将写入的byte长度追加至写入的总长度Total中
145 | func (wc *writeSumCounter) Write(p []byte) (int, error) {
146 | n := len(p)
147 | wc.total += uint64(n)
148 | wc.hash.Write(p)
149 | fmt.Printf("\r ")
150 | fmt.Printf("\rDownloading... %s complete", humanBytes(wc.total))
151 | return n, nil
152 | }
153 |
154 | func logn(n, b float64) float64 {
155 | return math.Log(n) / math.Log(b)
156 | }
157 |
158 | func humanBytes(s uint64) string {
159 | sizes := []string{"B", "kB", "MB", "GB"} // GB对于go-cqhttp来说已经够用了
160 | if s < 10 {
161 | return fmt.Sprintf("%d B", s)
162 | }
163 | e := math.Floor(logn(float64(s), 1000))
164 | suffix := sizes[int(e)]
165 | val := math.Floor(float64(s)/math.Pow(1000, e)*10+0.5) / 10
166 | f := "%.0f %s"
167 | if val < 10 {
168 | f = "%.1f %s"
169 | }
170 | return fmt.Sprintf(f, val, suffix)
171 | }
172 |
173 | // FromStream copy form getlantern/go-update
174 | func fromStream(updateWith io.Reader) (err error, errRecover error) {
175 | updatePath, err := os.Executable()
176 | updatePath = filepath.Clean(updatePath)
177 | if err != nil {
178 | return
179 | }
180 |
181 | // get the directory the executable exists in
182 | updateDir := filepath.Dir(updatePath)
183 | filename := filepath.Base(updatePath)
184 | // Copy the contents of of newbinary to a the new executable file
185 | newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
186 | fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
187 | if err != nil {
188 | return
189 | }
190 | // We won't log this error, because it's always going to happen.
191 | defer func() { _ = fp.Close() }()
192 | if _, err = bufio.NewReader(updateWith).WriteTo(fp); err != nil {
193 | logrus.Errorf("Unable to copy data: %v\n", err)
194 | }
195 |
196 | // if we don't call fp.Close(), windows won't let us move the new executable
197 | // because the file will still be "in use"
198 | if err := fp.Close(); err != nil {
199 | logrus.Errorf("Unable to close file: %v\n", err)
200 | }
201 | // this is where we'll move the executable to so that we can swap in the updated replacement
202 | oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
203 |
204 | // delete any existing old exec file - this is necessary on Windows for two reasons:
205 | // 1. after a successful update, Windows can't remove the .old file because the process is still running
206 | // 2. windows rename operations fail if the destination file already exists
207 | _ = os.Remove(oldPath)
208 |
209 | // move the existing executable to a new file in the same directory
210 | err = os.Rename(updatePath, oldPath)
211 | if err != nil {
212 | return
213 | }
214 |
215 | // move the new executable in to become the new program
216 | err = os.Rename(newPath, updatePath)
217 |
218 | if err != nil {
219 | // copy unsuccessful
220 | errRecover = os.Rename(oldPath, updatePath)
221 | } else {
222 | // copy successful, remove the old binary
223 | _ = os.Remove(oldPath)
224 | }
225 | return
226 | }
227 |
--------------------------------------------------------------------------------
/internal/selfupdate/update_others.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package selfupdate
4 |
5 | import (
6 | "archive/tar"
7 | "bytes"
8 | "compress/gzip"
9 | "crypto/sha256"
10 | "errors"
11 | "io"
12 | "net/http"
13 | )
14 |
15 | // update go-cqhttp自我更新
16 | func update(url string, sum []byte) error {
17 | resp, err := http.Get(url)
18 | if err != nil {
19 | return err
20 | }
21 | defer resp.Body.Close()
22 | wc := writeSumCounter{
23 | hash: sha256.New(),
24 | }
25 | rsp, err := io.ReadAll(io.TeeReader(resp.Body, &wc))
26 | if err != nil {
27 | return err
28 | }
29 | if !bytes.Equal(wc.hash.Sum(nil), sum) {
30 | return errors.New("文件已损坏")
31 | }
32 | gr, err := gzip.NewReader(bytes.NewReader(rsp))
33 | if err != nil {
34 | return err
35 | }
36 | tr := tar.NewReader(gr)
37 | for {
38 | header, err := tr.Next()
39 | if err != nil {
40 | return err
41 | }
42 | if header.Name == "go-cqhttp" {
43 | err, _ := fromStream(tr)
44 | if err != nil {
45 | return err
46 | }
47 | return nil
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/selfupdate/update_windows.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "crypto/sha256"
7 | "errors"
8 | "io"
9 | "net/http"
10 | )
11 |
12 | // update go-cqhttp自我更新
13 | func update(url string, sum []byte) error {
14 | resp, err := http.Get(url)
15 | if err != nil {
16 | return err
17 | }
18 | defer resp.Body.Close()
19 | wc := writeSumCounter{
20 | hash: sha256.New(),
21 | }
22 | rsp, err := io.ReadAll(io.TeeReader(resp.Body, &wc))
23 | if err != nil {
24 | return err
25 | }
26 | if !bytes.Equal(wc.hash.Sum(nil), sum) {
27 | return errors.New("文件已损坏")
28 | }
29 | reader, _ := zip.NewReader(bytes.NewReader(rsp), resp.ContentLength)
30 | file, err := reader.Open("go-cqhttp.exe")
31 | if err != nil {
32 | return err
33 | }
34 | err, _ = fromStream(file)
35 | if err != nil {
36 | return err
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Package main
2 | package main
3 |
4 | import (
5 | "github.com/ProtocolScience/AstralGocq/cmd/gocq"
6 | "github.com/ProtocolScience/AstralGocq/global/terminal"
7 |
8 | _ "github.com/ProtocolScience/AstralGocq/db/leveldb" // leveldb 数据库支持
9 | _ "github.com/ProtocolScience/AstralGocq/modules/silk" // silk编码模块
10 | // 其他模块
11 | // _ "github.com/ProtocolScience/AstralGocq/db/sqlite3" // sqlite3 数据库支持
12 | // _ "github.com/ProtocolScience/AstralGocq/db/mongodb" // mongodb 数据库支持
13 | // _ "github.com/ProtocolScience/AstralGocq/modules/pprof" // pprof 性能分析
14 | )
15 |
16 | func main() {
17 | terminal.SetTitle()
18 | gocq.InitBase()
19 | gocq.PrepareData()
20 | gocq.LoginInteract()
21 | _ = terminal.DisableQuickEdit()
22 | _ = terminal.EnableVT100()
23 | gocq.WaitSignal()
24 | _ = terminal.RestoreInputMode()
25 | }
26 |
--------------------------------------------------------------------------------
/modules/api/caller.go:
--------------------------------------------------------------------------------
1 | // Package api implements the API route for servers.
2 | package api
3 |
4 | import (
5 | "github.com/tidwall/gjson"
6 |
7 | "github.com/ProtocolScience/AstralGocq/coolq"
8 | "github.com/ProtocolScience/AstralGocq/global"
9 | "github.com/ProtocolScience/AstralGocq/pkg/onebot"
10 | )
11 |
12 | //go:generate go run ./../../cmd/api-generator -pkg api -path=./../../coolq/api.go,./../../coolq/api_v12.go -o api.go
13 |
14 | // Getter 参数获取
15 | type Getter interface {
16 | Get(string) gjson.Result
17 | }
18 |
19 | // Handler 中间件
20 | type Handler func(action string, spe *onebot.Spec, p Getter) global.MSG
21 |
22 | // Caller api route caller
23 | type Caller struct {
24 | bot *coolq.CQBot
25 | handlers []Handler
26 | }
27 |
28 | // Call specific API
29 | func (c *Caller) Call(action string, spec *onebot.Spec, p Getter) global.MSG {
30 | for _, fn := range c.handlers {
31 | if ret := fn(action, spec, p); ret != nil {
32 | return ret
33 | }
34 | }
35 | return c.call(action, spec, p)
36 | }
37 |
38 | // Use add handlers to the API caller
39 | func (c *Caller) Use(middlewares ...Handler) {
40 | c.handlers = append(c.handlers, middlewares...)
41 | }
42 |
43 | // NewCaller create a new API caller
44 | func NewCaller(bot *coolq.CQBot) *Caller {
45 | return &Caller{
46 | bot: bot,
47 | handlers: make([]Handler, 0),
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/modules/config/config.go:
--------------------------------------------------------------------------------
1 | // Package config 包含go-cqhttp操作配置文件的相关函数
2 | package config
3 |
4 | import (
5 | "bufio"
6 | _ "embed" // embed the default config file
7 | "fmt"
8 | "os"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 |
13 | log "github.com/sirupsen/logrus"
14 | "gopkg.in/yaml.v3"
15 | )
16 |
17 | // defaultConfig 默认配置文件
18 | //
19 | //go:embed default_config.yml
20 | var defaultConfig string
21 |
22 | // Reconnect 重连配置
23 | type Reconnect struct {
24 | Disabled bool `yaml:"disabled"`
25 | Delay uint `yaml:"delay"`
26 | MaxTimes uint `yaml:"max-times"`
27 | Interval int `yaml:"interval"`
28 | }
29 |
30 | // Account 账号配置
31 | type Account struct {
32 | Uin int64 `yaml:"uin"`
33 | Password string `yaml:"password"`
34 | Encrypt bool `yaml:"encrypt"`
35 | Status int `yaml:"status"`
36 | ReLogin *Reconnect `yaml:"relogin"`
37 | UseSSOAddress bool `yaml:"use-sso-address"`
38 | AllowTempSession bool `yaml:"allow-temp-session"`
39 | SignServers []SignServer `yaml:"sign-servers"`
40 | RuleChangeSignServer int `yaml:"rule-change-sign-server"`
41 | MaxCheckCount uint `yaml:"max-check-count"`
42 | SignServerTimeout uint `yaml:"sign-server-timeout"`
43 | RefreshInterval int64 `yaml:"refresh-interval"`
44 | }
45 |
46 | // SignServer 签名服务器
47 | type SignServer struct {
48 | URL string `yaml:"url"`
49 | Key string `yaml:"key"`
50 | Authorization string `yaml:"authorization"`
51 | }
52 |
53 | // Config 总配置文件
54 | type Config struct {
55 | Account *Account `yaml:"account"`
56 | Heartbeat struct {
57 | Disabled bool `yaml:"disabled"`
58 | Interval int `yaml:"interval"`
59 | } `yaml:"heartbeat"`
60 |
61 | Message struct {
62 | PostFormat string `yaml:"post-format"`
63 | ProxyRewrite string `yaml:"proxy-rewrite"`
64 | IgnoreInvalidCQCode bool `yaml:"ignore-invalid-cqcode"`
65 | ForceFragment bool `yaml:"force-fragment"`
66 | FixURL bool `yaml:"fix-url"`
67 | ReportSelfMessage bool `yaml:"report-self-message"`
68 | RemoveReplyAt bool `yaml:"remove-reply-at"`
69 | ExtraReplyData bool `yaml:"extra-reply-data"`
70 | SkipMimeScan bool `yaml:"skip-mime-scan"`
71 | ConvertWebpImage bool `yaml:"convert-webp-image"`
72 | HTTPTimeout int `yaml:"http-timeout"`
73 | } `yaml:"message"`
74 |
75 | Output struct {
76 | LogLevel string `yaml:"log-level"`
77 | LogAging int `yaml:"log-aging"`
78 | LogForceNew bool `yaml:"log-force-new"`
79 | LogColorful *bool `yaml:"log-colorful"`
80 | Debug bool `yaml:"debug"`
81 | } `yaml:"output"`
82 |
83 | Servers []map[string]yaml.Node `yaml:"servers"`
84 | Database map[string]yaml.Node `yaml:"database"`
85 | }
86 |
87 | // Server 的简介和初始配置
88 | type Server struct {
89 | Brief string
90 | Default string
91 | }
92 |
93 | // Parse 从默认配置文件路径中获取
94 | func Parse(path string) *Config {
95 | _, err := os.Stat(path)
96 | if err != nil {
97 | generateConfig()
98 | fmt.Println("配置文件已生成,按Enter继续,或者Ctrl+C退出程序来手动修改配置文件")
99 | _, _ = bufio.NewReader(os.Stdin).ReadString('\n')
100 | }
101 | file, err := os.ReadFile(path)
102 | config := &Config{}
103 | if err == nil {
104 | err = yaml.NewDecoder(strings.NewReader(expand(string(file), os.Getenv))).Decode(config)
105 | if err == nil {
106 | return config
107 | }
108 | }
109 | fmt.Println("配置文件不合法!", err)
110 | os.Exit(1)
111 | return nil
112 | }
113 |
114 | var serverconfs []*Server
115 |
116 | // AddServer 添加该服务的简介和默认配置
117 | func AddServer(s *Server) {
118 | serverconfs = append(serverconfs, s)
119 | }
120 |
121 | // generateConfig 生成配置文件
122 | func generateConfig() {
123 | fmt.Println("未找到配置文件,正在为您生成配置文件中!")
124 | sb := strings.Builder{}
125 | sb.WriteString(defaultConfig)
126 | hint := "请选择你需要的通信方式:"
127 | for i, s := range serverconfs {
128 | hint += fmt.Sprintf("\n> %d: %s", i, s.Brief)
129 | }
130 | hint += `
131 | 请输入你需要的编号(0-9),可输入多个,同一编号也可输入多个(如: 233)
132 | 您的选择是:`
133 | fmt.Print(hint)
134 | input := bufio.NewReader(os.Stdin)
135 | readString, err := input.ReadString('\n')
136 | if err != nil {
137 | log.Fatal("输入不合法: ", err)
138 | }
139 | rmax := len(serverconfs)
140 | if rmax > 10 {
141 | rmax = 10
142 | }
143 | for _, r := range readString {
144 | r -= '0'
145 | if r >= 0 && r < rune(rmax) {
146 | sb.WriteString(serverconfs[r].Default)
147 | }
148 | }
149 |
150 | // Parse the YAML configuration
151 | var config map[string]interface{}
152 | err = yaml.Unmarshal([]byte(sb.String()), &config)
153 | if err != nil {
154 | log.Fatal("无法解析配置: ", err)
155 | }
156 | // Access nested map for account
157 | if account, ok := config["account"].(map[string]interface{}); ok {
158 | // Capture QQ account information
159 | fmt.Print("请输入您的QQ账号: ")
160 | uinStr, err := input.ReadString('\n')
161 | if err != nil {
162 | log.Fatal("输入不合法: ", err)
163 | }
164 | uinStr = strings.TrimSpace(uinStr)
165 | uin, err := strconv.ParseInt(uinStr, 10, 64)
166 | if err != nil {
167 | log.Fatal("QQ账号必须是数字: ", err)
168 | }
169 | account["uin"] = uin
170 |
171 | fmt.Print("请输入您的密码(可空): ")
172 | password, err := input.ReadString('\n')
173 | if err != nil {
174 | log.Fatal("输入不合法: ", err)
175 | }
176 |
177 | account["password"] = strings.TrimSpace(password)
178 |
179 | // Serialize the updated configuration back to YAML
180 | updatedConfig, err := yaml.Marshal(&config)
181 | if err != nil {
182 | log.Fatal("无法序列化配置: ", err)
183 | }
184 |
185 | // Write the updated configuration to a file
186 | err = os.WriteFile("config.yml", updatedConfig, 0o644)
187 | if err != nil {
188 | log.Fatal("无法写入配置文件: ", err)
189 | }
190 | } else {
191 | log.Fatal("无法解析配置: ", err)
192 | }
193 | }
194 |
195 | // expand 使用正则进行环境变量展开
196 | // os.ExpandEnv 字符 $ 无法逃逸
197 | // https://github.com/golang/go/issues/43482
198 | func expand(s string, mapping func(string) string) string {
199 | r := regexp.MustCompile(`\${([a-zA-Z_]+[a-zA-Z0-9_:/.]*)}`)
200 | return r.ReplaceAllStringFunc(s, func(s string) string {
201 | s = strings.Trim(s, "${}")
202 | before, after, ok := strings.Cut(s, ":")
203 | m := mapping(before)
204 | if ok && m == "" {
205 | return after
206 | }
207 | return m
208 | })
209 | }
210 |
--------------------------------------------------------------------------------
/modules/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | func Test_expand(t *testing.T) {
9 | nullStringMapping := func(_ string) string {
10 | return ""
11 | }
12 | tests := []struct {
13 | src string
14 | mapping func(string) string
15 | expected string
16 | }{
17 | {
18 | src: "foo: ${bar}",
19 | mapping: strings.ToUpper,
20 | expected: "foo: BAR",
21 | },
22 | {
23 | src: "$123",
24 | mapping: strings.ToUpper,
25 | expected: "$123",
26 | },
27 | {
28 | src: "foo: ${bar:123456}",
29 | mapping: nullStringMapping,
30 | expected: "foo: 123456",
31 | },
32 | {
33 | src: "foo: ${bar:127.0.0.1:5700}",
34 | mapping: nullStringMapping,
35 | expected: "foo: 127.0.0.1:5700",
36 | },
37 | {
38 | src: "foo: ${bar:ws//localhost:9999/ws}",
39 | mapping: nullStringMapping,
40 | expected: "foo: ws//localhost:9999/ws",
41 | },
42 | }
43 | for i, tt := range tests {
44 | if got := expand(tt.src, tt.mapping); got != tt.expected {
45 | t.Errorf("testcase %d failed, expected %v but got %v", i, tt.expected, got)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/modules/config/default_config.yml:
--------------------------------------------------------------------------------
1 | # go-cqhttp 默认配置文件
2 |
3 | account: # 账号相关
4 | uin: 1233456 # QQ账号
5 | password: '' # 密码为空时使用扫码登录
6 | encrypt: false # 是否开启密码加密
7 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
8 | relogin: # 重连设置
9 | delay: 3 # 首次重连延迟, 单位秒
10 | interval: 3 # 重连间隔
11 | max-times: 0 # 最大重连次数, 0为无限制
12 |
13 | # 是否使用服务器下发的新地址进行重连
14 | # 注意, 此设置可能导致在海外服务器上连接情况更差
15 | use-sso-address: true
16 | # 是否允许发送临时会话消息
17 | allow-temp-session: false
18 |
19 | # 数据包的签名服务器列表,第一个作为主签名服务器,后续作为备用
20 | # 兼容 https://github.com/fuqiuluo/unidbg-fetch-qsign
21 | # 如果遇到 登录 45 错误, 或者发送信息风控的话需要填入一个或多个服务器
22 | # 不建议设置过多,设置主备各一个即可,超过 5 个只会取前五个
23 | # 示例:
24 | # sign-servers:
25 | # - url: 'http://127.0.0.1:8080' # 本地签名服务器
26 | # key: "114514" # 相应 key
27 | # authorization: "-" # authorization 内容, 依服务端设置
28 | # - url: 'https://signserver.example.com' # 线上签名服务器
29 | # key: "114514"
30 | # authorization: "-"
31 | # ...
32 | #
33 | # 服务器可使用docker在本地搭建或者使用他人开放的服务
34 | sign-servers:
35 | #- url: 'ws://183.131.51.152:7860/ws' # 备用
36 | # key: 'selfshare'
37 | # authorization: '-'
38 | - url: 'wss://qsign.trpgbot.com/ws' # 备用
39 | key: 'selfshare'
40 | authorization: '-'
41 | - url: 'https://qsign.trpgbot.com' # 主签名服务器地址, 必填
42 | key: 'selfshare' # 签名服务器所需要的apikey, 如果签名服务器的版本在1.1.0及以下则此项无效
43 | authorization: '-' # authorization 内容, 依服务端设置,如 'Bearer xxxx'
44 | - url: 'https://qsign-v3.trpgbot.com' # 备用
45 | key: 'selfshare'
46 | authorization: '-'
47 | - url: 'wss://qsign-v3.trpgbot.com/ws' # 备用
48 | key: 'selfshare'
49 | authorization: '-'
50 | - url: 'https://zyr15r-astralqsign.hf.space' # 备用
51 | key: 'selfshare'
52 | authorization: '-'
53 | - url: 'wss://qsign.chahuyun.cn/ws' # 备用
54 | key: 'selfshare'
55 | authorization: '-'
56 | - url: 'https://qsign.chahuyun.cn' # 备用
57 | key: 'selfshare'
58 | authorization: '-'
59 | # 判断签名服务不可用(需要切换)的额外规则
60 | # 0: 不设置 (此时仅在请求无法返回结果时判定为不可用)
61 | # 1: 在获取到的 sign 为空 (若选此建议关闭 auto-register,一般为实例未注册但是请求签名的情况)
62 | # 2: 在获取到的 sign 或 token 为空(若选此建议关闭 auto-refresh-token )
63 | rule-change-sign-server: 1
64 |
65 | # 连续寻找可用签名服务器最大尝试次数
66 | # 为 0 时会在连续 3 次没有找到可用签名服务器后保持使用主签名服务器,不再尝试进行切换备用
67 | # 否则会在达到指定次数后 **退出** 主程序
68 | max-check-count: 0
69 | # 签名服务请求超时时间(s)
70 | sign-server-timeout: 15
71 | # 如果签名服务器的版本在1.1.0及以下, 请将下面的参数改成true
72 | # 建议使用 1.1.6 以上版本,低版本普遍半个月冻结一次
73 | is-below-110: false
74 | # 在实例可能丢失(获取到的签名为空)时是否尝试重新注册
75 | # 为 true 时,在签名服务不可用时可能每次发消息都会尝试重新注册并签名。
76 | # 为 false 时,将不会自动注册实例,在签名服务器重启或实例被销毁后需要重启 go-cqhttp 以获取实例
77 | # 否则后续消息将不会正常签名。关闭此项后可以考虑开启签名服务器端 auto_register 避免需要重启
78 | # 由于实现问题,当前建议关闭此项,推荐开启签名服务器的自动注册实例
79 | auto-register: false
80 | # 是否在 token 过期后立即自动刷新签名 token(在需要签名时才会检测到,主要防止 token 意外丢失)
81 | # 独立于定时刷新
82 | auto-refresh-token: false
83 | # 定时刷新 token 间隔时间,单位为分钟, 建议 30~40 分钟, 不可超过 60 分钟
84 | # 目前丢失token也不会有太大影响,可设置为 0 以关闭,推荐开启
85 | refresh-interval: 40
86 |
87 | heartbeat:
88 | # 心跳频率, 单位秒
89 | # -1 为关闭心跳
90 | interval: 5
91 |
92 | message:
93 | # 上报数据类型
94 | # 可选: string,array
95 | post-format: string
96 | # 是否忽略无效的CQ码, 如果为假将原样发送
97 | ignore-invalid-cqcode: false
98 | # 是否强制分片发送消息
99 | # 分片发送将会带来更快的速度
100 | # 但是兼容性会有些问题
101 | force-fragment: false
102 | # 是否将url分片发送
103 | fix-url: false
104 | # 下载图片等请求网络代理
105 | proxy-rewrite: ''
106 | # 是否上报自身消息
107 | report-self-message: false
108 | # 移除服务端的Reply附带的At
109 | remove-reply-at: false
110 | # 为Reply附加更多信息
111 | extra-reply-data: false
112 | # 跳过 Mime 扫描, 忽略错误数据
113 | skip-mime-scan: false
114 | # 是否自动转换 WebP 图片
115 | convert-webp-image: false
116 | # download 超时时间(s)
117 | http-timeout: 15
118 |
119 | output:
120 | # 日志等级 trace,debug,info,warn,error
121 | log-level: warn
122 | # 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留.
123 | log-aging: 15
124 | # 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写
125 | log-force-new: true
126 | # 是否启用日志颜色
127 | log-colorful: true
128 | # 是否启用 DEBUG
129 | debug: false # 开启调试模式
130 |
131 | # 默认中间件锚点
132 | default-middlewares: &default
133 | # 访问密钥, 强烈推荐在公网的服务器设置
134 | access-token: ''
135 | # 事件过滤器文件目录
136 | filter: ''
137 | # API限速设置
138 | # 该设置为全局生效
139 | # 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
140 | # 目前该限速设置为令牌桶算法, 请参考:
141 | # https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
142 | rate-limit:
143 | enabled: false # 是否启用限速
144 | frequency: 1 # 令牌回复频率, 单位秒
145 | bucket: 1 # 令牌桶大小
146 |
147 | database: # 数据库相关设置
148 | leveldb:
149 | # 是否启用内置leveldb数据库
150 | # 启用将会增加10-20MB的内存占用和一定的磁盘空间
151 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
152 | enable: true
153 | sqlite3:
154 | # 是否启用内置sqlite3数据库
155 | # 启用将会增加一定的内存占用和一定的磁盘空间
156 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
157 | enable: false
158 | cachettl: 3600000000000 # 1h
159 |
160 | # 连接服务列表
161 | servers:
162 | # 添加方式,同一连接方式可添加多个,具体配置说明请查看文档
163 | #- http: # http 通信
164 | #- ws: # 正向 Websocket
165 | #- ws-reverse: # 反向 Websocket
166 | #- pprof: #性能分析服务器
167 |
--------------------------------------------------------------------------------
/modules/filter/filter.go:
--------------------------------------------------------------------------------
1 | // Package filter implements an event filter for go-cqhttp
2 | package filter
3 |
4 | import (
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/tidwall/gjson"
9 | )
10 |
11 | // Filter 定义了一个消息上报过滤接口
12 | type Filter interface {
13 | Eval(payload gjson.Result) bool
14 | }
15 |
16 | type operationNode struct {
17 | key string
18 | filter Filter
19 | }
20 |
21 | // notOperator 定义了过滤器中Not操作符
22 | type notOperator struct {
23 | operand Filter
24 | }
25 |
26 | func newNotOp(argument gjson.Result) Filter {
27 | if !argument.IsObject() {
28 | panic("the argument of 'not' operator must be an object")
29 | }
30 | return ¬Operator{operand: Generate("and", argument)}
31 | }
32 |
33 | // Eval 对payload执行Not过滤
34 | func (op *notOperator) Eval(payload gjson.Result) bool {
35 | return !op.operand.Eval(payload)
36 | }
37 |
38 | // andOperator 定义了过滤器中And操作符
39 | type andOperator struct {
40 | operands []operationNode
41 | }
42 |
43 | func newAndOp(argument gjson.Result) Filter {
44 | if !argument.IsObject() {
45 | panic("the argument of 'and' operator must be an object")
46 | }
47 | op := new(andOperator)
48 | argument.ForEach(func(key, value gjson.Result) bool {
49 | switch {
50 | case key.Str[0] == '.':
51 | // is an operator
52 | // ".foo": {
53 | // "bar": "baz"
54 | // }
55 | opKey := key.Str[1:]
56 | op.operands = append(op.operands, operationNode{"", Generate(opKey, value)})
57 | case value.IsObject():
58 | // is a normal key with an object as the value
59 | // "foo": {
60 | // ".bar": "baz"
61 | // }
62 | opKey := key.String()
63 | op.operands = append(op.operands, operationNode{opKey, Generate("and", value)})
64 | default:
65 | // is a normal key with a non-object as the value
66 | // "foo": "bar"
67 | opKey := key.String()
68 | op.operands = append(op.operands, operationNode{opKey, Generate("eq", value)})
69 | }
70 | return true
71 | })
72 | return op
73 | }
74 |
75 | // Eval 对payload执行And过滤
76 | func (op *andOperator) Eval(payload gjson.Result) bool {
77 | res := true
78 | for _, operand := range op.operands {
79 | if len(operand.key) == 0 {
80 | // is an operator
81 | res = res && operand.filter.Eval(payload)
82 | } else {
83 | // is a normal key
84 | val := payload.Get(operand.key)
85 | res = res && operand.filter.Eval(val)
86 | }
87 |
88 | if !res {
89 | break
90 | }
91 | }
92 | return res
93 | }
94 |
95 | // orOperator 定义了过滤器中Or操作符
96 | type orOperator struct {
97 | operands []Filter
98 | }
99 |
100 | func newOrOp(argument gjson.Result) Filter {
101 | if !argument.IsArray() {
102 | panic("the argument of 'or' operator must be an array")
103 | }
104 | op := new(orOperator)
105 | argument.ForEach(func(_, value gjson.Result) bool {
106 | op.operands = append(op.operands, Generate("and", value))
107 | return true
108 | })
109 | return op
110 | }
111 |
112 | // Eval 对payload执行Or过滤
113 | func (op *orOperator) Eval(payload gjson.Result) bool {
114 | res := false
115 | for _, operand := range op.operands {
116 | res = res || operand.Eval(payload)
117 | if res {
118 | break
119 | }
120 | }
121 | return res
122 | }
123 |
124 | // eqOperator 定义了过滤器中Equal操作符
125 | type eqOperator struct {
126 | operand string
127 | }
128 |
129 | func newEqOp(argument gjson.Result) Filter {
130 | return &eqOperator{operand: argument.String()}
131 | }
132 |
133 | // Eval 对payload执行Equal过滤
134 | func (op *eqOperator) Eval(payload gjson.Result) bool {
135 | return payload.String() == op.operand
136 | }
137 |
138 | // neqOperator 定义了过滤器中NotEqual操作符
139 | type neqOperator struct {
140 | operand string
141 | }
142 |
143 | func newNeqOp(argument gjson.Result) Filter {
144 | return &neqOperator{operand: argument.String()}
145 | }
146 |
147 | // Eval 对payload执行NotEqual过滤
148 | func (op *neqOperator) Eval(payload gjson.Result) bool {
149 | return !(payload.String() == op.operand)
150 | }
151 |
152 | // inOperator 定义了过滤器中In操作符
153 | type inOperator struct {
154 | operandString string
155 | operandArray []string
156 | }
157 |
158 | func newInOp(argument gjson.Result) Filter {
159 | if argument.IsObject() {
160 | panic("the argument of 'in' operator must be an array or a string")
161 | }
162 | op := new(inOperator)
163 | if argument.IsArray() {
164 | op.operandArray = []string{}
165 | argument.ForEach(func(_, value gjson.Result) bool {
166 | op.operandArray = append(op.operandArray, value.String())
167 | return true
168 | })
169 | } else {
170 | op.operandString = argument.String()
171 | }
172 | return op
173 | }
174 |
175 | // Eval 对payload执行In过滤
176 | func (op *inOperator) Eval(payload gjson.Result) bool {
177 | payloadStr := payload.String()
178 | if op.operandArray != nil {
179 | for _, value := range op.operandArray {
180 | if value == payloadStr {
181 | return true
182 | }
183 | }
184 | return false
185 | }
186 | return strings.Contains(op.operandString, payloadStr)
187 | }
188 |
189 | // containsOperator 定义了过滤器中Contains操作符
190 | type containsOperator struct {
191 | operand string
192 | }
193 |
194 | func newContainOp(argument gjson.Result) Filter {
195 | if argument.IsArray() || argument.IsObject() {
196 | panic("the argument of 'contains' operator must be a string")
197 | }
198 | return &containsOperator{operand: argument.String()}
199 | }
200 |
201 | // Eval 对payload执行Contains过滤
202 | func (op *containsOperator) Eval(payload gjson.Result) bool {
203 | return strings.Contains(payload.String(), op.operand)
204 | }
205 |
206 | // regexOperator 定义了过滤器中Regex操作符
207 | type regexOperator struct {
208 | regex *regexp.Regexp
209 | }
210 |
211 | func newRegexOp(argument gjson.Result) Filter {
212 | if argument.IsArray() || argument.IsObject() {
213 | panic("the argument of 'regex' operator must be a string")
214 | }
215 | return ®exOperator{regex: regexp.MustCompile(argument.String())}
216 | }
217 |
218 | // Eval 对payload执行RegexO过滤
219 | func (op *regexOperator) Eval(payload gjson.Result) bool {
220 | return op.regex.MatchString(payload.String())
221 | }
222 |
223 | // Generate 根据给定操作符名opName及操作符参数argument创建一个过滤器实例
224 | func Generate(opName string, argument gjson.Result) Filter {
225 | switch opName {
226 | case "not":
227 | return newNotOp(argument)
228 | case "and":
229 | return newAndOp(argument)
230 | case "or":
231 | return newOrOp(argument)
232 | case "eq":
233 | return newEqOp(argument)
234 | case "neq":
235 | return newNeqOp(argument)
236 | case "in":
237 | return newInOp(argument)
238 | case "contains":
239 | return newContainOp(argument)
240 | case "regex":
241 | return newRegexOp(argument)
242 | default:
243 | panic("the operator " + opName + " is not supported")
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/modules/filter/middlewares.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "os"
5 | "sync"
6 |
7 | "github.com/sirupsen/logrus"
8 | "github.com/tidwall/gjson"
9 | )
10 |
11 | var (
12 | filters = make(map[string]Filter)
13 | filterMutex sync.RWMutex
14 | )
15 |
16 | // Add adds a filter to the list of filters
17 | func Add(file string) {
18 | if file == "" {
19 | return
20 | }
21 | bs, err := os.ReadFile(file)
22 | if err != nil {
23 | logrus.Error("init filter error: ", err)
24 | return
25 | }
26 | defer func() {
27 | if err := recover(); err != nil {
28 | logrus.Error("init filter error: ", err)
29 | }
30 | }()
31 | filter := Generate("and", gjson.ParseBytes(bs))
32 | filterMutex.Lock()
33 | filters[file] = filter
34 | filterMutex.Unlock()
35 | }
36 |
37 | // Find returns the filter for the given file
38 | func Find(file string) Filter {
39 | if file == "" {
40 | return nil
41 | }
42 | filterMutex.RLock()
43 | defer filterMutex.RUnlock()
44 | return filters[file]
45 | }
46 |
--------------------------------------------------------------------------------
/modules/pprof/pprof.go:
--------------------------------------------------------------------------------
1 | // Package pprof provide pprof server of go-cqhttp
2 | package pprof
3 |
4 | import (
5 | "fmt"
6 | "net/http"
7 | "net/http/pprof"
8 | "os"
9 | "time"
10 |
11 | log "github.com/sirupsen/logrus"
12 | "gopkg.in/yaml.v3"
13 |
14 | "github.com/ProtocolScience/AstralGocq/coolq"
15 | "github.com/ProtocolScience/AstralGocq/modules/config"
16 | "github.com/ProtocolScience/AstralGocq/modules/servers"
17 | )
18 |
19 | const pprofDefault = ` # pprof 性能分析服务器, 一般情况下不需要启用.
20 | # 如果遇到性能问题请上传报告给开发者处理
21 | # 注意: pprof服务不支持中间件、不支持鉴权. 请不要开放到公网
22 | - pprof:
23 | # pprof服务器监听地址
24 | host: 127.0.0.1
25 | # pprof服务器监听端口
26 | port: 7700
27 | `
28 |
29 | // pprofServer pprof性能分析服务器相关配置
30 | type pprofServer struct {
31 | Disabled bool `yaml:"disabled"`
32 | Host string `yaml:"host"`
33 | Port int `yaml:"port"`
34 | }
35 |
36 | func init() {
37 | config.AddServer(&config.Server{
38 | Brief: "pprof 性能分析服务器",
39 | Default: pprofDefault,
40 | })
41 | }
42 |
43 | // runPprof 启动 pprof 性能分析服务器
44 | func runPprof(_ *coolq.CQBot, node yaml.Node) {
45 | var conf pprofServer
46 | switch err := node.Decode(&conf); {
47 | case err != nil:
48 | log.Warn("读取pprof配置失败 :", err)
49 | fallthrough
50 | case conf.Disabled:
51 | return
52 | }
53 |
54 | addr := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
55 | mux := http.NewServeMux()
56 | mux.HandleFunc("/debug/pprof/", pprof.Index)
57 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
58 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
59 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
60 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
61 | server := http.Server{Addr: addr, Handler: mux}
62 | log.Infof("pprof debug 服务器已启动: %v/debug/pprof", addr)
63 | log.Warnf("警告: pprof 服务不支持鉴权, 请不要运行在公网.")
64 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
65 | log.Error(err)
66 | log.Infof("pprof 服务启动失败, 请检查端口是否被占用.")
67 | log.Warnf("将在五秒后退出.")
68 | time.Sleep(time.Second * 5)
69 | os.Exit(1)
70 | }
71 | }
72 |
73 | func init() {
74 | servers.Register("pprof", runPprof)
75 | }
76 |
--------------------------------------------------------------------------------
/modules/servers/servers.go:
--------------------------------------------------------------------------------
1 | // Package servers provide servers register
2 | package servers
3 |
4 | import (
5 | "gopkg.in/yaml.v3"
6 |
7 | "github.com/ProtocolScience/AstralGocq/coolq"
8 | "github.com/ProtocolScience/AstralGocq/internal/base"
9 | )
10 |
11 | var (
12 | svr = make(map[string]func(*coolq.CQBot, yaml.Node))
13 | nocfgsvr = make(map[string]func(*coolq.CQBot))
14 | )
15 |
16 | // Register 注册 Server
17 | func Register(name string, proc func(*coolq.CQBot, yaml.Node)) {
18 | _, ok := svr[name]
19 | if ok {
20 | panic(name + " server has existed")
21 | }
22 | svr[name] = proc
23 | }
24 |
25 | // RegisterCustom 注册无需 config 的自定义 Server
26 | func RegisterCustom(name string, proc func(*coolq.CQBot)) {
27 | _, ok := nocfgsvr[name]
28 | if ok {
29 | panic(name + " server has existed")
30 | }
31 | nocfgsvr[name] = proc
32 | }
33 |
34 | // Run 运行所有svr
35 | func Run(bot *coolq.CQBot) {
36 | for _, l := range base.Servers {
37 | for name, conf := range l {
38 | if fn, ok := svr[name]; ok {
39 | go fn(bot, conf)
40 | }
41 | }
42 | }
43 | for _, fn := range nocfgsvr {
44 | go fn(bot)
45 | }
46 | base.Servers = nil
47 | }
48 |
--------------------------------------------------------------------------------
/modules/silk/codec.go:
--------------------------------------------------------------------------------
1 | //go:build (linux || (windows && !arm && !arm64) || darwin) && (386 || amd64 || arm || arm64) && !race && !nosilk
2 | // +build linux windows,!arm,!arm64 darwin
3 | // +build 386 amd64 arm arm64
4 | // +build !race
5 | // +build !nosilk
6 |
7 | package silk
8 |
9 | import (
10 | "os"
11 | "os/exec"
12 | "path"
13 |
14 | "github.com/pkg/errors"
15 | "github.com/wdvxdr1123/go-silk"
16 |
17 | "github.com/ProtocolScience/AstralGocq/internal/base"
18 | )
19 |
20 | const silkCachePath = "data/cache"
21 |
22 | // encode 将音频编码为Silk
23 | func encode(record []byte, tempName string) (silkWav []byte, err error) {
24 | // 1. 写入缓存文件
25 | rawPath := path.Join(silkCachePath, tempName+".wav")
26 | err = os.WriteFile(rawPath, record, os.ModePerm)
27 | if err != nil {
28 | return nil, errors.Wrap(err, "write temp file error")
29 | }
30 | defer os.Remove(rawPath)
31 |
32 | // 2.转换pcm
33 | pcmPath := path.Join(silkCachePath, tempName+".pcm")
34 | cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath)
35 | if errors.Is(cmd.Err, exec.ErrDot) {
36 | cmd.Err = nil
37 | }
38 | if base.Debug {
39 | cmd.Stdout = os.Stdout
40 | cmd.Stderr = os.Stderr
41 | }
42 | if err = cmd.Run(); err != nil {
43 | return nil, errors.Wrap(err, "convert pcm file error")
44 | }
45 | defer os.Remove(pcmPath)
46 |
47 | // 3. 转silk
48 | pcm, err := os.ReadFile(pcmPath)
49 | if err != nil {
50 | return nil, errors.Wrap(err, "read pcm file err")
51 | }
52 | silkWav, err = silk.EncodePcmBuffToSilk(pcm, 24000, 24000, true)
53 | if err != nil {
54 | return nil, errors.Wrap(err, "silk encode error")
55 | }
56 | silkPath := path.Join(silkCachePath, tempName+".silk")
57 | err = os.WriteFile(silkPath, silkWav, 0o666)
58 | return
59 | }
60 |
61 | // resample 将silk重新编码为 24000 bit rate
62 | func resample(data []byte) []byte {
63 | pcm, err := silk.DecodeSilkBuffToPcm(data, 24000)
64 | if err != nil {
65 | panic(err)
66 | }
67 | data, err = silk.EncodePcmBuffToSilk(pcm, 24000, 24000, true)
68 | if err != nil {
69 | panic(err)
70 | }
71 | return data
72 | }
73 |
--------------------------------------------------------------------------------
/modules/silk/codec_unsupported.go:
--------------------------------------------------------------------------------
1 | //go:build (!arm && !arm64 && !amd64 && !386) || (!windows && !linux && !darwin) || (windows && arm) || (windows && arm64) || race || nosilk
2 | // +build !arm,!arm64,!amd64,!386 !windows,!linux,!darwin windows,arm windows,arm64 race nosilk
3 |
4 | package silk
5 |
6 | import "errors"
7 |
8 | // encode 将音频编码为Silk
9 | func encode(record []byte, tempName string) ([]byte, error) {
10 | return nil, errors.New("not supported now")
11 | }
12 |
13 | // resample 将silk重新编码为 24000 bit rate
14 | func resample(data []byte) []byte {
15 | return data
16 | }
17 |
--------------------------------------------------------------------------------
/modules/silk/stubs.go:
--------------------------------------------------------------------------------
1 | // Package silk Silk编码核心模块
2 | package silk
3 |
4 | import (
5 | "github.com/ProtocolScience/AstralGocq/internal/base"
6 | )
7 |
8 | func init() {
9 | base.EncodeSilk = encode
10 | base.ResampleSilk = resample
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/onebot/attr.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package onebot
6 |
7 | import (
8 | "time"
9 | )
10 |
11 | // An Attr is a key-value pair.
12 | type Attr struct {
13 | Key string
14 | Value Value
15 | }
16 |
17 | // String returns an Attr for a string value.
18 | func String(key, value string) Attr {
19 | return Attr{key, StringValue(value)}
20 | }
21 |
22 | // Int64 returns an Attr for an int64.
23 | func Int64(key string, value int64) Attr {
24 | return Attr{key, Int64Value(value)}
25 | }
26 |
27 | // Int converts an int to an int64 and returns
28 | // an Attr with that value.
29 | func Int(key string, value int) Attr {
30 | return Int64(key, int64(value))
31 | }
32 |
33 | // Uint64 returns an Attr for a uint64.
34 | func Uint64(key string, v uint64) Attr {
35 | return Attr{key, Uint64Value(v)}
36 | }
37 |
38 | // Float64 returns an Attr for a floating-point number.
39 | func Float64(key string, v float64) Attr {
40 | return Attr{key, Float64Value(v)}
41 | }
42 |
43 | // Bool returns an Attr for a bool.
44 | func Bool(key string, v bool) Attr {
45 | return Attr{key, BoolValue(v)}
46 | }
47 |
48 | // Time returns an Attr for a time.Time.
49 | // It discards the monotonic portion.
50 | func Time(key string, v time.Time) Attr {
51 | return Attr{key, TimeValue(v)}
52 | }
53 |
54 | // Duration returns an Attr for a time.Duration.
55 | func Duration(key string, v time.Duration) Attr {
56 | return Attr{key, DurationValue(v)}
57 | }
58 |
59 | // Group returns an Attr for a Group Value.
60 | // The caller must not subsequently mutate the
61 | // argument slice.
62 | //
63 | // Use Group to collect several Attrs under a single
64 | // key on a log line, or as the result of LogValue
65 | // in order to log a single value as multiple Attrs.
66 | func Group(key string, as ...Attr) Attr {
67 | return Attr{key, GroupValue(as...)}
68 | }
69 |
70 | // Any returns an Attr for the supplied value.
71 | // See [Value.AnyValue] for how values are treated.
72 | func Any(key string, value any) Attr {
73 | return Attr{key, AnyValue(value)}
74 | }
75 |
76 | func (a Attr) String() string {
77 | return a.Key + "=" + a.Value.String()
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/onebot/kind_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type=Kind -trimprefix=Kind"; DO NOT EDIT.
2 |
3 | package onebot
4 |
5 | import "strconv"
6 |
7 | func _() {
8 | // An "invalid array index" compiler error signifies that the constant values have changed.
9 | // Re-run the stringer command to generate them again.
10 | var x [1]struct{}
11 | _ = x[KindAny-0]
12 | _ = x[KindBool-1]
13 | _ = x[KindDuration-2]
14 | _ = x[KindFloat64-3]
15 | _ = x[KindInt64-4]
16 | _ = x[KindString-5]
17 | _ = x[KindTime-6]
18 | _ = x[KindUint64-7]
19 | _ = x[KindGroup-8]
20 | }
21 |
22 | const _Kind_name = "AnyBoolDurationFloat64Int64StringTimeUint64Group"
23 |
24 | var _Kind_index = [...]uint8{0, 3, 7, 15, 22, 27, 33, 37, 43, 48}
25 |
26 | func (i Kind) String() string {
27 | if i < 0 || i >= Kind(len(_Kind_index)-1) {
28 | return "Kind(" + strconv.FormatInt(int64(i), 10) + ")"
29 | }
30 | return _Kind_name[_Kind_index[i]:_Kind_index[i+1]]
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/onebot/onebot.go:
--------------------------------------------------------------------------------
1 | package onebot
2 |
3 | // Self 机器人自身标识
4 | //
5 | // https://12.onebot.dev/connect/data-protocol/basic-types/#_10
6 | type Self struct {
7 | Platform string `json:"platform"`
8 | UserID string `json:"user_id"`
9 | }
10 |
11 | // Request 动作请求是应用端为了主动向 OneBot 实现请求服务而发送的数据
12 | //
13 | // https://12.onebot.dev/connect/data-protocol/action-request/
14 | type Request struct {
15 | Action string // 动作名称
16 | Params any // 动作参数
17 | Echo any // 每次请求的唯一标识
18 | }
19 |
20 | // Response 动作响应是 OneBot 实现收到应用端的动作请求并处理完毕后,发回应用端的数据
21 | //
22 | // https://12.onebot.dev/connect/data-protocol/action-response/
23 | type Response struct {
24 | Status string `json:"status"` // 执行状态,必须是 ok、failed 中的一个
25 | Code int64 `json:"retcode"` // 返回码
26 | Data any `json:"data"` // 响应数据
27 | Message string `json:"message"` // 错误信息
28 | Echo any `json:"echo"` // 动作请求中的 echo 字段值
29 | }
30 |
31 | // Event 事件
32 | //
33 | // https://12.onebot.dev/connect/data-protocol/event/
34 | type Event struct {
35 | ID string
36 | Time int64
37 | Type string
38 | DetailType string
39 | SubType string
40 | Self *Self
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/onebot/spec.go:
--------------------------------------------------------------------------------
1 | // Package onebot defines onebot protocol struct and some spec info.
2 | package onebot
3 |
4 | import "fmt"
5 |
6 | //go:generate go run ./../../cmd/api-generator -pkg onebot -path=./../../coolq/api.go,./../../coolq/api_v12.go -supported -o supported.go
7 |
8 | // Spec OneBot Specification
9 | type Spec struct {
10 | Version int // must be 11 or 12
11 | SupportedActions []string
12 | }
13 |
14 | // V11 OneBot V11
15 | var V11 = &Spec{
16 | Version: 11,
17 | SupportedActions: supportedV11,
18 | }
19 |
20 | // V12 OneBot V12
21 | var V12 = &Spec{
22 | Version: 12,
23 | SupportedActions: supportedV12,
24 | }
25 |
26 | // ConvertID 根据版本转换ID
27 | func (s *Spec) ConvertID(id any) any {
28 | if s.Version == 12 {
29 | return fmt.Sprint(id)
30 | }
31 | return id
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/onebot/supported.go:
--------------------------------------------------------------------------------
1 | // Code generated by cmd/api-generator. DO NOT EDIT.
2 |
3 | package onebot
4 |
5 | var supportedV11 = []string{
6 | ".get_word_slices",
7 | ".handle_quick_operation",
8 | ".ocr_image",
9 | "ocr_image",
10 | "_del_group_notice",
11 | "_get_group_notice",
12 | "_get_model_show",
13 | "_send_group_notice",
14 | "_set_model_show",
15 | "can_send_image",
16 | "can_send_record",
17 | "check_url_safely",
18 | "create_group_file_folder",
19 | "create_guild_role",
20 | "delete_essence_msg",
21 | "delete_friend",
22 | "delete_group_file",
23 | "delete_group_folder",
24 | "delete_guild_role",
25 | "delete_msg",
26 | "delete_unidirectional_friend",
27 | "download_file",
28 | "get_essence_msg_list",
29 | "get_forward_msg",
30 | "get_friend_list",
31 | "get_group_at_all_remain",
32 | "get_group_file_system_info",
33 | "get_group_file_url",
34 | "get_group_files_by_folder",
35 | "get_group_honor_info",
36 | "get_group_info",
37 | "get_group_list",
38 | "get_group_member_info",
39 | "get_group_member_list",
40 | "get_group_msg_history",
41 | "get_group_root_files",
42 | "get_group_system_msg",
43 | "get_guild_channel_list",
44 | "get_guild_list",
45 | "get_guild_member_list",
46 | "get_guild_member_profile",
47 | "get_guild_meta_by_guest",
48 | "get_guild_msg",
49 | "get_guild_roles",
50 | "get_guild_service_profile",
51 | "get_image",
52 | "get_login_info",
53 | "get_msg",
54 | "get_online_clients",
55 | "get_status",
56 | "get_stranger_info",
57 | "get_supported_actions",
58 | "get_topic_channel_feeds",
59 | "get_unidirectional_friend_list",
60 | "get_version_info",
61 | "mark_msg_as_read",
62 | "qidian_get_account_info",
63 | "reload_event_filter",
64 | "send_forward_msg",
65 | "set_group_reaction",
66 | "send_group_forward_msg",
67 | "send_group_msg",
68 | "send_group_sign",
69 | "send_guild_channel_msg",
70 | "send_msg",
71 | "send_private_forward_msg",
72 | "send_private_msg",
73 | "set_essence_msg",
74 | "set_friend_add_request",
75 | "set_group_add_request",
76 | "set_group_admin",
77 | "set_group_anonymous",
78 | "set_group_anonymous_ban",
79 | "set_group_ban",
80 | "set_group_card",
81 | "set_group_kick",
82 | "set_group_leave",
83 | "set_group_name",
84 | "set_group_portrait",
85 | "set_group_special_title",
86 | "set_group_whole_ban",
87 | "set_guild_member_role",
88 | "set_qq_profile",
89 | "update_guild_role",
90 | "upload_group_file",
91 | "upload_private_file",
92 | }
93 |
94 | var supportedV12 = []string{
95 | ".get_word_slices",
96 | ".ocr_image",
97 | "ocr_image",
98 | "_del_group_notice",
99 | "_get_group_notice",
100 | "_get_model_show",
101 | "_send_group_notice",
102 | "_set_model_show",
103 | "check_url_safely",
104 | "create_group_file_folder",
105 | "create_guild_role",
106 | "delete_essence_msg",
107 | "delete_friend",
108 | "delete_group_file",
109 | "delete_group_folder",
110 | "delete_guild_role",
111 | "delete_msg",
112 | "delete_unidirectional_friend",
113 | "download_file",
114 | "get_essence_msg_list",
115 | "get_forward_msg",
116 | "get_friend_list",
117 | "get_group_at_all_remain",
118 | "get_group_file_system_info",
119 | "get_group_file_url",
120 | "get_group_files_by_folder",
121 | "get_group_honor_info",
122 | "get_group_info",
123 | "get_group_list",
124 | "get_group_member_info",
125 | "get_group_member_list",
126 | "get_group_msg_history",
127 | "get_group_root_files",
128 | "get_group_system_msg",
129 | "get_guild_channel_list",
130 | "get_guild_list",
131 | "get_guild_member_list",
132 | "get_guild_member_profile",
133 | "get_guild_meta_by_guest",
134 | "get_guild_msg",
135 | "get_guild_roles",
136 | "get_guild_service_profile",
137 | "get_image",
138 | "get_self_info",
139 | "get_msg",
140 | "get_online_clients",
141 | "get_status",
142 | "get_user_info",
143 | "get_supported_actions",
144 | "get_topic_channel_feeds",
145 | "get_unidirectional_friend_list",
146 | "mark_msg_as_read",
147 | "qidian_get_account_info",
148 | "reload_event_filter",
149 | "send_group_sign",
150 | "send_guild_channel_msg",
151 | "set_essence_msg",
152 | "set_friend_add_request",
153 | "set_group_add_request",
154 | "set_group_admin",
155 | "set_group_anonymous",
156 | "set_group_anonymous_ban",
157 | "set_group_ban",
158 | "set_group_card",
159 | "set_group_kick",
160 | "set_group_leave",
161 | "set_group_name",
162 | "set_group_portrait",
163 | "set_group_special_title",
164 | "set_group_whole_ban",
165 | "set_guild_member_role",
166 | "set_qq_profile",
167 | "update_guild_role",
168 | "upload_group_file",
169 | "upload_private_file",
170 | }
171 |
--------------------------------------------------------------------------------
/scripts/bootstrap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | function index.main_handler() {
3 | echo "Start GOCQHTTP~~~"
4 | cp -f config.yml /tmp/config.yml
5 | cp -f device.json /tmp/device.json
6 | ./go-cqhttp -w="/tmp/" faststart
7 | }
8 | index.main_handler
9 |
--------------------------------------------------------------------------------
/scripts/upload_dist.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$GITHUB_ACTIONS" != "true" ]; then
4 | echo "This script is only meant to be run in GitHub Actions."
5 | exit 1
6 | fi
7 |
8 | cp -f dist/*.tar.gz upstream/dist/downloads
9 | cp -f dist/*.zip upstream/dist/downloads
10 | cd upstream/dist || exit
11 | LATEST_VERSION="${GITHUB_REF#"refs/tags/"}"
12 | git config --local user.name 'Github Actions'
13 | git config --local user.email 'github-actions@users.noreply.github.com'
14 | git add --all
15 | git commit -m "update to $LATEST_VERSION"
16 | git tag -d "$LATEST_VERSION"
17 | git tag "$LATEST_VERSION"
18 | git push
19 | git push --tags
--------------------------------------------------------------------------------
/server/daemon.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | // daemon 功能写在这,目前仅支持了-d 作为后台运行参数,stop,start,restart这些功能目前看起来并不需要,可以通过api控制,后续需要的话再补全。
4 |
5 | import (
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/ProtocolScience/AstralGocq/global"
13 |
14 | log "github.com/sirupsen/logrus"
15 | )
16 |
17 | // Daemon go-cqhttp server 的 daemon的实现函数
18 | func Daemon() {
19 | args := os.Args[1:]
20 |
21 | execArgs := make([]string, 0)
22 |
23 | l := len(args)
24 | for i := 0; i < l; i++ {
25 | if strings.Index(args[i], "-d") == 0 {
26 | continue
27 | }
28 |
29 | execArgs = append(execArgs, args[i])
30 | }
31 |
32 | ex, _ := os.Executable()
33 | p, _ := filepath.Abs(ex)
34 | proc := exec.Command(p, execArgs...)
35 | err := proc.Start()
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | log.Info("[PID] ", proc.Process.Pid)
41 | // pid写入到pid文件中,方便后续stop的时候kill
42 | pidErr := savePid("go-cqhttp.pid", strconv.FormatInt(int64(proc.Process.Pid), 10))
43 | if pidErr != nil {
44 | log.Errorf("save pid file error: %v", pidErr)
45 | }
46 |
47 | os.Exit(0)
48 | }
49 |
50 | // savePid 保存pid到文件中,便于后续restart/stop的时候kill pid用。
51 | func savePid(path string, data string) error {
52 | return global.WriteAllText(path, data)
53 | }
54 |
--------------------------------------------------------------------------------
/server/doc.go:
--------------------------------------------------------------------------------
1 | // Package server 包含HTTP,WebSocket,反向WebSocket请求处理的相关函数与结构体
2 | package server
3 |
4 | import "github.com/ProtocolScience/AstralGocq/modules/servers"
5 |
6 | // 注册
7 | func init() {
8 | servers.Register("http", runHTTP)
9 | servers.Register("ws", runWSServer)
10 | servers.Register("ws-reverse", runWSClient)
11 | servers.Register("lambda", runLambda)
12 | }
13 |
--------------------------------------------------------------------------------
/server/http_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tidwall/gjson"
9 | )
10 |
11 | func TestHttpCtx_Get(t *testing.T) {
12 | cases := []struct {
13 | ctx *httpCtx
14 | key string
15 | expected string
16 | }{
17 | {
18 | ctx: &httpCtx{
19 | json: gjson.Result{},
20 | query: url.Values{
21 | "sub_type": []string{"hello"},
22 | "type": []string{"world"},
23 | },
24 | },
25 | key: "[sub_type,type].0",
26 | expected: "hello",
27 | },
28 | {
29 | ctx: &httpCtx{
30 | json: gjson.Result{},
31 | query: url.Values{
32 | "type": []string{"114514"},
33 | },
34 | },
35 | key: "[sub_type,type].0",
36 | expected: "114514",
37 | },
38 | }
39 | for _, c := range cases {
40 | assert.Equal(t, c.expected, c.ctx.Get(c.key).String())
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/server/middlewares.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "container/list"
5 | "context"
6 | "sync"
7 | "time"
8 |
9 | "github.com/ProtocolScience/AstralGocq/coolq"
10 | "github.com/ProtocolScience/AstralGocq/global"
11 | "github.com/ProtocolScience/AstralGocq/modules/api"
12 | "github.com/ProtocolScience/AstralGocq/pkg/onebot"
13 |
14 | "golang.org/x/time/rate"
15 | )
16 |
17 | // MiddleWares 通信中间件
18 | type MiddleWares struct {
19 | AccessToken string `yaml:"access-token"`
20 | Filter string `yaml:"filter"`
21 | RateLimit struct {
22 | Enabled bool `yaml:"enabled"`
23 | Frequency float64 `yaml:"frequency"`
24 | Bucket int `yaml:"bucket"`
25 | } `yaml:"rate-limit"`
26 | }
27 |
28 | func rateLimit(frequency float64, bucketSize int) api.Handler {
29 | limiter := rate.NewLimiter(rate.Limit(frequency), bucketSize)
30 | return func(_ string, _ *onebot.Spec, _ api.Getter) global.MSG {
31 | _ = limiter.Wait(context.Background())
32 | return nil
33 | }
34 | }
35 |
36 | func longPolling(bot *coolq.CQBot, maxSize int) api.Handler {
37 | var mutex sync.Mutex
38 | cond := sync.NewCond(&mutex)
39 | queue := list.New()
40 | bot.OnEventPush(func(event *coolq.Event) {
41 | mutex.Lock()
42 | defer mutex.Unlock()
43 | queue.PushBack(event.Raw)
44 | for maxSize != 0 && queue.Len() > maxSize {
45 | queue.Remove(queue.Front())
46 | }
47 | cond.Signal()
48 | })
49 | return func(action string, spec *onebot.Spec, p api.Getter) global.MSG {
50 | switch {
51 | case spec.Version == 11 && action == "get_updates": // ok
52 | case spec.Version == 12 && action == "get_latest_events": // ok
53 | default:
54 | return nil
55 | }
56 | var (
57 | ch = make(chan []any)
58 | timeout = time.Duration(p.Get("timeout").Int()) * time.Second
59 | )
60 | go func() {
61 | mutex.Lock()
62 | defer mutex.Unlock()
63 | for queue.Len() == 0 {
64 | cond.Wait()
65 | }
66 | limit := int(p.Get("limit").Int())
67 | if limit <= 0 || queue.Len() < limit {
68 | limit = queue.Len()
69 | }
70 | ret := make([]any, limit)
71 | elem := queue.Front()
72 | for i := 0; i < limit; i++ {
73 | ret[i] = elem.Value
74 | elem = elem.Next()
75 | }
76 | select {
77 | case ch <- ret:
78 | for i := 0; i < limit; i++ { // remove sent msg
79 | queue.Remove(queue.Front())
80 | }
81 | default:
82 | // don't block if parent already return due to timeout
83 | }
84 | }()
85 | if timeout != 0 {
86 | select {
87 | case <-time.After(timeout):
88 | return coolq.OK([]any{})
89 | case ret := <-ch:
90 | return coolq.OK(ret)
91 | }
92 | }
93 | return coolq.OK(<-ch)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/server/scf.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "runtime/debug"
12 | "strings"
13 |
14 | "github.com/ProtocolScience/AstralGo/utils"
15 | log "github.com/sirupsen/logrus"
16 | "gopkg.in/yaml.v3"
17 |
18 | "github.com/ProtocolScience/AstralGocq/coolq"
19 | "github.com/ProtocolScience/AstralGocq/global"
20 | api2 "github.com/ProtocolScience/AstralGocq/modules/api"
21 | "github.com/ProtocolScience/AstralGocq/modules/config"
22 | )
23 |
24 | type lambdaClient struct {
25 | nextURL string
26 | responseURL string
27 | lambdaType string
28 |
29 | client http.Client
30 | }
31 |
32 | type lambdaResponse struct {
33 | IsBase64Encoded bool `json:"isBase64Encoded"`
34 | StatusCode int `json:"statusCode"`
35 | Headers map[string]string `json:"headers"`
36 | Body string `json:"body"`
37 | }
38 |
39 | type lambdaResponseWriter struct {
40 | statusCode int
41 | buf bytes.Buffer
42 | header http.Header
43 | }
44 |
45 | func (l *lambdaResponseWriter) Write(p []byte) (n int, err error) {
46 | return l.buf.Write(p)
47 | }
48 |
49 | func (l *lambdaResponseWriter) Header() http.Header {
50 | return l.header
51 | }
52 |
53 | func (l *lambdaResponseWriter) flush() error {
54 | buffer := global.NewBuffer()
55 | defer global.PutBuffer(buffer)
56 | body := utils.B2S(l.buf.Bytes())
57 | header := make(map[string]string, len(l.header))
58 | for k, v := range l.header {
59 | header[k] = v[0]
60 | }
61 | _ = json.NewEncoder(buffer).Encode(&lambdaResponse{
62 | IsBase64Encoded: false,
63 | StatusCode: l.statusCode,
64 | Headers: header,
65 | Body: body,
66 | })
67 |
68 | r, _ := http.NewRequest(http.MethodPost, cli.responseURL, buffer)
69 | do, err := cli.client.Do(r)
70 | if err != nil {
71 | return err
72 | }
73 | return do.Body.Close()
74 | }
75 |
76 | func (l *lambdaResponseWriter) WriteHeader(statusCode int) {
77 | l.statusCode = statusCode
78 | }
79 |
80 | var cli *lambdaClient
81 |
82 | // runLambda type: [scf,aws]
83 | func runLambda(bot *coolq.CQBot, node yaml.Node) {
84 | var conf LambdaServer
85 | switch err := node.Decode(&conf); {
86 | case err != nil:
87 | log.Warn("读取lambda配置失败 :", err)
88 | fallthrough
89 | case conf.Disabled:
90 | return
91 | }
92 |
93 | cli = &lambdaClient{
94 | lambdaType: conf.Type,
95 | client: http.Client{Timeout: 0},
96 | }
97 | switch cli.lambdaType { // todo: aws
98 | case "scf": // tencent serverless function
99 | base := fmt.Sprintf("http://%s:%s/runtime/",
100 | os.Getenv("SCF_RUNTIME_API"),
101 | os.Getenv("SCF_RUNTIME_API_PORT"))
102 | cli.nextURL = base + "invocation/next"
103 | cli.responseURL = base + "invocation/response"
104 | post, err := http.Post(base+"init/ready", "", nil)
105 | if err != nil {
106 | log.Warnf("lambda 初始化失败: %v", err)
107 | return
108 | }
109 | _ = post.Body.Close()
110 | case "aws": // aws lambda
111 | const apiVersion = "2018-06-01"
112 | base := fmt.Sprintf("http://%s/%s/runtime/", os.Getenv("AWS_LAMBDA_RUNTIME_API"), apiVersion)
113 | cli.nextURL = base + "invocation/next"
114 | cli.responseURL = base + "invocation/response"
115 | default:
116 | log.Fatal("unknown lambda type:", conf.Type)
117 | }
118 |
119 | api := api2.NewCaller(bot)
120 | if conf.RateLimit.Enabled {
121 | api.Use(rateLimit(conf.RateLimit.Frequency, conf.RateLimit.Bucket))
122 | }
123 | server := &httpServer{
124 | api: api,
125 | accessToken: conf.AccessToken,
126 | }
127 |
128 | for {
129 | req := cli.next()
130 | writer := lambdaResponseWriter{statusCode: 200, header: make(http.Header)}
131 | func() {
132 | defer func() {
133 | if e := recover(); e != nil {
134 | log.Warnf("Lambda 出现不可恢复错误: %v\n%s", e, debug.Stack())
135 | }
136 | }()
137 | if req != nil {
138 | server.ServeHTTP(&writer, req)
139 | }
140 | }()
141 | if err := writer.flush(); err != nil {
142 | log.Warnf("Lambda 发送响应失败: %v", err)
143 | }
144 | }
145 | }
146 |
147 | type lambdaInvoke struct {
148 | Headers map[string]string
149 | HTTPMethod string `json:"httpMethod"`
150 | Body string `json:"body"`
151 | Path string `json:"path"`
152 | QueryString map[string]string
153 | RequestContext struct {
154 | Path string `json:"path"`
155 | } `json:"requestContext"`
156 | }
157 |
158 | const lambdaDefault = ` # LambdaServer 配置
159 | - lambda:
160 | type: scf # scf: 腾讯云函数 aws: aws Lambda
161 | middlewares:
162 | <<: *default # 引用默认中间件
163 | `
164 |
165 | // LambdaServer 云函数配置
166 | type LambdaServer struct {
167 | Disabled bool `yaml:"disabled"`
168 | Type string `yaml:"type"`
169 |
170 | MiddleWares `yaml:"middlewares"`
171 | }
172 |
173 | func init() {
174 | config.AddServer(&config.Server{
175 | Brief: "云函数服务",
176 | Default: lambdaDefault,
177 | })
178 | }
179 |
180 | func (c *lambdaClient) next() *http.Request {
181 | r, err := http.NewRequest(http.MethodGet, c.nextURL, nil)
182 | if err != nil {
183 | return nil
184 | }
185 | resp, err := c.client.Do(r)
186 | if err != nil {
187 | return nil
188 | }
189 | defer resp.Body.Close()
190 | if resp.StatusCode != http.StatusOK {
191 | return nil
192 | }
193 | var req http.Request
194 | var invoke lambdaInvoke
195 | _ = json.NewDecoder(resp.Body).Decode(&invoke)
196 | if invoke.HTTPMethod == "" { // 不是 api 网关
197 | return nil
198 | }
199 |
200 | req.Method = invoke.HTTPMethod
201 | req.Body = io.NopCloser(strings.NewReader(invoke.Body))
202 | req.Header = make(map[string][]string)
203 | for k, v := range invoke.Headers {
204 | req.Header.Set(k, v)
205 | }
206 | req.URL = new(url.URL)
207 | req.URL.Path = strings.TrimPrefix(invoke.Path, invoke.RequestContext.Path)
208 | // todo: avoid encoding
209 | query := make(url.Values)
210 | for k, v := range invoke.QueryString {
211 | query[k] = []string{v}
212 | }
213 | req.URL.RawQuery = query.Encode()
214 | return &req
215 | }
216 |
--------------------------------------------------------------------------------
/winres/.gitignore:
--------------------------------------------------------------------------------
1 | winres.json
--------------------------------------------------------------------------------
/winres/gen/json.go:
--------------------------------------------------------------------------------
1 | // Package main generates winres.json
2 | package main
3 |
4 | import (
5 | "bytes"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "strings"
10 | "time"
11 |
12 | "github.com/ProtocolScience/AstralGocq/internal/base"
13 | )
14 |
15 | const js = `{
16 | "RT_GROUP_ICON": {
17 | "APP": {
18 | "0000": [
19 | "icon.png",
20 | "icon16.png"
21 | ]
22 | }
23 | },
24 | "RT_MANIFEST": {
25 | "#1": {
26 | "0409": {
27 | "identity": {
28 | "name": "go-cqhttp",
29 | "version": "%s"
30 | },
31 | "description": "",
32 | "minimum-os": "vista",
33 | "execution-level": "as invoker",
34 | "ui-access": false,
35 | "auto-elevate": false,
36 | "dpi-awareness": "system",
37 | "disable-theming": false,
38 | "disable-window-filtering": false,
39 | "high-resolution-scrolling-aware": false,
40 | "ultra-high-resolution-scrolling-aware": false,
41 | "long-path-aware": false,
42 | "printer-driver-isolation": false,
43 | "gdi-scaling": false,
44 | "segment-heap": false,
45 | "use-common-controls-v6": false
46 | }
47 | }
48 | },
49 | "RT_VERSION": {
50 | "#1": {
51 | "0000": {
52 | "fixed": {
53 | "file_version": "%s",
54 | "product_version": "%s",
55 | "timestamp": "%s"
56 | },
57 | "info": {
58 | "0409": {
59 | "Comments": "Golang implementation of cqhttp.",
60 | "CompanyName": "Mrs4s",
61 | "FileDescription": "https://github.com/ProtocolScience/AstralGocq",
62 | "FileVersion": "%s",
63 | "InternalName": "",
64 | "LegalCopyright": "©️ 2020 - %d Mrs4s. All Rights Reserved.",
65 | "LegalTrademarks": "",
66 | "OriginalFilename": "GOCQHTTP.EXE",
67 | "PrivateBuild": "",
68 | "ProductName": "go-cqhttp",
69 | "ProductVersion": "%s",
70 | "SpecialBuild": ""
71 | }
72 | }
73 | }
74 | }
75 | }
76 | }`
77 |
78 | const timeformat = `2006-01-02T15:04:05+08:00`
79 |
80 | func main() {
81 | f, err := os.Create("winres.json")
82 | if err != nil {
83 | panic(err)
84 | }
85 | defer f.Close()
86 | v := ""
87 | if base.Version == "(devel)" {
88 | vartag := bytes.NewBuffer(nil)
89 | vartagcmd := exec.Command("git", "tag", "--sort=committerdate")
90 | vartagcmd.Stdout = vartag
91 | err = vartagcmd.Run()
92 | if err != nil {
93 | panic(err)
94 | }
95 | s := strings.Split(vartag.String(), "\n")
96 | v = s[len(s)-2]
97 | } else {
98 | v = base.Version
99 | }
100 | i := strings.Index(v, "-") // remove -rc / -beta
101 | if i <= 0 {
102 | i = len(v)
103 | }
104 | commitcnt := strings.Builder{}
105 | commitcnt.WriteString(v[1:i])
106 | commitcnt.WriteByte('.')
107 | commitcntcmd := exec.Command("git", "rev-list", "--count", "nt")
108 | commitcntcmd.Stdout = &commitcnt
109 | err = commitcntcmd.Run()
110 | if err != nil {
111 | panic(err)
112 | }
113 | fv := commitcnt.String()[:commitcnt.Len()-1]
114 | _, err = fmt.Fprintf(f, js, fv, fv, v, time.Now().Format(timeformat), fv, time.Now().Year(), v)
115 | if err != nil {
116 | panic(err)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/winres/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProtocolScience/AstralGocq/1fd035f470a92de515ccc42403256add22031715/winres/icon.png
--------------------------------------------------------------------------------
/winres/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProtocolScience/AstralGocq/1fd035f470a92de515ccc42403256add22031715/winres/icon16.png
--------------------------------------------------------------------------------
/winres/init.go:
--------------------------------------------------------------------------------
1 | // Package winres 生成windows资源
2 | package winres
3 |
4 | //go:generate go run github.com/ProtocolScience/AstralGocq/winres/gen
5 |
--------------------------------------------------------------------------------