├── .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 | 40 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 61 | 62 |
coolq 41 | 包含与 MiraiGo 交互部分, CQ码解析等部分 42 |
server 47 | 包含 http,ws 通信的实现部分 48 |
global 53 | 一个实用的工具包 54 |
docs 59 | 使用教程与文档 60 |
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 | ![image.png](https://i.loli.net/2020/12/27/yXdomOnQ8tkauMe.png) 18 | 19 | 首先选择 `1` 并提取链接在浏览器中打开 20 | 21 | ![image.png](https://i.loli.net/2020/12/27/HYhmZv1wARMV7Uq.png) 22 | 23 | ![image.png](https://i.loli.net/2020/12/27/otk9Hz7lBCaRFMV.png) 24 | 25 | 此时不要滑动验证码, 首先按下 `F12` (键盘右上角退格键上方) 打开 *开发者工具* 26 | 27 | ![image.png](https://i.loli.net/2020/12/27/JDioadLPwcKWpt1.png) 28 | 29 | 点击 `Network` 选项卡 (在某些浏览器它可能叫做 `网络`) 30 | 31 | ![image.png](https://i.loli.net/2020/12/27/qEzTB5jrDZUWSwp.png) 32 | 33 | 点开 `Filter` (箭头) 按钮以确定您能看到下面的工具栏, 勾选 `Preserve log`(红框) 34 | 35 | 此时可以滑动并通过验证码 36 | 37 | ![image.png](https://i.loli.net/2020/12/27/Id4hxzyDprQuF2G.png) 38 | 39 | 回到 *开发者工具*, 我们可以看到已经有了一个请求. 40 | 41 | ![image.png](https://i.loli.net/2020/12/27/3C6Y2XVKBRv1z9E.png) 42 | 43 | 此时如果有多个请求, 请不要慌张. 看到上面的 `Filter` 没? 此时在 `Filter` 输入框中输入 `cap_union_new`, 就应该只剩一个请求了. 44 | 45 | 然后点击该请求. 点开 `Preview` 选项卡 (箭头): 46 | 47 | ![image.png](https://i.loli.net/2020/12/27/P1VtxRWpjY8524Z.png) 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 | ![image.png](https://i.loli.net/2020/12/27/winG4SkxhgLoNDZ.png) 62 | 63 | 打开 `go-cqhttp` 并选择 `2`: 64 | 65 | ![image.png](https://i.loli.net/2020/12/27/yXdomOnQ8tkauMe.png) 66 | 67 | 复制 `ID` 并前往工具粘贴: 68 | 69 | ![image.png](https://i.loli.net/2020/12/27/fIwXx5nN9r8Zbc7.png) 70 | 71 | ![image.png](https://i.loli.net/2020/12/27/WZsTCyGwSjc9mb5.png) 72 | 73 | 点击 `OK` 并处理滑块, 完成即可登录成功. (OK可能反应稍微慢点, 请不要多次点击) 74 | 75 | ![image.png](https://i.loli.net/2020/12/27/UnvAuxreijYzgLC.png) 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 | --------------------------------------------------------------------------------