├── .dockerignore
├── .github
├── FUNDING.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .gitmodules
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build.sh
├── build_android.sh
├── build_proto.sh
├── dto_proto
└── http_dto.proto
├── go.mod
├── go.sum
├── pkg
├── bot
│ ├── api_handler.go
│ ├── api_handler_test.go
│ ├── bot.go
│ ├── bot_test.go
│ ├── gen_client_map.go
│ ├── gen_remote_map.go
│ ├── gen_token_map.go
│ ├── mirai2proto.go
│ ├── mirai2raw.go
│ ├── proto2mirai.go
│ ├── raw2mirai.go
│ └── remote.go
├── cache
│ ├── cache.go
│ └── cache_test.go
├── config
│ ├── config.go
│ ├── gen_plugin_map.go
│ └── setting.go
├── device
│ └── device.go
├── download
│ └── download.go
├── gmc
│ ├── gmc.go
│ ├── handler
│ │ ├── bot.go
│ │ └── middlewares.go
│ └── plugins
│ │ ├── hello.go
│ │ ├── log.go
│ │ └── report.go
├── plugin
│ └── plugin.go
├── safe_ws
│ └── safe_ws.go
├── static
│ ├── static.go
│ └── static
│ │ ├── asset-manifest.json
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ ├── robots.txt
│ │ └── static
│ │ ├── css
│ │ ├── 2.26352ec4.chunk.css
│ │ └── main.13efcf60.chunk.css
│ │ └── js
│ │ ├── 2.57752891.chunk.js
│ │ ├── 2.57752891.chunk.js.LICENSE.txt
│ │ ├── 3.a6cb6b59.chunk.js
│ │ ├── main.ebd0543e.chunk.js
│ │ └── runtime-main.016cb37b.js
└── util
│ ├── rand.go
│ ├── util.go
│ └── util_test.go
├── proto_gen
├── dto
│ ├── http_dto.pb.go
│ └── http_dto_grpc.pb.go
└── onebot
│ ├── onebot_api.pb.go
│ ├── onebot_base.pb.go
│ ├── onebot_event.pb.go
│ ├── onebot_forward.pb.go
│ └── onebot_frame.pb.go
├── scripts
├── env_run.sh
├── linux_run.sh
└── windows_run.bat
└── service
├── glc
└── main.go
└── gmc_android
├── gmc.go
└── logger.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | /device
2 | /releases
3 | /output
4 | /video
5 | captcha.jpg
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [2mf8] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | with:
22 | go-version: stable
23 | # More assembly might be required: Docker logins, GPG, etc.
24 | # It all depends on your needs.
25 | - name: Run GoReleaser
26 | uses: goreleaser/goreleaser-action@v6
27 | with:
28 | # either 'goreleaser' (default) or 'goreleaser-pro'
29 | distribution: goreleaser
30 | # 'latest', 'nightly', or a semver
31 | version: "~> v1"
32 | args: release --clean
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.o
2 | *.a
3 | *.so
4 | _obj
5 | _test
6 | *.[568vq]
7 | [568vq].out
8 | *.cgo1.go
9 | *.cgo2.c
10 | _cgo_defun.c
11 | _cgo_gotypes.go
12 | _cgo_export.*
13 | _testmain.go
14 | *.exe
15 | *.exe~
16 | *.test
17 | *.prof
18 | *.rar
19 | *.zip
20 | *.gz
21 | *.psd
22 | *.bmd
23 | *.cfg
24 | *.pptx
25 | *.log
26 | *nohup.out
27 | *settings.pyc
28 | *.sublime-project
29 | *.sublime-workspace
30 | !.gitkeep
31 | .DS_Store
32 | /.idea
33 | /.vscode
34 | /output
35 | *.local.yml
36 | device.json
37 | Go-Mirai-Client
38 | device-*.json
39 | gmc-*.zip
40 | captcha.jpg
41 | /video
42 | /device
43 | /releases
44 | /gmc_config.json
45 | cuberbot
46 | tmp
47 | dist/
48 | plugins/
49 | gmc_android.aar
50 | gmc_android-sources.jar
51 | *.bat
52 | *.jar
53 | *.aar
54 | *.syso
55 | *.toml
56 | *.token
57 | login.json
58 | *.jpg
59 | glc
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "onebot_glc"]
2 | path = onebot_glc
3 | url = https://github.com/2mf8/onebot_glc.git
4 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | env:
2 | - GO111MODULE=on
3 | before:
4 | hooks:
5 | - go mod tidy
6 | release:
7 | draft: true
8 | name_template: "{{.ProjectName}}-{{.Tag}}"
9 | builds:
10 | - id: console
11 | main: ./service/glc
12 | env:
13 | - CGO_ENABLED=0
14 | - GO111MODULE=on
15 | goos:
16 | - linux
17 | - darwin
18 | - windows
19 | goarch:
20 | - 386
21 | - amd64
22 | - arm
23 | - arm64
24 | goarm:
25 | - 7
26 | ignore:
27 | - goos: darwin
28 | goarch: arm
29 | - goos: darwin
30 | goarch: 386
31 | - goos: windows
32 | goarch: arm
33 | - goos: windows
34 | goarch: arm64
35 | mod_timestamp: "{{ .CommitTimestamp }}"
36 | flags:
37 | - -trimpath
38 | ldflags:
39 | - -s -w -extldflags '-static'
40 | # hooks:
41 | # post:
42 | # - upx "{{ .Path }}"
43 |
44 | checksum:
45 | name_template: "{{ .ProjectName }}_checksums.txt"
46 | changelog:
47 | skip: true
48 | archives:
49 | - id: console
50 | builds:
51 | - console
52 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
53 | format_overrides:
54 | - goos: windows
55 | format: binary
56 | # - goos: linux
57 | # format: zip
58 | # - goos: darwin
59 | # format: zip
60 |
61 | nfpms:
62 | - license: AGPL 3.0
63 | file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
64 | formats:
65 | - deb
66 | - rpm
67 | maintainer: lz1998
68 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.17-alpine AS glc_builder
2 |
3 | RUN go env -w GO111MODULE=auto \
4 | && go env -w CGO_ENABLED=0 \
5 | && go env -w GOPROXY="https://goproxy.io,direct" \
6 | && mkdir /build
7 |
8 | WORKDIR /build
9 |
10 | COPY ./ .
11 |
12 | RUN cd /build \
13 | && go build -ldflags "-s -w -extldflags '-static'" -o glc ./service/glc
14 |
15 | FROM alpine:latest
16 |
17 | WORKDIR /data
18 |
19 | COPY --from=glc_builder /build/glc /usr/bin/glc
20 | RUN chmod +x /usr/bin/glc
21 |
22 | ADD ./scripts/env_run.sh /data/
23 |
24 | RUN chmod +x /data/env_run.sh
25 | EXPOSE 9000
26 | CMD /data/env_run.sh
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go-Lagrange-Client
2 |
3 | # Go-Mirai-Client 已经停更,新项目已迁移至 [Go-Lagrange-Client](https://github.com/2mf8/Go-Lagrange-Client)
4 |
5 | ## 默认端口9000,若启动失败,将依次在9001-9020端口中选择可用的端口启动。
6 |
7 | 用于收发QQ消息,并通过 websocket + protobuf 或 websocket + json 上报给 server 进行处理。
8 |
9 | 已支持与 `OneBot V11` 协议的服务端通信, 使用前需要选用 OneBot V11 协议
10 |
11 | Golang 推荐使用 [GoneBot](https://github.com/2mf8/GoneBot)
12 | TypeScript / JavaScript 推荐使用 [ToneBot](https://github.com/2mf8/ToneBot)
13 |
14 | 可以使用任意语言编写websocket server实现通信,协议:[onebot_glc](https://github.com/2mf8/onebot_glc)
15 |
16 | 有问题发issue,或者进QQ群 `901125207`
17 |
18 | 支持的开发语言(需要根据协议修改):[Java/Kotlin](https://github.com/protobufbot/spring-mirai-server) , [JavaScript](https://github.com/2mf8/TSPbBot) , [TypeScript](https://github.com/2mf8/TSPbBot/blob/master/src/demo/index.ts) , [Python](https://github.com/PHIKN1GHT/pypbbot/tree/main/pypbbot_examples) , [Golang](https://github.com/2mf8/GoPbBot/blob/master/test/bot_test.go) , [C/C++](https://github.com/ProtobufBot/cpp-pbbot/blob/main/src/event_handler/event_handler.cpp) , [易语言](https://github.com/protobufbot/pbbot_e_sdk) 。详情查看 [Protobufbot](https://github.com/ProtobufBot/ProtobufBot) 。
19 |
20 | ## 使用说明
21 |
22 | 1. 启动程序
23 | - 用户在 [Releases](https://github.com/ProtobufBot/Go-Mirai-Client/releases) 下载适合自己的版本运行,然后手动打开浏览器地址`http://localhost:9000/`,Linux服务器可以远程访问`http://<服务器地址>:9000`。
24 |
25 | 2. 创建机器人
26 | - 建议选择扫码创建,使用**机器人账号**直接扫码,点击确认后登录。
27 | - 每次登录**必须**使用相同随机种子(数字),方便后续 `session` 登录。(建议使用账号作为随机种子)
28 |
29 | 3. 配置消息处理器
30 | - 在首次启动自动生成的`default.json`中配置服务器URL,修改后重启生效。
31 | - 如果使用其他人编写的程序,建议把`default.json`打包在一起发送给用户。
32 |
33 | ## 多插件支持
34 |
35 | 支持多插件,且每个插件URL可以配置多个作为候选项
36 |
37 | cd service/glc && go run *.go
38 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | NAME="Go-Lagrange-Client"
4 | OUTPUT_DIR="output"
5 | RELEASE_DIR="releases"
6 |
7 | rm -rf $OUTPUT_DIR
8 | mkdir -p $OUTPUT_DIR
9 | mkdir -p $RELEASE_DIR
10 |
11 | PLATFORMS="darwin/amd64" # amd64 only as of go1.5
12 | PLATFORMS="$PLATFORMS windows/amd64 windows/386" # arm compilation not available for Windows
13 | PLATFORMS="$PLATFORMS linux/amd64 linux/386"
14 | PLATFORMS="$PLATFORMS linux/ppc64 linux/ppc64le"
15 | PLATFORMS="$PLATFORMS linux/mips64 linux/mips64le" # experimental in go1.6
16 | PLATFORMS="$PLATFORMS freebsd/amd64"
17 | PLATFORMS="$PLATFORMS netbsd/amd64" # amd64 only as of go1.6
18 | PLATFORMS="$PLATFORMS openbsd/amd64" # amd64 only as of go1.6
19 | PLATFORMS="$PLATFORMS dragonfly/amd64" # amd64 only as of go1.5
20 | PLATFORMS="$PLATFORMS plan9/amd64 plan9/386" # as of go1.4
21 | PLATFORMS="$PLATFORMS solaris/amd64" # as of go1.3
22 | PLATFORMS="$PLATFORMS linux/arm"
23 | PLATFORMS="$PLATFORMS linux/arm64"
24 |
25 | for PLATFORM in $PLATFORMS; do
26 | GOOS=${PLATFORM%/*}
27 | GOARCH=${PLATFORM#*/}
28 | BIN_FILENAME="${OUTPUT_DIR}/${NAME}-${GOOS}-${GOARCH}"
29 | if [[ "${GOOS}" == "windows" ]]; then BIN_FILENAME="${BIN_FILENAME}.exe"; fi
30 | CMD="GOOS=${GOOS} GOARCH=${GOARCH} go build -v -ldflags \"-s -w -extldflags '-static'\" -o ${BIN_FILENAME} github.com/2mf8/Go-Lagrange-Client/service/glc"
31 | echo $CMD
32 | eval $CMD || FAILURES="${FAILURES} ${PLATFORM}"
33 | done
34 |
35 | # ARM builds
36 | PLATFORMS_ARM="linux freebsd netbsd"
37 | for GOOS in $PLATFORMS_ARM; do
38 | GOARCH="arm"
39 | # build for each ARM version
40 | for GOARM in 7 6 5; do
41 | BIN_FILENAME="${OUTPUT_DIR}/${NAME}-${GOOS}-${GOARCH}${GOARM}"
42 | CMD="GOARM=${GOARM} GOOS=${GOOS} GOARCH=${GOARCH} go build -v -ldflags \"-s -w -extldflags '-static'\" -o ${BIN_FILENAME}"
43 | echo "${CMD}"
44 | eval "${CMD}" || FAILURES="${FAILURES} ${GOOS}/${GOARCH}${GOARM}"
45 | done
46 | done
47 |
48 | #cp application.yml "${OUTPUT_DIR}/application.yml"
49 | cp ./scripts/* "${OUTPUT_DIR}/" # 复制运行脚本
50 |
51 | echo "可以把不要的系统删掉">"${OUTPUT_DIR}/可以把不要的系统删掉"
52 |
53 | if [ -n "$1" ]; then
54 | rm "$RELEASE_DIR/gmc-$1.zip"
55 | CMD="zip -r $RELEASE_DIR/gmc-$1.zip $OUTPUT_DIR -x \".*\" -x \"__MACOSX\""
56 | echo $CMD
57 | eval $CMD
58 | fi
--------------------------------------------------------------------------------
/build_android.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | gomobile bind -target=android ./service/gmc_android
--------------------------------------------------------------------------------
/build_proto.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | protoc -I onebot_idl --gofast_out=proto_gen/onebot onebot_idl/*.proto
4 | protoc -I dto_proto --gofast_out=proto_gen/dto dto_proto/*.proto
--------------------------------------------------------------------------------
/dto_proto/http_dto.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | package dto;
3 |
4 |
5 | message Bot{
6 | int64 bot_id = 1;
7 | bool is_online = 2;
8 | Captcha captcha = 3;
9 | message Captcha{
10 | int64 bot_id = 1;
11 | CaptchaType captcha_type = 2;
12 | oneof data{
13 | string url = 3;
14 | bytes image = 4;
15 | }
16 | enum CaptchaType{
17 | PIC_CAPTCHA = 0;
18 | SLIDER_CAPTCHA = 1;
19 | UNSAFE_DEVICE_LOGIN_VERIFY = 2;
20 | SMS = 4;
21 | }
22 | }
23 | }
24 |
25 | // 创建机器人 /bot/create/v1/
26 | message CreateBotReq{
27 | int64 bot_id = 1;
28 | string password = 2;
29 | int64 device_seed = 3; // 设备信息随机种子
30 | int32 client_protocol = 4; // 协议类型
31 | string sign_server = 5;
32 | string sign_server_auth = 6;
33 | }
34 | message CreateBotResp{
35 | }
36 |
37 | // 删除机器人 /bot/delete/v1/
38 | message DeleteBotReq{
39 | int64 bot_id = 1;
40 | }
41 | message DeleteBotResp{
42 | }
43 |
44 | // 查询机器人 /bot/list/v1/
45 | message ListBotReq{
46 | }
47 | message ListBotResp{
48 | repeated Bot bot_list = 1;
49 | }
50 |
51 | // 处理验证码 /captcha/solve/v1/
52 | message SolveCaptchaReq{
53 | int64 bot_id = 1;
54 | string result = 2;
55 | }
56 | message SolveCaptchaResp{
57 | }
58 |
59 | // 获取二维码 /qrcode/fetch/v1/
60 | message FetchQRCodeReq{
61 | int64 device_seed = 1;
62 | int32 client_protocol = 2; // 协议类型
63 | }
64 | // 查询二维码状态 /qrcode/query/v1/
65 | message QueryQRCodeStatusReq{
66 | bytes sig = 1;
67 | int64 bot_id = 2;
68 | }
69 | // 二维码登陆响应(获取和查询统一)
70 | message QRCodeLoginResp{
71 | QRCodeLoginState state = 1;
72 | bytes image_data = 2;
73 | bytes sig = 3;
74 | enum QRCodeLoginState{
75 | Unknown = 0;
76 | QRCodeImageFetch = 1;
77 | QRCodeWaitingForScan = 2; // 等待扫描
78 | QRCodeWaitingForConfirm = 3; // 扫码成功,请确认登陆
79 | QRCodeTimeout = 4; // 二维码过期
80 | QRCodeConfirmed = 5; // 已确认登陆
81 | QRCodeCanceled = 6; // 扫码被用户取消
82 | }
83 | }
84 |
85 |
86 | message Plugin{
87 | string name = 1;
88 | bool disabled = 2;
89 | bool json = 3;
90 | repeated string urls = 4;
91 | repeated int32 event_filter = 5;
92 | repeated int32 api_filter = 6;
93 | string regex_filter = 7;
94 | string regex_replace = 8;
95 | repeated Header extra_header = 9;
96 | message Header{
97 | string key = 1;
98 | repeated string value = 2;
99 | }
100 | }
101 |
102 | message ListPluginReq{
103 | }
104 | message ListPluginResp{
105 | repeated Plugin plugins = 1;
106 | }
107 |
108 | message SavePluginReq{
109 | Plugin plugin = 1;
110 | }
111 | message SavePluginResp{
112 | }
113 |
114 | message DeletePluginReq{
115 | string name = 1;
116 | }
117 | message DeletePluginResp{
118 | }
119 |
120 |
121 | service HttpService{
122 | rpc CreateBot(CreateBotReq)returns (CreateBotResp);
123 | rpc DeleteBot(DeleteBotReq)returns (DeleteBotResp);
124 | rpc ListBot(ListBotReq)returns (ListBotResp);
125 | rpc SolveCaptcha(SolveCaptchaReq)returns (SolveCaptchaResp);
126 | rpc FetchQRCode(FetchQRCodeReq)returns (QRCodeLoginResp);
127 | rpc QueryQRCodeStatus(QueryQRCodeStatusReq)returns (QRCodeLoginResp);
128 |
129 | rpc ListPlugin(ListPluginReq)returns (ListPluginResp);
130 | rpc SavePlugin(SavePluginReq)returns (SavePluginResp);
131 | rpc DeletePlugin(DeletePluginReq)returns (DeletePluginResp);
132 | }
133 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/2mf8/Go-Lagrange-Client
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/2mf8/LagrangeGo v0.1.0
7 | github.com/BurntSushi/toml v1.3.2
8 | github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72
9 | github.com/gin-gonic/gin v1.9.1
10 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
11 | github.com/golang/protobuf v1.5.4
12 | github.com/gorilla/websocket v1.5.1
13 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
14 | github.com/pkg/errors v0.9.1
15 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
16 | github.com/sirupsen/logrus v1.9.3
17 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
18 | github.com/tidwall/gjson v1.17.1
19 | golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed
20 | google.golang.org/grpc v1.63.2
21 | google.golang.org/protobuf v1.33.0
22 | )
23 |
24 | require (
25 | github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d // indirect
26 | github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 // indirect
27 | github.com/bytedance/sonic v1.11.5 // indirect
28 | github.com/bytedance/sonic/loader v0.1.1 // indirect
29 | github.com/cloudwego/base64x v0.1.3 // indirect
30 | github.com/cloudwego/iasm v0.2.0 // indirect
31 | github.com/fumiama/gofastTEA v0.0.10 // indirect
32 | github.com/fumiama/imgsz v0.0.4 // indirect
33 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
34 | github.com/gin-contrib/sse v0.1.0 // indirect
35 | github.com/go-playground/locales v0.14.1 // indirect
36 | github.com/go-playground/universal-translator v0.18.1 // indirect
37 | github.com/go-playground/validator/v10 v10.19.0 // indirect
38 | github.com/goccy/go-json v0.10.2 // indirect
39 | github.com/jonboulle/clockwork v0.4.0 // indirect
40 | github.com/json-iterator/go v1.1.12 // indirect
41 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
42 | github.com/leodido/go-urn v1.4.0 // indirect
43 | github.com/lestrrat-go/strftime v1.0.6 // indirect
44 | github.com/mattn/go-isatty v0.0.20 // indirect
45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
46 | github.com/modern-go/reflect2 v1.0.2 // indirect
47 | github.com/pelletier/go-toml/v2 v2.2.1 // indirect
48 | github.com/smartystreets/goconvey v1.8.1 // indirect
49 | github.com/tidwall/match v1.1.1 // indirect
50 | github.com/tidwall/pretty v1.2.1 // indirect
51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
52 | github.com/ugorji/go/codec v1.2.12 // indirect
53 | golang.org/x/arch v0.7.0 // indirect
54 | golang.org/x/crypto v0.23.0 // indirect
55 | golang.org/x/image v0.18.0 // indirect
56 | golang.org/x/mod v0.17.0 // indirect
57 | golang.org/x/net v0.25.0 // indirect
58 | golang.org/x/sync v0.7.0 // indirect
59 | golang.org/x/sys v0.20.0 // indirect
60 | golang.org/x/text v0.16.0 // indirect
61 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
63 | gopkg.in/yaml.v3 v3.0.1 // indirect
64 | )
65 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/2mf8/LagrangeGo v0.1.0 h1:N/42LiFF59EcmKcTHS4uc7gyXZ7URcnKvYrOYFwM6/0=
2 | github.com/2mf8/LagrangeGo v0.1.0/go.mod h1:jNEJMrnBI8ENQTFSbE+dLoEUak/N6SMoIWbOr7fORZo=
3 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
4 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
5 | github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d h1:/Xuj3fIiMY2ls1TwvPKmaqQrtJsPY+c9s+0lOScVHd8=
6 | github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d/go.mod h1:2Ie+hdBFQpQFDHfeklgxoFmQRCE7O+KwFpISeXq7OwA=
7 | github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 h1:S/ferNiehVjNaBMNNBxUjLtVmP/YWD6Yh79RfPv4ehU=
8 | github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
9 | github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k=
10 | github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw=
11 | github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
12 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
13 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
14 | github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg=
15 | github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8=
16 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
17 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21 | github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 h1:0eU/faU2oDIB2BkQVM02hgRLJjGzzUuRf19HUhp0394=
22 | github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
23 | github.com/fumiama/gofastTEA v0.0.10 h1:JJJ+brWD4kie+mmK2TkspDXKzqq0IjXm89aGYfoGhhQ=
24 | github.com/fumiama/gofastTEA v0.0.10/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk=
25 | github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI=
26 | github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E=
27 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
28 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
29 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
30 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
31 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
32 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
33 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
34 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
35 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
36 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
37 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
38 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
39 | github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
40 | github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
41 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
42 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
45 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
46 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
47 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
48 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
50 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
51 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
52 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
53 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
54 | github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
55 | github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
56 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
57 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
58 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
59 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
60 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
61 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
62 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
63 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
64 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
65 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
66 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
67 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
68 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
69 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
70 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
71 | github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
72 | github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
73 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
74 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
75 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
78 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
79 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
80 | github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
81 | github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
82 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
83 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
86 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
87 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
88 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
89 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
90 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
91 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
92 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
93 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
94 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
95 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
96 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
97 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
98 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
99 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
100 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
101 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
102 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
103 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
104 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
105 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
106 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
107 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
108 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
109 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk=
110 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA=
111 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
112 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
113 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
114 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
115 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
116 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
117 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
118 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
119 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
120 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
121 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
122 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
123 | golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
124 | golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
125 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
126 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
127 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
128 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
129 | golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed h1:vZhAhVr5zF1IJaVKTawyTq78WSspLnK53iuMJ1fJgLc=
130 | golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed/go.mod h1:z041I2NhLjANgIfD0XbB2AmUZ8sLUcSgyLaSNGEP50M=
131 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
132 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
133 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
134 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
135 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
136 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
137 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
138 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
139 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
140 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
141 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
142 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
143 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
144 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
145 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
146 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
147 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
148 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
149 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
150 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
151 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
152 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
153 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
154 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
155 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
156 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
157 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
158 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
159 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
160 |
--------------------------------------------------------------------------------
/pkg/bot/api_handler_test.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "math"
5 | "testing"
6 | )
7 |
8 | func TestSplit(t *testing.T) {
9 | text := ""
10 | LIMIT := 3
11 | num := int(math.Ceil(float64(len(text))/float64(LIMIT)))
12 | for i := 0; i < num; i++ {
13 | start := i * LIMIT
14 | end := func() int {
15 | if (i+1)*LIMIT > len(text) {
16 | return len(text)
17 | } else {
18 | return (i + 1) * LIMIT
19 | }
20 | }()
21 | t.Log(text[start:end])
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/bot/bot.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 | )
6 |
7 | //go:generate go run github.com/a8m/syncmap -o "gen_client_map.go" -pkg bot -name ClientMap "map[int64]*client.QQClient"
8 | //go:generate go run github.com/a8m/syncmap -o "gen_token_map.go" -pkg bot -name TokenMap "map[int64][]byte"
9 | var (
10 | Clients ClientMap
11 | LoginTokens TokenMap
12 | EnergyCount = 0
13 | EnergyStop = false
14 | SignCount = 0
15 | SignStop = false
16 | RegisterSignCount = 0
17 | RegisterSignStop = false
18 | SubmitRequestCallbackCount = 0
19 | SubmitRequestCallbackStop = false
20 | RequestTokenCount = 0
21 | RequestTokenStop = false
22 | DestoryInstanceCount = 0
23 | DestoryInstanceStop = false
24 | RSR RequestSignResult
25 | GTL *GMCLogin
26 | SR SignRegister
27 | IsRequestTokenAgain bool = false
28 | TTI_i = 30
29 | )
30 |
31 | type Logger struct {
32 | }
33 |
34 | type GMCLogin struct {
35 | DeviceSeed int64
36 | ClientProtocol int32
37 | SignServer string
38 | SignServerKey string
39 | }
40 |
41 | type SignRegister struct {
42 | Uin uint64
43 | AndroidId string
44 | Guid string
45 | Qimei36 string
46 | Key string
47 | }
48 |
49 | type RequestCallback struct {
50 | Cmd string `json:"cmd,omitempty"` // trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Establish
51 | Body string `json:"body,omitempty"`
52 | CallBackId int `json:"callbackId,omitempty"`
53 | }
54 |
55 | type RequestSignData struct {
56 | Token string `json:"token,omitempty"`
57 | Extra string `json:"extra,omitempty"`
58 | Sign string `json:"sign,omitempty"`
59 | O3dId string `json:"o3did,omitempty"`
60 | RequestCallback []*RequestCallback
61 | }
62 |
63 | type RequestSignResult struct {
64 | Code int `json:"code,omitempty"`
65 | Msg string `json:"msg,omitempty"`
66 | Data *RequestSignData
67 | }
68 |
69 | func (l *Logger) Info(format string, args ...any) {
70 | log.Infof(format, args)
71 | }
72 | func (l *Logger) Warning(format string, args ...any) {
73 | log.Warnf(format, args)
74 | }
75 | func (l *Logger) Error(format string, args ...any) {
76 | log.Errorf(format, args)
77 | }
78 | func (l *Logger) Debug(format string, args ...any) {
79 | log.Debug(format, args)
80 | }
81 | func (l *Logger) Dump(dumped []byte, format string, args ...any) {
82 | }
83 |
84 | func IsClientExist(uin int64) bool {
85 | _, ok := Clients.Load(uin)
86 | return ok
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/bot/bot_test.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "path"
5 | "testing"
6 | )
7 |
8 | func TestInitDevice(t *testing.T) {
9 | println(path.Dir("1.json"))
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/bot/gen_client_map.go:
--------------------------------------------------------------------------------
1 | // Code generated by syncmap; DO NOT EDIT.
2 |
3 | // Copyright 2016 The Go Authors. All rights reserved.
4 | // Use of this source code is governed by a BSD-style
5 | // license that can be found in the LICENSE file.
6 |
7 | package bot
8 |
9 | import (
10 | "sync"
11 | "sync/atomic"
12 | "unsafe"
13 |
14 | "github.com/2mf8/LagrangeGo/client"
15 | )
16 |
17 | // Map is like a Go map[interface{}]interface{} but is safe for concurrent use
18 | // by multiple goroutines without additional locking or coordination.
19 | // Loads, stores, and deletes run in amortized constant time.
20 | //
21 | // The Map type is specialized. Most code should use a plain Go map instead,
22 | // with separate locking or coordination, for better type safety and to make it
23 | // easier to maintain other invariants along with the map content.
24 | //
25 | // The Map type is optimized for two common use cases: (1) when the entry for a given
26 | // key is only ever written once but read many times, as in caches that only grow,
27 | // or (2) when multiple goroutines read, write, and overwrite entries for disjoint
28 | // sets of keys. In these two cases, use of a Map may significantly reduce lock
29 | // contention compared to a Go map paired with a separate Mutex or RWMutex.
30 | //
31 | // The zero Map is empty and ready for use. A Map must not be copied after first use.
32 | type ClientMap struct {
33 | mu sync.Mutex
34 |
35 | // read contains the portion of the map's contents that are safe for
36 | // concurrent access (with or without mu held).
37 | //
38 | // The read field itself is always safe to load, but must only be stored with
39 | // mu held.
40 | //
41 | // Entries stored in read may be updated concurrently without mu, but updating
42 | // a previously-expunged entry requires that the entry be copied to the dirty
43 | // map and unexpunged with mu held.
44 | read atomic.Value // readOnly
45 |
46 | // dirty contains the portion of the map's contents that require mu to be
47 | // held. To ensure that the dirty map can be promoted to the read map quickly,
48 | // it also includes all of the non-expunged entries in the read map.
49 | //
50 | // Expunged entries are not stored in the dirty map. An expunged entry in the
51 | // clean map must be unexpunged and added to the dirty map before a new value
52 | // can be stored to it.
53 | //
54 | // If the dirty map is nil, the next write to the map will initialize it by
55 | // making a shallow copy of the clean map, omitting stale entries.
56 | dirty map[int64]*entryClientMap
57 |
58 | // misses counts the number of loads since the read map was last updated that
59 | // needed to lock mu to determine whether the key was present.
60 | //
61 | // Once enough misses have occurred to cover the cost of copying the dirty
62 | // map, the dirty map will be promoted to the read map (in the unamended
63 | // state) and the next store to the map will make a new dirty copy.
64 | misses int
65 | }
66 |
67 | // readOnly is an immutable struct stored atomically in the Map.read field.
68 | type readOnlyClientMap struct {
69 | m map[int64]*entryClientMap
70 | amended bool // true if the dirty map contains some key not in m.
71 | }
72 |
73 | // expunged is an arbitrary pointer that marks entries which have been deleted
74 | // from the dirty map.
75 | var expungedClientMap = unsafe.Pointer(new(*client.QQClient))
76 |
77 | // An entry is a slot in the map corresponding to a particular key.
78 | type entryClientMap struct {
79 | // p points to the interface{} value stored for the entry.
80 | //
81 | // If p == nil, the entry has been deleted and m.dirty == nil.
82 | //
83 | // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
84 | // is missing from m.dirty.
85 | //
86 | // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
87 | // != nil, in m.dirty[key].
88 | //
89 | // An entry can be deleted by atomic replacement with nil: when m.dirty is
90 | // next created, it will atomically replace nil with expunged and leave
91 | // m.dirty[key] unset.
92 | //
93 | // An entry's associated value can be updated by atomic replacement, provided
94 | // p != expunged. If p == expunged, an entry's associated value can be updated
95 | // only after first setting m.dirty[key] = e so that lookups using the dirty
96 | // map find the entry.
97 | p unsafe.Pointer // *interface{}
98 | }
99 |
100 | func newEntryClientMap(i *client.QQClient) *entryClientMap {
101 | return &entryClientMap{p: unsafe.Pointer(&i)}
102 | }
103 |
104 | // Load returns the value stored in the map for a key, or nil if no
105 | // value is present.
106 | // The ok result indicates whether value was found in the map.
107 | func (m *ClientMap) Load(key int64) (value *client.QQClient, ok bool) {
108 | read, _ := m.read.Load().(readOnlyClientMap)
109 | e, ok := read.m[key]
110 | if !ok && read.amended {
111 | m.mu.Lock()
112 | // Avoid reporting a spurious miss if m.dirty got promoted while we were
113 | // blocked on m.mu. (If further loads of the same key will not miss, it's
114 | // not worth copying the dirty map for this key.)
115 | read, _ = m.read.Load().(readOnlyClientMap)
116 | e, ok = read.m[key]
117 | if !ok && read.amended {
118 | e, ok = m.dirty[key]
119 | // Regardless of whether the entry was present, record a miss: this key
120 | // will take the slow path until the dirty map is promoted to the read
121 | // map.
122 | m.missLocked()
123 | }
124 | m.mu.Unlock()
125 | }
126 | if !ok {
127 | return value, false
128 | }
129 | return e.load()
130 | }
131 |
132 | func (e *entryClientMap) load() (value *client.QQClient, ok bool) {
133 | p := atomic.LoadPointer(&e.p)
134 | if p == nil || p == expungedClientMap {
135 | return value, false
136 | }
137 | return *(**client.QQClient)(p), true
138 | }
139 |
140 | // Store sets the value for a key.
141 | func (m *ClientMap) Store(key int64, value *client.QQClient) {
142 | read, _ := m.read.Load().(readOnlyClientMap)
143 | if e, ok := read.m[key]; ok && e.tryStore(&value) {
144 | return
145 | }
146 |
147 | m.mu.Lock()
148 | read, _ = m.read.Load().(readOnlyClientMap)
149 | if e, ok := read.m[key]; ok {
150 | if e.unexpungeLocked() {
151 | // The entry was previously expunged, which implies that there is a
152 | // non-nil dirty map and this entry is not in it.
153 | m.dirty[key] = e
154 | }
155 | e.storeLocked(&value)
156 | } else if e, ok := m.dirty[key]; ok {
157 | e.storeLocked(&value)
158 | } else {
159 | if !read.amended {
160 | // We're adding the first new key to the dirty map.
161 | // Make sure it is allocated and mark the read-only map as incomplete.
162 | m.dirtyLocked()
163 | m.read.Store(readOnlyClientMap{m: read.m, amended: true})
164 | }
165 | m.dirty[key] = newEntryClientMap(value)
166 | }
167 | m.mu.Unlock()
168 | }
169 |
170 | // tryStore stores a value if the entry has not been expunged.
171 | //
172 | // If the entry is expunged, tryStore returns false and leaves the entry
173 | // unchanged.
174 | func (e *entryClientMap) tryStore(i **client.QQClient) bool {
175 | for {
176 | p := atomic.LoadPointer(&e.p)
177 | if p == expungedClientMap {
178 | return false
179 | }
180 | if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
181 | return true
182 | }
183 | }
184 | }
185 |
186 | // unexpungeLocked ensures that the entry is not marked as expunged.
187 | //
188 | // If the entry was previously expunged, it must be added to the dirty map
189 | // before m.mu is unlocked.
190 | func (e *entryClientMap) unexpungeLocked() (wasExpunged bool) {
191 | return atomic.CompareAndSwapPointer(&e.p, expungedClientMap, nil)
192 | }
193 |
194 | // storeLocked unconditionally stores a value to the entry.
195 | //
196 | // The entry must be known not to be expunged.
197 | func (e *entryClientMap) storeLocked(i **client.QQClient) {
198 | atomic.StorePointer(&e.p, unsafe.Pointer(i))
199 | }
200 |
201 | // LoadOrStore returns the existing value for the key if present.
202 | // Otherwise, it stores and returns the given value.
203 | // The loaded result is true if the value was loaded, false if stored.
204 | func (m *ClientMap) LoadOrStore(key int64, value *client.QQClient) (actual *client.QQClient, loaded bool) {
205 | // Avoid locking if it's a clean hit.
206 | read, _ := m.read.Load().(readOnlyClientMap)
207 | if e, ok := read.m[key]; ok {
208 | actual, loaded, ok := e.tryLoadOrStore(value)
209 | if ok {
210 | return actual, loaded
211 | }
212 | }
213 |
214 | m.mu.Lock()
215 | read, _ = m.read.Load().(readOnlyClientMap)
216 | if e, ok := read.m[key]; ok {
217 | if e.unexpungeLocked() {
218 | m.dirty[key] = e
219 | }
220 | actual, loaded, _ = e.tryLoadOrStore(value)
221 | } else if e, ok := m.dirty[key]; ok {
222 | actual, loaded, _ = e.tryLoadOrStore(value)
223 | m.missLocked()
224 | } else {
225 | if !read.amended {
226 | // We're adding the first new key to the dirty map.
227 | // Make sure it is allocated and mark the read-only map as incomplete.
228 | m.dirtyLocked()
229 | m.read.Store(readOnlyClientMap{m: read.m, amended: true})
230 | }
231 | m.dirty[key] = newEntryClientMap(value)
232 | actual, loaded = value, false
233 | }
234 | m.mu.Unlock()
235 |
236 | return actual, loaded
237 | }
238 |
239 | // tryLoadOrStore atomically loads or stores a value if the entry is not
240 | // expunged.
241 | //
242 | // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
243 | // returns with ok==false.
244 | func (e *entryClientMap) tryLoadOrStore(i *client.QQClient) (actual *client.QQClient, loaded, ok bool) {
245 | p := atomic.LoadPointer(&e.p)
246 | if p == expungedClientMap {
247 | return actual, false, false
248 | }
249 | if p != nil {
250 | return *(**client.QQClient)(p), true, true
251 | }
252 |
253 | // Copy the interface after the first load to make this method more amenable
254 | // to escape analysis: if we hit the "load" path or the entry is expunged, we
255 | // shouldn't bother heap-allocating.
256 | ic := i
257 | for {
258 | if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
259 | return i, false, true
260 | }
261 | p = atomic.LoadPointer(&e.p)
262 | if p == expungedClientMap {
263 | return actual, false, false
264 | }
265 | if p != nil {
266 | return *(**client.QQClient)(p), true, true
267 | }
268 | }
269 | }
270 |
271 | // LoadAndDelete deletes the value for a key, returning the previous value if any.
272 | // The loaded result reports whether the key was present.
273 | func (m *ClientMap) LoadAndDelete(key int64) (value *client.QQClient, loaded bool) {
274 | read, _ := m.read.Load().(readOnlyClientMap)
275 | e, ok := read.m[key]
276 | if !ok && read.amended {
277 | m.mu.Lock()
278 | read, _ = m.read.Load().(readOnlyClientMap)
279 | e, ok = read.m[key]
280 | if !ok && read.amended {
281 | e, ok = m.dirty[key]
282 | delete(m.dirty, key)
283 | // Regardless of whether the entry was present, record a miss: this key
284 | // will take the slow path until the dirty map is promoted to the read
285 | // map.
286 | m.missLocked()
287 | }
288 | m.mu.Unlock()
289 | }
290 | if ok {
291 | return e.delete()
292 | }
293 | return value, false
294 | }
295 |
296 | // Delete deletes the value for a key.
297 | func (m *ClientMap) Delete(key int64) {
298 | m.LoadAndDelete(key)
299 | }
300 |
301 | func (e *entryClientMap) delete() (value *client.QQClient, ok bool) {
302 | for {
303 | p := atomic.LoadPointer(&e.p)
304 | if p == nil || p == expungedClientMap {
305 | return value, false
306 | }
307 | if atomic.CompareAndSwapPointer(&e.p, p, nil) {
308 | return *(**client.QQClient)(p), true
309 | }
310 | }
311 | }
312 |
313 | // Range calls f sequentially for each key and value present in the map.
314 | // If f returns false, range stops the iteration.
315 | //
316 | // Range does not necessarily correspond to any consistent snapshot of the Map's
317 | // contents: no key will be visited more than once, but if the value for any key
318 | // is stored or deleted concurrently, Range may reflect any mapping for that key
319 | // from any point during the Range call.
320 | //
321 | // Range may be O(N) with the number of elements in the map even if f returns
322 | // false after a constant number of calls.
323 | func (m *ClientMap) Range(f func(key int64, value *client.QQClient) bool) {
324 | // We need to be able to iterate over all of the keys that were already
325 | // present at the start of the call to Range.
326 | // If read.amended is false, then read.m satisfies that property without
327 | // requiring us to hold m.mu for a long time.
328 | read, _ := m.read.Load().(readOnlyClientMap)
329 | if read.amended {
330 | // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
331 | // (assuming the caller does not break out early), so a call to Range
332 | // amortizes an entire copy of the map: we can promote the dirty copy
333 | // immediately!
334 | m.mu.Lock()
335 | read, _ = m.read.Load().(readOnlyClientMap)
336 | if read.amended {
337 | read = readOnlyClientMap{m: m.dirty}
338 | m.read.Store(read)
339 | m.dirty = nil
340 | m.misses = 0
341 | }
342 | m.mu.Unlock()
343 | }
344 |
345 | for k, e := range read.m {
346 | v, ok := e.load()
347 | if !ok {
348 | continue
349 | }
350 | if !f(k, v) {
351 | break
352 | }
353 | }
354 | }
355 |
356 | func (m *ClientMap) missLocked() {
357 | m.misses++
358 | if m.misses < len(m.dirty) {
359 | return
360 | }
361 | m.read.Store(readOnlyClientMap{m: m.dirty})
362 | m.dirty = nil
363 | m.misses = 0
364 | }
365 |
366 | func (m *ClientMap) dirtyLocked() {
367 | if m.dirty != nil {
368 | return
369 | }
370 |
371 | read, _ := m.read.Load().(readOnlyClientMap)
372 | m.dirty = make(map[int64]*entryClientMap, len(read.m))
373 | for k, e := range read.m {
374 | if !e.tryExpungeLocked() {
375 | m.dirty[k] = e
376 | }
377 | }
378 | }
379 |
380 | func (e *entryClientMap) tryExpungeLocked() (isExpunged bool) {
381 | p := atomic.LoadPointer(&e.p)
382 | for p == nil {
383 | if atomic.CompareAndSwapPointer(&e.p, nil, expungedClientMap) {
384 | return true
385 | }
386 | p = atomic.LoadPointer(&e.p)
387 | }
388 | return p == expungedClientMap
389 | }
390 |
--------------------------------------------------------------------------------
/pkg/bot/gen_remote_map.go:
--------------------------------------------------------------------------------
1 | // Code generated by syncmap; DO NOT EDIT.
2 |
3 | // Copyright 2016 The Go Authors. All rights reserved.
4 | // Use of this source code is governed by a BSD-style
5 | // license that can be found in the LICENSE file.
6 |
7 | package bot
8 |
9 | import (
10 | "sync"
11 | "sync/atomic"
12 | "unsafe"
13 | )
14 |
15 | // Map is like a Go map[interface{}]interface{} but is safe for concurrent use
16 | // by multiple goroutines without additional locking or coordination.
17 | // Loads, stores, and deletes run in amortized constant time.
18 | //
19 | // The Map type is specialized. Most code should use a plain Go map instead,
20 | // with separate locking or coordination, for better type safety and to make it
21 | // easier to maintain other invariants along with the map content.
22 | //
23 | // The Map type is optimized for two common use cases: (1) when the entry for a given
24 | // key is only ever written once but read many times, as in caches that only grow,
25 | // or (2) when multiple goroutines read, write, and overwrite entries for disjoint
26 | // sets of keys. In these two cases, use of a Map may significantly reduce lock
27 | // contention compared to a Go map paired with a separate Mutex or RWMutex.
28 | //
29 | // The zero Map is empty and ready for use. A Map must not be copied after first use.
30 | type RemoteMap struct {
31 | mu sync.Mutex
32 |
33 | // read contains the portion of the map's contents that are safe for
34 | // concurrent access (with or without mu held).
35 | //
36 | // The read field itself is always safe to load, but must only be stored with
37 | // mu held.
38 | //
39 | // Entries stored in read may be updated concurrently without mu, but updating
40 | // a previously-expunged entry requires that the entry be copied to the dirty
41 | // map and unexpunged with mu held.
42 | read atomic.Value // readOnly
43 |
44 | // dirty contains the portion of the map's contents that require mu to be
45 | // held. To ensure that the dirty map can be promoted to the read map quickly,
46 | // it also includes all of the non-expunged entries in the read map.
47 | //
48 | // Expunged entries are not stored in the dirty map. An expunged entry in the
49 | // clean map must be unexpunged and added to the dirty map before a new value
50 | // can be stored to it.
51 | //
52 | // If the dirty map is nil, the next write to the map will initialize it by
53 | // making a shallow copy of the clean map, omitting stale entries.
54 | dirty map[int64]*entryRemoteMap
55 |
56 | // misses counts the number of loads since the read map was last updated that
57 | // needed to lock mu to determine whether the key was present.
58 | //
59 | // Once enough misses have occurred to cover the cost of copying the dirty
60 | // map, the dirty map will be promoted to the read map (in the unamended
61 | // state) and the next store to the map will make a new dirty copy.
62 | misses int
63 | }
64 |
65 | // readOnly is an immutable struct stored atomically in the Map.read field.
66 | type readOnlyRemoteMap struct {
67 | m map[int64]*entryRemoteMap
68 | amended bool // true if the dirty map contains some key not in m.
69 | }
70 |
71 | // expunged is an arbitrary pointer that marks entries which have been deleted
72 | // from the dirty map.
73 | var expungedRemoteMap = unsafe.Pointer(new(map[string]*WsServer))
74 |
75 | // An entry is a slot in the map corresponding to a particular key.
76 | type entryRemoteMap struct {
77 | // p points to the interface{} value stored for the entry.
78 | //
79 | // If p == nil, the entry has been deleted and m.dirty == nil.
80 | //
81 | // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
82 | // is missing from m.dirty.
83 | //
84 | // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
85 | // != nil, in m.dirty[key].
86 | //
87 | // An entry can be deleted by atomic replacement with nil: when m.dirty is
88 | // next created, it will atomically replace nil with expunged and leave
89 | // m.dirty[key] unset.
90 | //
91 | // An entry's associated value can be updated by atomic replacement, provided
92 | // p != expunged. If p == expunged, an entry's associated value can be updated
93 | // only after first setting m.dirty[key] = e so that lookups using the dirty
94 | // map find the entry.
95 | p unsafe.Pointer // *interface{}
96 | }
97 |
98 | func newEntryRemoteMap(i map[string]*WsServer) *entryRemoteMap {
99 | return &entryRemoteMap{p: unsafe.Pointer(&i)}
100 | }
101 |
102 | // Load returns the value stored in the map for a key, or nil if no
103 | // value is present.
104 | // The ok result indicates whether value was found in the map.
105 | func (m *RemoteMap) Load(key int64) (value map[string]*WsServer, ok bool) {
106 | read, _ := m.read.Load().(readOnlyRemoteMap)
107 | e, ok := read.m[key]
108 | if !ok && read.amended {
109 | m.mu.Lock()
110 | // Avoid reporting a spurious miss if m.dirty got promoted while we were
111 | // blocked on m.mu. (If further loads of the same key will not miss, it's
112 | // not worth copying the dirty map for this key.)
113 | read, _ = m.read.Load().(readOnlyRemoteMap)
114 | e, ok = read.m[key]
115 | if !ok && read.amended {
116 | e, ok = m.dirty[key]
117 | // Regardless of whether the entry was present, record a miss: this key
118 | // will take the slow path until the dirty map is promoted to the read
119 | // map.
120 | m.missLocked()
121 | }
122 | m.mu.Unlock()
123 | }
124 | if !ok {
125 | return value, false
126 | }
127 | return e.load()
128 | }
129 |
130 | func (e *entryRemoteMap) load() (value map[string]*WsServer, ok bool) {
131 | p := atomic.LoadPointer(&e.p)
132 | if p == nil || p == expungedRemoteMap {
133 | return value, false
134 | }
135 | return *(*map[string]*WsServer)(p), true
136 | }
137 |
138 | // Store sets the value for a key.
139 | func (m *RemoteMap) Store(key int64, value map[string]*WsServer) {
140 | read, _ := m.read.Load().(readOnlyRemoteMap)
141 | if e, ok := read.m[key]; ok && e.tryStore(&value) {
142 | return
143 | }
144 |
145 | m.mu.Lock()
146 | read, _ = m.read.Load().(readOnlyRemoteMap)
147 | if e, ok := read.m[key]; ok {
148 | if e.unexpungeLocked() {
149 | // The entry was previously expunged, which implies that there is a
150 | // non-nil dirty map and this entry is not in it.
151 | m.dirty[key] = e
152 | }
153 | e.storeLocked(&value)
154 | } else if e, ok := m.dirty[key]; ok {
155 | e.storeLocked(&value)
156 | } else {
157 | if !read.amended {
158 | // We're adding the first new key to the dirty map.
159 | // Make sure it is allocated and mark the read-only map as incomplete.
160 | m.dirtyLocked()
161 | m.read.Store(readOnlyRemoteMap{m: read.m, amended: true})
162 | }
163 | m.dirty[key] = newEntryRemoteMap(value)
164 | }
165 | m.mu.Unlock()
166 | }
167 |
168 | // tryStore stores a value if the entry has not been expunged.
169 | //
170 | // If the entry is expunged, tryStore returns false and leaves the entry
171 | // unchanged.
172 | func (e *entryRemoteMap) tryStore(i *map[string]*WsServer) bool {
173 | for {
174 | p := atomic.LoadPointer(&e.p)
175 | if p == expungedRemoteMap {
176 | return false
177 | }
178 | if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
179 | return true
180 | }
181 | }
182 | }
183 |
184 | // unexpungeLocked ensures that the entry is not marked as expunged.
185 | //
186 | // If the entry was previously expunged, it must be added to the dirty map
187 | // before m.mu is unlocked.
188 | func (e *entryRemoteMap) unexpungeLocked() (wasExpunged bool) {
189 | return atomic.CompareAndSwapPointer(&e.p, expungedRemoteMap, nil)
190 | }
191 |
192 | // storeLocked unconditionally stores a value to the entry.
193 | //
194 | // The entry must be known not to be expunged.
195 | func (e *entryRemoteMap) storeLocked(i *map[string]*WsServer) {
196 | atomic.StorePointer(&e.p, unsafe.Pointer(i))
197 | }
198 |
199 | // LoadOrStore returns the existing value for the key if present.
200 | // Otherwise, it stores and returns the given value.
201 | // The loaded result is true if the value was loaded, false if stored.
202 | func (m *RemoteMap) LoadOrStore(key int64, value map[string]*WsServer) (actual map[string]*WsServer, loaded bool) {
203 | // Avoid locking if it's a clean hit.
204 | read, _ := m.read.Load().(readOnlyRemoteMap)
205 | if e, ok := read.m[key]; ok {
206 | actual, loaded, ok := e.tryLoadOrStore(value)
207 | if ok {
208 | return actual, loaded
209 | }
210 | }
211 |
212 | m.mu.Lock()
213 | read, _ = m.read.Load().(readOnlyRemoteMap)
214 | if e, ok := read.m[key]; ok {
215 | if e.unexpungeLocked() {
216 | m.dirty[key] = e
217 | }
218 | actual, loaded, _ = e.tryLoadOrStore(value)
219 | } else if e, ok := m.dirty[key]; ok {
220 | actual, loaded, _ = e.tryLoadOrStore(value)
221 | m.missLocked()
222 | } else {
223 | if !read.amended {
224 | // We're adding the first new key to the dirty map.
225 | // Make sure it is allocated and mark the read-only map as incomplete.
226 | m.dirtyLocked()
227 | m.read.Store(readOnlyRemoteMap{m: read.m, amended: true})
228 | }
229 | m.dirty[key] = newEntryRemoteMap(value)
230 | actual, loaded = value, false
231 | }
232 | m.mu.Unlock()
233 |
234 | return actual, loaded
235 | }
236 |
237 | // tryLoadOrStore atomically loads or stores a value if the entry is not
238 | // expunged.
239 | //
240 | // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
241 | // returns with ok==false.
242 | func (e *entryRemoteMap) tryLoadOrStore(i map[string]*WsServer) (actual map[string]*WsServer, loaded, ok bool) {
243 | p := atomic.LoadPointer(&e.p)
244 | if p == expungedRemoteMap {
245 | return actual, false, false
246 | }
247 | if p != nil {
248 | return *(*map[string]*WsServer)(p), true, true
249 | }
250 |
251 | // Copy the interface after the first load to make this method more amenable
252 | // to escape analysis: if we hit the "load" path or the entry is expunged, we
253 | // shouldn't bother heap-allocating.
254 | ic := i
255 | for {
256 | if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
257 | return i, false, true
258 | }
259 | p = atomic.LoadPointer(&e.p)
260 | if p == expungedRemoteMap {
261 | return actual, false, false
262 | }
263 | if p != nil {
264 | return *(*map[string]*WsServer)(p), true, true
265 | }
266 | }
267 | }
268 |
269 | // LoadAndDelete deletes the value for a key, returning the previous value if any.
270 | // The loaded result reports whether the key was present.
271 | func (m *RemoteMap) LoadAndDelete(key int64) (value map[string]*WsServer, loaded bool) {
272 | read, _ := m.read.Load().(readOnlyRemoteMap)
273 | e, ok := read.m[key]
274 | if !ok && read.amended {
275 | m.mu.Lock()
276 | read, _ = m.read.Load().(readOnlyRemoteMap)
277 | e, ok = read.m[key]
278 | if !ok && read.amended {
279 | e, ok = m.dirty[key]
280 | delete(m.dirty, key)
281 | // Regardless of whether the entry was present, record a miss: this key
282 | // will take the slow path until the dirty map is promoted to the read
283 | // map.
284 | m.missLocked()
285 | }
286 | m.mu.Unlock()
287 | }
288 | if ok {
289 | return e.delete()
290 | }
291 | return value, false
292 | }
293 |
294 | // Delete deletes the value for a key.
295 | func (m *RemoteMap) Delete(key int64) {
296 | m.LoadAndDelete(key)
297 | }
298 |
299 | func (e *entryRemoteMap) delete() (value map[string]*WsServer, ok bool) {
300 | for {
301 | p := atomic.LoadPointer(&e.p)
302 | if p == nil || p == expungedRemoteMap {
303 | return value, false
304 | }
305 | if atomic.CompareAndSwapPointer(&e.p, p, nil) {
306 | return *(*map[string]*WsServer)(p), true
307 | }
308 | }
309 | }
310 |
311 | // Range calls f sequentially for each key and value present in the map.
312 | // If f returns false, range stops the iteration.
313 | //
314 | // Range does not necessarily correspond to any consistent snapshot of the Map's
315 | // contents: no key will be visited more than once, but if the value for any key
316 | // is stored or deleted concurrently, Range may reflect any mapping for that key
317 | // from any point during the Range call.
318 | //
319 | // Range may be O(N) with the number of elements in the map even if f returns
320 | // false after a constant number of calls.
321 | func (m *RemoteMap) Range(f func(key int64, value map[string]*WsServer) bool) {
322 | // We need to be able to iterate over all of the keys that were already
323 | // present at the start of the call to Range.
324 | // If read.amended is false, then read.m satisfies that property without
325 | // requiring us to hold m.mu for a long time.
326 | read, _ := m.read.Load().(readOnlyRemoteMap)
327 | if read.amended {
328 | // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
329 | // (assuming the caller does not break out early), so a call to Range
330 | // amortizes an entire copy of the map: we can promote the dirty copy
331 | // immediately!
332 | m.mu.Lock()
333 | read, _ = m.read.Load().(readOnlyRemoteMap)
334 | if read.amended {
335 | read = readOnlyRemoteMap{m: m.dirty}
336 | m.read.Store(read)
337 | m.dirty = nil
338 | m.misses = 0
339 | }
340 | m.mu.Unlock()
341 | }
342 |
343 | for k, e := range read.m {
344 | v, ok := e.load()
345 | if !ok {
346 | continue
347 | }
348 | if !f(k, v) {
349 | break
350 | }
351 | }
352 | }
353 |
354 | func (m *RemoteMap) missLocked() {
355 | m.misses++
356 | if m.misses < len(m.dirty) {
357 | return
358 | }
359 | m.read.Store(readOnlyRemoteMap{m: m.dirty})
360 | m.dirty = nil
361 | m.misses = 0
362 | }
363 |
364 | func (m *RemoteMap) dirtyLocked() {
365 | if m.dirty != nil {
366 | return
367 | }
368 |
369 | read, _ := m.read.Load().(readOnlyRemoteMap)
370 | m.dirty = make(map[int64]*entryRemoteMap, len(read.m))
371 | for k, e := range read.m {
372 | if !e.tryExpungeLocked() {
373 | m.dirty[k] = e
374 | }
375 | }
376 | }
377 |
378 | func (e *entryRemoteMap) tryExpungeLocked() (isExpunged bool) {
379 | p := atomic.LoadPointer(&e.p)
380 | for p == nil {
381 | if atomic.CompareAndSwapPointer(&e.p, nil, expungedRemoteMap) {
382 | return true
383 | }
384 | p = atomic.LoadPointer(&e.p)
385 | }
386 | return p == expungedRemoteMap
387 | }
388 |
--------------------------------------------------------------------------------
/pkg/bot/gen_token_map.go:
--------------------------------------------------------------------------------
1 | // Code generated by syncmap; DO NOT EDIT.
2 |
3 | // Copyright 2016 The Go Authors. All rights reserved.
4 | // Use of this source code is governed by a BSD-style
5 | // license that can be found in the LICENSE file.
6 |
7 | package bot
8 |
9 | import (
10 | "sync"
11 | "sync/atomic"
12 | "unsafe"
13 | )
14 |
15 | // Map is like a Go map[interface{}]interface{} but is safe for concurrent use
16 | // by multiple goroutines without additional locking or coordination.
17 | // Loads, stores, and deletes run in amortized constant time.
18 | //
19 | // The Map type is specialized. Most code should use a plain Go map instead,
20 | // with separate locking or coordination, for better type safety and to make it
21 | // easier to maintain other invariants along with the map content.
22 | //
23 | // The Map type is optimized for two common use cases: (1) when the entry for a given
24 | // key is only ever written once but read many times, as in caches that only grow,
25 | // or (2) when multiple goroutines read, write, and overwrite entries for disjoint
26 | // sets of keys. In these two cases, use of a Map may significantly reduce lock
27 | // contention compared to a Go map paired with a separate Mutex or RWMutex.
28 | //
29 | // The zero Map is empty and ready for use. A Map must not be copied after first use.
30 | type TokenMap struct {
31 | mu sync.Mutex
32 |
33 | // read contains the portion of the map's contents that are safe for
34 | // concurrent access (with or without mu held).
35 | //
36 | // The read field itself is always safe to load, but must only be stored with
37 | // mu held.
38 | //
39 | // Entries stored in read may be updated concurrently without mu, but updating
40 | // a previously-expunged entry requires that the entry be copied to the dirty
41 | // map and unexpunged with mu held.
42 | read atomic.Value // readOnly
43 |
44 | // dirty contains the portion of the map's contents that require mu to be
45 | // held. To ensure that the dirty map can be promoted to the read map quickly,
46 | // it also includes all of the non-expunged entries in the read map.
47 | //
48 | // Expunged entries are not stored in the dirty map. An expunged entry in the
49 | // clean map must be unexpunged and added to the dirty map before a new value
50 | // can be stored to it.
51 | //
52 | // If the dirty map is nil, the next write to the map will initialize it by
53 | // making a shallow copy of the clean map, omitting stale entries.
54 | dirty map[int64]*entryTokenMap
55 |
56 | // misses counts the number of loads since the read map was last updated that
57 | // needed to lock mu to determine whether the key was present.
58 | //
59 | // Once enough misses have occurred to cover the cost of copying the dirty
60 | // map, the dirty map will be promoted to the read map (in the unamended
61 | // state) and the next store to the map will make a new dirty copy.
62 | misses int
63 | }
64 |
65 | // readOnly is an immutable struct stored atomically in the Map.read field.
66 | type readOnlyTokenMap struct {
67 | m map[int64]*entryTokenMap
68 | amended bool // true if the dirty map contains some key not in m.
69 | }
70 |
71 | // expunged is an arbitrary pointer that marks entries which have been deleted
72 | // from the dirty map.
73 | var expungedTokenMap = unsafe.Pointer(new([]byte))
74 |
75 | // An entry is a slot in the map corresponding to a particular key.
76 | type entryTokenMap struct {
77 | // p points to the interface{} value stored for the entry.
78 | //
79 | // If p == nil, the entry has been deleted and m.dirty == nil.
80 | //
81 | // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
82 | // is missing from m.dirty.
83 | //
84 | // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
85 | // != nil, in m.dirty[key].
86 | //
87 | // An entry can be deleted by atomic replacement with nil: when m.dirty is
88 | // next created, it will atomically replace nil with expunged and leave
89 | // m.dirty[key] unset.
90 | //
91 | // An entry's associated value can be updated by atomic replacement, provided
92 | // p != expunged. If p == expunged, an entry's associated value can be updated
93 | // only after first setting m.dirty[key] = e so that lookups using the dirty
94 | // map find the entry.
95 | p unsafe.Pointer // *interface{}
96 | }
97 |
98 | func newEntryTokenMap(i []byte) *entryTokenMap {
99 | return &entryTokenMap{p: unsafe.Pointer(&i)}
100 | }
101 |
102 | // Load returns the value stored in the map for a key, or nil if no
103 | // value is present.
104 | // The ok result indicates whether value was found in the map.
105 | func (m *TokenMap) Load(key int64) (value []byte, ok bool) {
106 | read, _ := m.read.Load().(readOnlyTokenMap)
107 | e, ok := read.m[key]
108 | if !ok && read.amended {
109 | m.mu.Lock()
110 | // Avoid reporting a spurious miss if m.dirty got promoted while we were
111 | // blocked on m.mu. (If further loads of the same key will not miss, it's
112 | // not worth copying the dirty map for this key.)
113 | read, _ = m.read.Load().(readOnlyTokenMap)
114 | e, ok = read.m[key]
115 | if !ok && read.amended {
116 | e, ok = m.dirty[key]
117 | // Regardless of whether the entry was present, record a miss: this key
118 | // will take the slow path until the dirty map is promoted to the read
119 | // map.
120 | m.missLocked()
121 | }
122 | m.mu.Unlock()
123 | }
124 | if !ok {
125 | return value, false
126 | }
127 | return e.load()
128 | }
129 |
130 | func (e *entryTokenMap) load() (value []byte, ok bool) {
131 | p := atomic.LoadPointer(&e.p)
132 | if p == nil || p == expungedTokenMap {
133 | return value, false
134 | }
135 | return *(*[]byte)(p), true
136 | }
137 |
138 | // Store sets the value for a key.
139 | func (m *TokenMap) Store(key int64, value []byte) {
140 | read, _ := m.read.Load().(readOnlyTokenMap)
141 | if e, ok := read.m[key]; ok && e.tryStore(&value) {
142 | return
143 | }
144 |
145 | m.mu.Lock()
146 | read, _ = m.read.Load().(readOnlyTokenMap)
147 | if e, ok := read.m[key]; ok {
148 | if e.unexpungeLocked() {
149 | // The entry was previously expunged, which implies that there is a
150 | // non-nil dirty map and this entry is not in it.
151 | m.dirty[key] = e
152 | }
153 | e.storeLocked(&value)
154 | } else if e, ok := m.dirty[key]; ok {
155 | e.storeLocked(&value)
156 | } else {
157 | if !read.amended {
158 | // We're adding the first new key to the dirty map.
159 | // Make sure it is allocated and mark the read-only map as incomplete.
160 | m.dirtyLocked()
161 | m.read.Store(readOnlyTokenMap{m: read.m, amended: true})
162 | }
163 | m.dirty[key] = newEntryTokenMap(value)
164 | }
165 | m.mu.Unlock()
166 | }
167 |
168 | // tryStore stores a value if the entry has not been expunged.
169 | //
170 | // If the entry is expunged, tryStore returns false and leaves the entry
171 | // unchanged.
172 | func (e *entryTokenMap) tryStore(i *[]byte) bool {
173 | for {
174 | p := atomic.LoadPointer(&e.p)
175 | if p == expungedTokenMap {
176 | return false
177 | }
178 | if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
179 | return true
180 | }
181 | }
182 | }
183 |
184 | // unexpungeLocked ensures that the entry is not marked as expunged.
185 | //
186 | // If the entry was previously expunged, it must be added to the dirty map
187 | // before m.mu is unlocked.
188 | func (e *entryTokenMap) unexpungeLocked() (wasExpunged bool) {
189 | return atomic.CompareAndSwapPointer(&e.p, expungedTokenMap, nil)
190 | }
191 |
192 | // storeLocked unconditionally stores a value to the entry.
193 | //
194 | // The entry must be known not to be expunged.
195 | func (e *entryTokenMap) storeLocked(i *[]byte) {
196 | atomic.StorePointer(&e.p, unsafe.Pointer(i))
197 | }
198 |
199 | // LoadOrStore returns the existing value for the key if present.
200 | // Otherwise, it stores and returns the given value.
201 | // The loaded result is true if the value was loaded, false if stored.
202 | func (m *TokenMap) LoadOrStore(key int64, value []byte) (actual []byte, loaded bool) {
203 | // Avoid locking if it's a clean hit.
204 | read, _ := m.read.Load().(readOnlyTokenMap)
205 | if e, ok := read.m[key]; ok {
206 | actual, loaded, ok := e.tryLoadOrStore(value)
207 | if ok {
208 | return actual, loaded
209 | }
210 | }
211 |
212 | m.mu.Lock()
213 | read, _ = m.read.Load().(readOnlyTokenMap)
214 | if e, ok := read.m[key]; ok {
215 | if e.unexpungeLocked() {
216 | m.dirty[key] = e
217 | }
218 | actual, loaded, _ = e.tryLoadOrStore(value)
219 | } else if e, ok := m.dirty[key]; ok {
220 | actual, loaded, _ = e.tryLoadOrStore(value)
221 | m.missLocked()
222 | } else {
223 | if !read.amended {
224 | // We're adding the first new key to the dirty map.
225 | // Make sure it is allocated and mark the read-only map as incomplete.
226 | m.dirtyLocked()
227 | m.read.Store(readOnlyTokenMap{m: read.m, amended: true})
228 | }
229 | m.dirty[key] = newEntryTokenMap(value)
230 | actual, loaded = value, false
231 | }
232 | m.mu.Unlock()
233 |
234 | return actual, loaded
235 | }
236 |
237 | // tryLoadOrStore atomically loads or stores a value if the entry is not
238 | // expunged.
239 | //
240 | // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
241 | // returns with ok==false.
242 | func (e *entryTokenMap) tryLoadOrStore(i []byte) (actual []byte, loaded, ok bool) {
243 | p := atomic.LoadPointer(&e.p)
244 | if p == expungedTokenMap {
245 | return actual, false, false
246 | }
247 | if p != nil {
248 | return *(*[]byte)(p), true, true
249 | }
250 |
251 | // Copy the interface after the first load to make this method more amenable
252 | // to escape analysis: if we hit the "load" path or the entry is expunged, we
253 | // shouldn't bother heap-allocating.
254 | ic := i
255 | for {
256 | if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
257 | return i, false, true
258 | }
259 | p = atomic.LoadPointer(&e.p)
260 | if p == expungedTokenMap {
261 | return actual, false, false
262 | }
263 | if p != nil {
264 | return *(*[]byte)(p), true, true
265 | }
266 | }
267 | }
268 |
269 | // LoadAndDelete deletes the value for a key, returning the previous value if any.
270 | // The loaded result reports whether the key was present.
271 | func (m *TokenMap) LoadAndDelete(key int64) (value []byte, loaded bool) {
272 | read, _ := m.read.Load().(readOnlyTokenMap)
273 | e, ok := read.m[key]
274 | if !ok && read.amended {
275 | m.mu.Lock()
276 | read, _ = m.read.Load().(readOnlyTokenMap)
277 | e, ok = read.m[key]
278 | if !ok && read.amended {
279 | e, ok = m.dirty[key]
280 | delete(m.dirty, key)
281 | // Regardless of whether the entry was present, record a miss: this key
282 | // will take the slow path until the dirty map is promoted to the read
283 | // map.
284 | m.missLocked()
285 | }
286 | m.mu.Unlock()
287 | }
288 | if ok {
289 | return e.delete()
290 | }
291 | return value, false
292 | }
293 |
294 | // Delete deletes the value for a key.
295 | func (m *TokenMap) Delete(key int64) {
296 | m.LoadAndDelete(key)
297 | }
298 |
299 | func (e *entryTokenMap) delete() (value []byte, ok bool) {
300 | for {
301 | p := atomic.LoadPointer(&e.p)
302 | if p == nil || p == expungedTokenMap {
303 | return value, false
304 | }
305 | if atomic.CompareAndSwapPointer(&e.p, p, nil) {
306 | return *(*[]byte)(p), true
307 | }
308 | }
309 | }
310 |
311 | // Range calls f sequentially for each key and value present in the map.
312 | // If f returns false, range stops the iteration.
313 | //
314 | // Range does not necessarily correspond to any consistent snapshot of the Map's
315 | // contents: no key will be visited more than once, but if the value for any key
316 | // is stored or deleted concurrently, Range may reflect any mapping for that key
317 | // from any point during the Range call.
318 | //
319 | // Range may be O(N) with the number of elements in the map even if f returns
320 | // false after a constant number of calls.
321 | func (m *TokenMap) Range(f func(key int64, value []byte) bool) {
322 | // We need to be able to iterate over all of the keys that were already
323 | // present at the start of the call to Range.
324 | // If read.amended is false, then read.m satisfies that property without
325 | // requiring us to hold m.mu for a long time.
326 | read, _ := m.read.Load().(readOnlyTokenMap)
327 | if read.amended {
328 | // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
329 | // (assuming the caller does not break out early), so a call to Range
330 | // amortizes an entire copy of the map: we can promote the dirty copy
331 | // immediately!
332 | m.mu.Lock()
333 | read, _ = m.read.Load().(readOnlyTokenMap)
334 | if read.amended {
335 | read = readOnlyTokenMap{m: m.dirty}
336 | m.read.Store(read)
337 | m.dirty = nil
338 | m.misses = 0
339 | }
340 | m.mu.Unlock()
341 | }
342 |
343 | for k, e := range read.m {
344 | v, ok := e.load()
345 | if !ok {
346 | continue
347 | }
348 | if !f(k, v) {
349 | break
350 | }
351 | }
352 | }
353 |
354 | func (m *TokenMap) missLocked() {
355 | m.misses++
356 | if m.misses < len(m.dirty) {
357 | return
358 | }
359 | m.read.Store(readOnlyTokenMap{m: m.dirty})
360 | m.dirty = nil
361 | m.misses = 0
362 | }
363 |
364 | func (m *TokenMap) dirtyLocked() {
365 | if m.dirty != nil {
366 | return
367 | }
368 |
369 | read, _ := m.read.Load().(readOnlyTokenMap)
370 | m.dirty = make(map[int64]*entryTokenMap, len(read.m))
371 | for k, e := range read.m {
372 | if !e.tryExpungeLocked() {
373 | m.dirty[k] = e
374 | }
375 | }
376 | }
377 |
378 | func (e *entryTokenMap) tryExpungeLocked() (isExpunged bool) {
379 | p := atomic.LoadPointer(&e.p)
380 | for p == nil {
381 | if atomic.CompareAndSwapPointer(&e.p, nil, expungedTokenMap) {
382 | return true
383 | }
384 | p = atomic.LoadPointer(&e.p)
385 | }
386 | return p == expungedTokenMap
387 | }
388 |
--------------------------------------------------------------------------------
/pkg/bot/mirai2proto.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/2mf8/Go-Lagrange-Client/proto_gen/onebot"
7 |
8 | "github.com/2mf8/LagrangeGo/client"
9 | "github.com/2mf8/LagrangeGo/message"
10 | )
11 |
12 | func MiraiMsgToProtoMsg(cli *client.QQClient, messageChain []message.IMessageElement) []*onebot.Message {
13 | msgList := make([]*onebot.Message, 0)
14 | for _, element := range messageChain {
15 | switch elem := element.(type) {
16 | case *message.TextElement:
17 | msgList = append(msgList, MiraiTextToProtoText(elem))
18 | case *message.AtElement:
19 | msgList = append(msgList, MiraiAtToProtoAt(elem))
20 | case *message.ImageElement:
21 | msgList = append(msgList, MiraiImageToProtoImage(elem))
22 | case *message.FaceElement:
23 | msgList = append(msgList, MiraiFaceToProtoFace(elem))
24 | case *message.VoiceElement:
25 | msgList = append(msgList, MiraiVoiceToProtoVoice(elem))
26 | case *message.ShortVideoElement:
27 | msgList = append(msgList, MiraiVideoToProtoVideo(cli, elem))
28 | case *message.ReplyElement:
29 | msgList = append(msgList, MiraiReplyToProtoReply(cli, elem))
30 | }
31 | }
32 | return msgList
33 | }
34 |
35 | func MiraiTextToProtoText(elem *message.TextElement) *onebot.Message {
36 | return &onebot.Message{
37 | Type: "text",
38 | Data: map[string]string{
39 | "text": elem.Content,
40 | },
41 | }
42 | }
43 |
44 | func MiraiImageToProtoImage(elem *message.ImageElement) *onebot.Message {
45 | msg := &onebot.Message{
46 | Type: "image",
47 | Data: map[string]string{
48 | "image_id": elem.ImageId,
49 | "file": elem.Url,
50 | "url": elem.Url,
51 | },
52 | }
53 | if elem.Flash {
54 | msg.Data["type"] = "flash"
55 | }
56 | if elem.EffectID != 0 {
57 | msg.Data["type"] = "show"
58 | msg.Data["effect_id"] = strconv.FormatInt(int64(elem.EffectID), 10)
59 | }
60 | return msg
61 | }
62 |
63 | func MiraiAtToProtoAt(elem *message.AtElement) *onebot.Message {
64 | return &onebot.Message{
65 | Type: "at",
66 | Data: map[string]string{
67 | "qq": func() string {
68 | if elem.TargetUin == 0 {
69 | return "all"
70 | }
71 | return strconv.FormatInt(int64(elem.TargetUin), 10)
72 | }(),
73 | },
74 | }
75 | }
76 |
77 | func MiraiFaceToProtoFace(elem *message.FaceElement) *onebot.Message {
78 | return &onebot.Message{
79 | Type: "face",
80 | Data: map[string]string{
81 | "id": strconv.Itoa(int(elem.FaceID)),
82 | },
83 | }
84 | }
85 |
86 | func MiraiVoiceToProtoVoice(elem *message.VoiceElement) *onebot.Message {
87 | return &onebot.Message{
88 | Type: "record",
89 | Data: map[string]string{
90 | "file": elem.Url,
91 | "url": elem.Url,
92 | },
93 | }
94 | }
95 |
96 | func MiraiVideoToProtoVideo(cli *client.QQClient, elem *message.ShortVideoElement) *onebot.Message {
97 | return &onebot.Message{
98 | Type: "video",
99 | Data: map[string]string{
100 | "name": elem.Name,
101 | "url": elem.Url,
102 | },
103 | }
104 | }
105 |
106 | func MiraiReplyToProtoReply(cli *client.QQClient, elem *message.ReplyElement) *onebot.Message {
107 | return &onebot.Message{
108 | Type: "reply",
109 | Data: map[string]string{
110 | "message_id": strconv.FormatInt(int64(elem.ReplySeq), 10),
111 | "sender": strconv.FormatInt(int64(elem.SenderUin), 10),
112 | "time": strconv.FormatInt(int64(elem.Time), 10),
113 | "raw_message": MiraiMsgToRawMsg(cli, elem.Elements),
114 | },
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/bot/mirai2raw.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "fmt"
5 | "html"
6 |
7 | "github.com/2mf8/LagrangeGo/client"
8 | "github.com/2mf8/LagrangeGo/message"
9 | )
10 |
11 | func MiraiMsgToRawMsg(cli *client.QQClient, messageChain []message.IMessageElement) string {
12 | result := ""
13 | for _, element := range messageChain {
14 | switch elem := element.(type) {
15 | case *message.TextElement:
16 | result += elem.Content
17 | case *message.ImageElement:
18 | result += fmt.Sprintf(``, html.EscapeString(elem.ImageId), html.EscapeString(elem.Url))
19 | case *message.FaceElement:
20 | result += fmt.Sprintf(``, elem.FaceID)
21 | case *message.VoiceElement:
22 | result += fmt.Sprintf(``, html.EscapeString(elem.Url))
23 | case *message.ReplyElement:
24 | result += fmt.Sprintf(``, elem.Time, elem.SenderUin, html.EscapeString(MiraiMsgToRawMsg(cli, elem.Elements)), elem.ReplySeq)
25 | }
26 | }
27 | return result
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/bot/proto2mirai.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "bytes"
5 |
6 | "strconv"
7 | "time"
8 |
9 | "github.com/2mf8/Go-Lagrange-Client/pkg/cache"
10 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
11 | "github.com/2mf8/Go-Lagrange-Client/proto_gen/onebot"
12 |
13 | "github.com/2mf8/LagrangeGo/client"
14 | "github.com/2mf8/LagrangeGo/message"
15 | log "github.com/sirupsen/logrus"
16 | )
17 |
18 | func EmptyText() *message.TextElement {
19 | return message.NewText("")
20 | }
21 |
22 | // 消息列表,不自动把code变成msg
23 | func ProtoMsgToMiraiMsg(cli *client.QQClient, msgList []*onebot.Message, notConvertText bool) []message.IMessageElement {
24 | containReply := false // 每条消息只能包含一个reply
25 | messageChain := make([]message.IMessageElement, 0)
26 | for _, protoMsg := range msgList {
27 | switch protoMsg.Type {
28 | case "text":
29 | if notConvertText {
30 | messageChain = append(messageChain, ProtoTextToMiraiText(protoMsg.Data))
31 | } else {
32 | text, ok := protoMsg.Data["text"]
33 | if !ok {
34 | log.Warnf("text不存在")
35 | continue
36 | }
37 | messageChain = append(messageChain, RawMsgToMiraiMsg(cli, text)...) // 转换xml码
38 | }
39 | case "at":
40 | messageChain = append(messageChain, ProtoAtToMiraiAt(protoMsg.Data))
41 | case "image":
42 | messageChain = append(messageChain, ProtoImageToMiraiImage(protoMsg.Data))
43 | case "img":
44 | messageChain = append(messageChain, ProtoImageToMiraiImage(protoMsg.Data))
45 | case "record":
46 | messageChain = append(messageChain, ProtoVoiceToMiraiVoice(protoMsg.Data))
47 | case "face":
48 | messageChain = append(messageChain, ProtoFaceToMiraiFace(protoMsg.Data))
49 | case "reply":
50 | if replyElement := ProtoReplyToMiraiReply(protoMsg.Data); replyElement != nil && !containReply {
51 | containReply = true
52 | messageChain = append([]message.IMessageElement{replyElement}, messageChain...)
53 | }
54 | case "sleep":
55 | ProtoSleep(protoMsg.Data)
56 | default:
57 | log.Errorf("不支持的消息类型 %+v", protoMsg)
58 | }
59 | }
60 | return messageChain
61 | }
62 |
63 | func ProtoTextToMiraiText(data map[string]string) message.IMessageElement {
64 | text, ok := data["text"]
65 | if !ok {
66 | log.Warnf("text不存在")
67 | return EmptyText()
68 | }
69 | return message.NewText(text)
70 | }
71 |
72 | func ProtoImageToMiraiImage(data map[string]string) message.IMessageElement {
73 | url, ok := data["url"]
74 | if !ok {
75 | url, ok = data["src"] // TODO 为了兼容我的旧代码偷偷加的
76 | if !ok {
77 | url, ok = data["file"]
78 | }
79 | }
80 | if !ok {
81 | log.Warnf("imageUrl不存在")
82 | return EmptyText()
83 | }
84 | return &message.ImageElement{Url: url}
85 | }
86 |
87 | func ProtoVoiceToMiraiVoice(data map[string]string) message.IMessageElement {
88 | url, ok := data["url"]
89 | if !ok {
90 | url, ok = data["file"]
91 | }
92 | if !ok {
93 | log.Warnf("recordUrl不存在")
94 | return EmptyText()
95 | }
96 | b, err := util.GetBytes(url)
97 | if err != nil {
98 | log.Errorf("下载语音失败")
99 | return EmptyText()
100 | }
101 | if !util.IsAMRorSILK(b) {
102 | log.Errorf("不是amr或silk格式")
103 | return EmptyText()
104 | }
105 | return &message.VoiceElement{Stream: bytes.NewReader(b)}
106 | }
107 |
108 | func ProtoAtToMiraiAt(data map[string]string) message.IMessageElement {
109 | qq, ok := data["qq"]
110 | if !ok {
111 | log.Warnf("atQQ不存在")
112 | return EmptyText()
113 | }
114 | if qq == "all" {
115 | return message.NewAt(0)
116 | }
117 | userId, err := strconv.ParseInt(qq, 10, 64)
118 | if err != nil {
119 | log.Warnf("atQQ不是数字")
120 | return EmptyText()
121 | }
122 | return message.NewAt(uint32(userId))
123 | }
124 |
125 | func ProtoFaceToMiraiFace(data map[string]string) message.IMessageElement {
126 | idStr, ok := data["id"]
127 | if !ok {
128 | log.Warnf("faceId不存在")
129 | return EmptyText()
130 | }
131 | id, err := strconv.Atoi(idStr)
132 | if err != nil {
133 | log.Warnf("faceId不是数字")
134 | return EmptyText()
135 | }
136 | return &message.FaceElement{
137 | FaceID: uint16(id),
138 | }
139 | }
140 |
141 | func ProtoReplyToMiraiReply(data map[string]string) *message.ReplyElement {
142 | rawMessage, hasRawMessage := data["raw_message"] // 如果存在 raw_message,按照raw_message显示
143 |
144 | messageIdStr, ok := data["message_id"]
145 | if !ok {
146 | return nil
147 | }
148 | messageIdInt, err := strconv.Atoi(messageIdStr)
149 | if err != nil {
150 | return nil
151 | }
152 | messageId := int32(messageIdInt)
153 | eventInterface, ok := cache.GroupMessageLru.Get(messageId)
154 | if ok {
155 | groupMessage, ok := eventInterface.(*message.GroupMessage)
156 | if ok {
157 | return &message.ReplyElement{
158 | ReplySeq: uint32(groupMessage.Id),
159 | SenderUin: groupMessage.Sender.Uin,
160 | Time: uint32(groupMessage.Time),
161 | Elements: func() []message.IMessageElement {
162 | if hasRawMessage {
163 | return []message.IMessageElement{message.NewText(rawMessage)}
164 | } else {
165 | return groupMessage.Elements
166 | }
167 | }(),
168 | }
169 | }
170 | }
171 | eventInterface, ok = cache.PrivateMessageLru.Get(messageId)
172 | if ok {
173 | privateMessage, ok := eventInterface.(*message.PrivateMessage)
174 | if ok {
175 | return &message.ReplyElement{
176 | ReplySeq: uint32(privateMessage.Id),
177 | SenderUin: privateMessage.Sender.Uin,
178 | Time: uint32(privateMessage.Time),
179 | Elements: func() []message.IMessageElement {
180 | if hasRawMessage {
181 | return []message.IMessageElement{message.NewText(rawMessage)}
182 | } else {
183 | return privateMessage.Elements
184 | }
185 | }(),
186 | }
187 | }
188 | }
189 | return nil
190 | }
191 |
192 | func ProtoSleep(data map[string]string) {
193 | t, ok := data["time"]
194 | if !ok {
195 | log.Warnf("failed to get sleep time1")
196 | return
197 | }
198 | ms, err := strconv.Atoi(t)
199 | if err != nil {
200 | log.Warnf("failed to get sleep time2, %+v", err)
201 | return
202 | }
203 | if ms > 24*3600*1000 {
204 | log.Warnf("最多 sleep 24小时")
205 | ms = 24 * 3600 * 1000
206 | }
207 | time.Sleep(time.Duration(ms) * time.Millisecond)
208 | }
209 |
210 | /*func ProtoMusicToMiraiMusic(_ *client.QQClient, data map[string]string) (m message.IMessageElement) {
211 | if data["type"] == "qq" {
212 | info, err := util.QQMusicSongInfo(data["id"])
213 | if err != nil {
214 | log.Warnf("failed to get qq music song info, %+v", data["id"])
215 | return EmptyText()
216 | }
217 | if !info.Get("track_info").Exists() {
218 | log.Warnf("music track_info not found, %+v", info.String())
219 | return EmptyText()
220 | }
221 | name := info.Get("track_info.name").Str
222 | mid := info.Get("track_info.mid").Str
223 | albumMid := info.Get("track_info.album.mid").Str
224 | pinfo, _ := util.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576")
225 | jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
226 | purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str
227 | preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg"
228 | content := info.Get("track_info.singer.0.name").Str
229 | if data["content"] != "" {
230 | content = data["content"]
231 | }
232 | return &message.MusicShareElement{
233 | MusicType: message.QQMusic,
234 | Title: name,
235 | Summary: content,
236 | Url: jumpURL,
237 | PictureUrl: preview,
238 | MusicUrl: purl,
239 | }
240 | }
241 | if data["type"] == "163" {
242 | info, err := util.NeteaseMusicSongInfo(data["id"])
243 | if err != nil {
244 | log.Warnf("failed to get qq music song info, %+v", data["id"])
245 | return EmptyText()
246 | }
247 | if !info.Exists() {
248 | log.Warnf("netease song not fount")
249 | return EmptyText()
250 | }
251 | name := info.Get("name").Str
252 | jumpURL := "https://y.music.163.com/m/song/" + data["id"]
253 | musicURL := "http://music.163.com/song/media/outer/url?id=" + data["id"]
254 | picURL := info.Get("album.picUrl").Str
255 | artistName := ""
256 | if info.Get("artists.0").Exists() {
257 | artistName = info.Get("artists.0.name").Str
258 | }
259 | return &message.MusicShareElement{
260 | MusicType: message.CloudMusic,
261 | Title: name,
262 | Summary: artistName,
263 | Url: jumpURL,
264 | PictureUrl: picURL,
265 | MusicUrl: musicURL,
266 | }
267 | }
268 | if data["type"] == "custom" {
269 | if data["subtype"] != "" {
270 | var subType int
271 | switch data["subtype"] {
272 | default:
273 | subType = message.QQMusic
274 | case "163":
275 | subType = message.CloudMusic
276 | case "migu":
277 | subType = message.MiguMusic
278 | case "kugou":
279 | subType = message.KugouMusic
280 | case "kuwo":
281 | subType = message.KuwoMusic
282 | }
283 | return &message.MusicShareElement{
284 | MusicType: subType,
285 | Title: data["title"],
286 | Summary: data["content"],
287 | Url: data["url"],
288 | PictureUrl: data["image"],
289 | MusicUrl: data["audio"],
290 | }
291 | }
292 | xml := fmt.Sprintf(`- %s%s
`,
293 | utils.XmlEscape(data["title"]), data["url"], data["image"], data["audio"], utils.XmlEscape(data["title"]), utils.XmlEscape(data["content"]))
294 | return &message.ServiceElement{
295 | Id: 60,
296 | Content: xml,
297 | SubType: "music",
298 | }
299 | }
300 | return EmptyText()
301 | }*/
302 |
--------------------------------------------------------------------------------
/pkg/bot/raw2mirai.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "encoding/xml"
5 | "html"
6 | "regexp"
7 | "strings"
8 |
9 | "github.com/2mf8/LagrangeGo/client"
10 | "github.com/2mf8/LagrangeGo/message"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | type Node struct {
15 | XMLName xml.Name
16 | Attr []xml.Attr `xml:",any,attr"`
17 | }
18 |
19 | var re = regexp.MustCompile("<[\\s\\S]+?/>")
20 |
21 | func RawMsgToMiraiMsg(cli *client.QQClient, str string) []message.IMessageElement {
22 | containReply := false
23 | var node Node
24 | textList := re.Split(str, -1)
25 | codeList := re.FindAllString(str, -1)
26 | elemList := make([]message.IMessageElement, 0)
27 | for len(textList) > 0 || len(codeList) > 0 {
28 | if len(textList) > 0 && strings.HasPrefix(str, textList[0]) {
29 | text := textList[0]
30 | textList = textList[1:]
31 | str = str[len(text):]
32 | elemList = append(elemList, message.NewText(text))
33 | }
34 | if len(codeList) > 0 && strings.HasPrefix(str, codeList[0]) {
35 | code := codeList[0]
36 | codeList = codeList[1:]
37 | str = str[len(code):]
38 | err := xml.Unmarshal([]byte(code), &node)
39 | if err != nil {
40 | elemList = append(elemList, message.NewText(code))
41 | continue
42 | }
43 | attrMap := make(map[string]string)
44 | for _, attr := range node.Attr {
45 | attrMap[attr.Name.Local] = html.UnescapeString(attr.Value)
46 | }
47 | switch node.XMLName.Local {
48 | case "at":
49 | elemList = append(elemList, ProtoAtToMiraiAt(attrMap))
50 | case "img":
51 | elemList = append(elemList, ProtoImageToMiraiImage(attrMap)) // TODO 为了兼容我的旧代码偷偷加的
52 | case "image":
53 | elemList = append(elemList, ProtoImageToMiraiImage(attrMap))
54 | case "face":
55 | elemList = append(elemList, ProtoFaceToMiraiFace(attrMap))
56 | case "voice":
57 | elemList = append(elemList, ProtoVoiceToMiraiVoice(attrMap))
58 | case "record":
59 | elemList = append(elemList, ProtoVoiceToMiraiVoice(attrMap))
60 | case "text":
61 | elemList = append(elemList, ProtoTextToMiraiText(attrMap))
62 | case "reply":
63 | if replyElement := ProtoReplyToMiraiReply(attrMap); replyElement != nil && !containReply {
64 | containReply = true
65 | elemList = append([]message.IMessageElement{replyElement}, elemList...)
66 | }
67 | case "sleep":
68 | ProtoSleep(attrMap)
69 | default:
70 | log.Warnf("不支持的类型 %s", code)
71 | elemList = append(elemList, message.NewText(code))
72 | }
73 | }
74 | }
75 | return elemList
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/golang/groupcache/lru"
7 | )
8 |
9 | type LruCache struct {
10 | *lru.Cache
11 | sync.Mutex
12 | }
13 |
14 | func NewLruCache(maxEntries int) *LruCache {
15 | return &LruCache{
16 | Cache: lru.New(maxEntries),
17 | }
18 | }
19 |
20 | func (l *LruCache) Add(key lru.Key, value interface{}) {
21 | l.Lock()
22 | l.Cache.Add(key, value)
23 | l.Unlock()
24 | }
25 | func (l *LruCache) Get(key lru.Key) (value interface{}, ok bool) {
26 | l.Lock()
27 | value, ok = l.Cache.Get(key)
28 | l.Unlock()
29 | return
30 | }
31 |
32 | // int:PrivateMessage
33 | var PrivateMessageLru = NewLruCache(512)
34 |
35 | // int:GroupMessage
36 | var GroupMessageLru = NewLruCache(2048)
37 |
38 | // int:ChannelMessage
39 | var ChannelMessageLru = NewLruCache(2048)
40 |
41 | // string:
42 | var FriendRequestLru = NewLruCache(128)
43 |
44 | // string:
45 | var GroupRequestLru = NewLruCache(128)
46 |
47 | // string:
48 | var GroupInvitedRequestLru = NewLruCache(16)
49 |
50 | var GuildAdminLru = NewLruCache(2048)
51 |
52 | var GetGuildAdminTimeLru = NewLruCache(100)
53 |
--------------------------------------------------------------------------------
/pkg/cache/cache_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import "testing"
4 |
5 | func TestLruCache_Add(t *testing.T) {
6 | lruCache := NewLruCache(3)
7 | lruCache.Add(10, 10)
8 | v, ok := lruCache.Get(10)
9 | t.Logf("1: %+v %+v", v, ok)
10 | lruCache.Add(11, 10)
11 | lruCache.Add(13, 10)
12 | v, ok = lruCache.Get(10)
13 | t.Logf("2: %+v %+v", v, ok)
14 | lruCache.Add(14, 10)
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 | "path"
9 | "strings"
10 |
11 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | //go:generate go run github.com/2mf8/syncmap -o "gen_plugin_map.go" -pkg config -name PluginMap "map[string]*Plugin"
16 | //go:generate go run github.com/2mf8/syncmap -o "gen_device_info_map.go" -pkg config -name PluginMap "map[string]*DeviceInfo"
17 | var (
18 | Fragment = false // 是否分片
19 | Port = "9000"
20 | SMS = false
21 | Device = ""
22 | Plugins = &PluginMap{}
23 | HttpAuth = map[string]string{}
24 | )
25 |
26 | func init() {
27 | Plugins.Store("default", &Plugin{
28 | Name: "default",
29 | Disabled: false,
30 | Json: false,
31 | Protocol: 0,
32 | Urls: []string{"ws://localhost:8081/ws/cq/"},
33 | EventFilter: []int32{},
34 | ApiFilter: []int32{},
35 | RegexFilter: "",
36 | RegexReplace: "",
37 | ExtraHeader: map[string][]string{
38 | "User-Agent": {"GMC"},
39 | },
40 | })
41 | }
42 |
43 | func ClearPlugins(pluginMap *PluginMap) {
44 | pluginMap.Range(func(key string, value *Plugin) bool {
45 | pluginMap.Delete(key)
46 | return true
47 | })
48 | }
49 |
50 | type Plugin struct {
51 | Name string `json:"-"` // 功能名称
52 | Disabled bool `json:"disabled"` // 不填false默认启用
53 | Json bool `json:"json"` // json上报
54 | Protocol int32 `json:"protocol"` // 通信协议
55 | Urls []string `json:"urls"` // 服务器列表
56 | EventFilter []int32 `json:"event_filter"` // 事件过滤
57 | ApiFilter []int32 `json:"api_filter"` // API过滤
58 | RegexFilter string `json:"regex_filter"` // 正则过滤
59 | RegexReplace string `json:"regex_replace"` // 正则替换
60 | ExtraHeader map[string][]string `json:"extra_header"` // 自定义请求头
61 | // TODO event filter, msg filter, regex filter, prefix filter, suffix filter
62 | }
63 |
64 | type DeviceInfo struct {
65 | Guid string `json:"guid"`
66 | DeviceName string `json:"device_name"`
67 | SystemKernel string `json:"system_kernel"`
68 | KernelVersion string `json:"kernel_version"`
69 | }
70 |
71 | var PluginPath = "plugins"
72 |
73 | func LoadPlugins() {
74 | if !util.PathExists(PluginPath) {
75 | return
76 | }
77 | files, err := os.ReadDir(PluginPath)
78 | if err != nil {
79 | log.Warnf("failed to read plugin dir: %s", err)
80 | return
81 | }
82 |
83 | if len(files) == 0 {
84 | log.Warnf("plugin dir is empty")
85 | return
86 | }
87 |
88 | ClearPlugins(Plugins)
89 | for _, file := range files {
90 | if !strings.HasSuffix(file.Name(), ".json") {
91 | continue
92 | }
93 | pluginName := strings.TrimSuffix(file.Name(), ".json")
94 | filepath := path.Join(PluginPath, file.Name())
95 | b, err := os.ReadFile(filepath)
96 | if err != nil {
97 | log.Warnf("failed to read plugin file: %s %s", filepath, err)
98 | continue
99 | }
100 | plugin := &Plugin{}
101 | if err := json.NewDecoder(bytes.NewReader(b)).Decode(plugin); err != nil {
102 | log.Warnf("failed to decode plugin file: %s %s", filepath, err)
103 | continue
104 | }
105 | plugin.Name = pluginName
106 | Plugins.Store(plugin.Name, plugin)
107 | }
108 | }
109 |
110 | func WritePlugins() {
111 | if !util.PathExists(PluginPath) {
112 | if err := os.MkdirAll(PluginPath, 0777); err != nil {
113 | log.Warnf("failed to mkdir")
114 | return
115 | }
116 | }
117 | DeletePluginFiles()
118 | Plugins.Range(func(key string, plugin *Plugin) bool {
119 | pluginFilename := fmt.Sprintf("%s.json", plugin.Name)
120 | filepath := path.Join(PluginPath, pluginFilename)
121 | b, err := json.MarshalIndent(plugin, "", " ")
122 | if err != nil {
123 | log.Warnf("failed to marshal plugin, %s", plugin.Name)
124 | return true
125 | }
126 | if err := os.WriteFile(filepath, b, 0777); err != nil {
127 | log.Warnf("failed to write file, %s", pluginFilename)
128 | return true
129 | }
130 | return true
131 | })
132 | }
133 |
134 | func DeletePluginFiles() {
135 | files, err := os.ReadDir(PluginPath)
136 | if err != nil {
137 | log.Warnf("failed to read plugin dir: %s", err)
138 | }
139 | for _, file := range files {
140 | if !strings.HasSuffix(file.Name(), ".json") {
141 | continue
142 | }
143 | filepath := path.Join(PluginPath, file.Name())
144 | if err := os.Remove(filepath); err != nil {
145 | log.Warnf("failed to remove plugin file: %s", filepath)
146 | continue
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/config/gen_plugin_map.go:
--------------------------------------------------------------------------------
1 | // Code generated by syncmap; DO NOT EDIT.
2 |
3 | // Copyright 2016 The Go Authors. All rights reserved.
4 | // Use of this source code is governed by a BSD-style
5 | // license that can be found in the LICENSE file.
6 |
7 | package config
8 |
9 | import (
10 | "sync"
11 | "sync/atomic"
12 | "unsafe"
13 | )
14 |
15 | // Map is like a Go map[interface{}]interface{} but is safe for concurrent use
16 | // by multiple goroutines without additional locking or coordination.
17 | // Loads, stores, and deletes run in amortized constant time.
18 | //
19 | // The Map type is specialized. Most code should use a plain Go map instead,
20 | // with separate locking or coordination, for better type safety and to make it
21 | // easier to maintain other invariants along with the map content.
22 | //
23 | // The Map type is optimized for two common use cases: (1) when the entry for a given
24 | // key is only ever written once but read many times, as in caches that only grow,
25 | // or (2) when multiple goroutines read, write, and overwrite entries for disjoint
26 | // sets of keys. In these two cases, use of a Map may significantly reduce lock
27 | // contention compared to a Go map paired with a separate Mutex or RWMutex.
28 | //
29 | // The zero Map is empty and ready for use. A Map must not be copied after first use.
30 | type PluginMap struct {
31 | mu sync.Mutex
32 |
33 | // read contains the portion of the map's contents that are safe for
34 | // concurrent access (with or without mu held).
35 | //
36 | // The read field itself is always safe to load, but must only be stored with
37 | // mu held.
38 | //
39 | // Entries stored in read may be updated concurrently without mu, but updating
40 | // a previously-expunged entry requires that the entry be copied to the dirty
41 | // map and unexpunged with mu held.
42 | read atomic.Value // readOnly
43 |
44 | // dirty contains the portion of the map's contents that require mu to be
45 | // held. To ensure that the dirty map can be promoted to the read map quickly,
46 | // it also includes all of the non-expunged entries in the read map.
47 | //
48 | // Expunged entries are not stored in the dirty map. An expunged entry in the
49 | // clean map must be unexpunged and added to the dirty map before a new value
50 | // can be stored to it.
51 | //
52 | // If the dirty map is nil, the next write to the map will initialize it by
53 | // making a shallow copy of the clean map, omitting stale entries.
54 | dirty map[string]*entryPluginMap
55 |
56 | // misses counts the number of loads since the read map was last updated that
57 | // needed to lock mu to determine whether the key was present.
58 | //
59 | // Once enough misses have occurred to cover the cost of copying the dirty
60 | // map, the dirty map will be promoted to the read map (in the unamended
61 | // state) and the next store to the map will make a new dirty copy.
62 | misses int
63 | }
64 |
65 | // readOnly is an immutable struct stored atomically in the Map.read field.
66 | type readOnlyPluginMap struct {
67 | m map[string]*entryPluginMap
68 | amended bool // true if the dirty map contains some key not in m.
69 | }
70 |
71 | // expunged is an arbitrary pointer that marks entries which have been deleted
72 | // from the dirty map.
73 | var expungedPluginMap = unsafe.Pointer(new(*Plugin))
74 |
75 | // An entry is a slot in the map corresponding to a particular key.
76 | type entryPluginMap struct {
77 | // p points to the interface{} value stored for the entry.
78 | //
79 | // If p == nil, the entry has been deleted, and either m.dirty == nil or
80 | // m.dirty[key] is e.
81 | //
82 | // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
83 | // is missing from m.dirty.
84 | //
85 | // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
86 | // != nil, in m.dirty[key].
87 | //
88 | // An entry can be deleted by atomic replacement with nil: when m.dirty is
89 | // next created, it will atomically replace nil with expunged and leave
90 | // m.dirty[key] unset.
91 | //
92 | // An entry's associated value can be updated by atomic replacement, provided
93 | // p != expunged. If p == expunged, an entry's associated value can be updated
94 | // only after first setting m.dirty[key] = e so that lookups using the dirty
95 | // map find the entry.
96 | p unsafe.Pointer // *interface{}
97 | }
98 |
99 | func newEntryPluginMap(i *Plugin) *entryPluginMap {
100 | return &entryPluginMap{p: unsafe.Pointer(&i)}
101 | }
102 |
103 | // Load returns the value stored in the map for a key, or nil if no
104 | // value is present.
105 | // The ok result indicates whether value was found in the map.
106 | func (m *PluginMap) Load(key string) (value *Plugin, ok bool) {
107 | read, _ := m.read.Load().(readOnlyPluginMap)
108 | e, ok := read.m[key]
109 | if !ok && read.amended {
110 | m.mu.Lock()
111 | // Avoid reporting a spurious miss if m.dirty got promoted while we were
112 | // blocked on m.mu. (If further loads of the same key will not miss, it's
113 | // not worth copying the dirty map for this key.)
114 | read, _ = m.read.Load().(readOnlyPluginMap)
115 | e, ok = read.m[key]
116 | if !ok && read.amended {
117 | e, ok = m.dirty[key]
118 | // Regardless of whether the entry was present, record a miss: this key
119 | // will take the slow path until the dirty map is promoted to the read
120 | // map.
121 | m.missLocked()
122 | }
123 | m.mu.Unlock()
124 | }
125 | if !ok {
126 | return value, false
127 | }
128 | return e.load()
129 | }
130 |
131 | func (e *entryPluginMap) load() (value *Plugin, ok bool) {
132 | p := atomic.LoadPointer(&e.p)
133 | if p == nil || p == expungedPluginMap {
134 | return value, false
135 | }
136 | return *(**Plugin)(p), true
137 | }
138 |
139 | // Store sets the value for a key.
140 | func (m *PluginMap) Store(key string, value *Plugin) {
141 | read, _ := m.read.Load().(readOnlyPluginMap)
142 | if e, ok := read.m[key]; ok && e.tryStore(&value) {
143 | return
144 | }
145 |
146 | m.mu.Lock()
147 | read, _ = m.read.Load().(readOnlyPluginMap)
148 | if e, ok := read.m[key]; ok {
149 | if e.unexpungeLocked() {
150 | // The entry was previously expunged, which implies that there is a
151 | // non-nil dirty map and this entry is not in it.
152 | m.dirty[key] = e
153 | }
154 | e.storeLocked(&value)
155 | } else if e, ok := m.dirty[key]; ok {
156 | e.storeLocked(&value)
157 | } else {
158 | if !read.amended {
159 | // We're adding the first new key to the dirty map.
160 | // Make sure it is allocated and mark the read-only map as incomplete.
161 | m.dirtyLocked()
162 | m.read.Store(readOnlyPluginMap{m: read.m, amended: true})
163 | }
164 | m.dirty[key] = newEntryPluginMap(value)
165 | }
166 | m.mu.Unlock()
167 | }
168 |
169 | // tryStore stores a value if the entry has not been expunged.
170 | //
171 | // If the entry is expunged, tryStore returns false and leaves the entry
172 | // unchanged.
173 | func (e *entryPluginMap) tryStore(i **Plugin) bool {
174 | for {
175 | p := atomic.LoadPointer(&e.p)
176 | if p == expungedPluginMap {
177 | return false
178 | }
179 | if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
180 | return true
181 | }
182 | }
183 | }
184 |
185 | // unexpungeLocked ensures that the entry is not marked as expunged.
186 | //
187 | // If the entry was previously expunged, it must be added to the dirty map
188 | // before m.mu is unlocked.
189 | func (e *entryPluginMap) unexpungeLocked() (wasExpunged bool) {
190 | return atomic.CompareAndSwapPointer(&e.p, expungedPluginMap, nil)
191 | }
192 |
193 | // storeLocked unconditionally stores a value to the entry.
194 | //
195 | // The entry must be known not to be expunged.
196 | func (e *entryPluginMap) storeLocked(i **Plugin) {
197 | atomic.StorePointer(&e.p, unsafe.Pointer(i))
198 | }
199 |
200 | // LoadOrStore returns the existing value for the key if present.
201 | // Otherwise, it stores and returns the given value.
202 | // The loaded result is true if the value was loaded, false if stored.
203 | func (m *PluginMap) LoadOrStore(key string, value *Plugin) (actual *Plugin, loaded bool) {
204 | // Avoid locking if it's a clean hit.
205 | read, _ := m.read.Load().(readOnlyPluginMap)
206 | if e, ok := read.m[key]; ok {
207 | actual, loaded, ok := e.tryLoadOrStore(value)
208 | if ok {
209 | return actual, loaded
210 | }
211 | }
212 |
213 | m.mu.Lock()
214 | read, _ = m.read.Load().(readOnlyPluginMap)
215 | if e, ok := read.m[key]; ok {
216 | if e.unexpungeLocked() {
217 | m.dirty[key] = e
218 | }
219 | actual, loaded, _ = e.tryLoadOrStore(value)
220 | } else if e, ok := m.dirty[key]; ok {
221 | actual, loaded, _ = e.tryLoadOrStore(value)
222 | m.missLocked()
223 | } else {
224 | if !read.amended {
225 | // We're adding the first new key to the dirty map.
226 | // Make sure it is allocated and mark the read-only map as incomplete.
227 | m.dirtyLocked()
228 | m.read.Store(readOnlyPluginMap{m: read.m, amended: true})
229 | }
230 | m.dirty[key] = newEntryPluginMap(value)
231 | actual, loaded = value, false
232 | }
233 | m.mu.Unlock()
234 |
235 | return actual, loaded
236 | }
237 |
238 | // tryLoadOrStore atomically loads or stores a value if the entry is not
239 | // expunged.
240 | //
241 | // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
242 | // returns with ok==false.
243 | func (e *entryPluginMap) tryLoadOrStore(i *Plugin) (actual *Plugin, loaded, ok bool) {
244 | p := atomic.LoadPointer(&e.p)
245 | if p == expungedPluginMap {
246 | return actual, false, false
247 | }
248 | if p != nil {
249 | return *(**Plugin)(p), true, true
250 | }
251 |
252 | // Copy the interface after the first load to make this method more amenable
253 | // to escape analysis: if we hit the "load" path or the entry is expunged, we
254 | // shouldn't bother heap-allocating.
255 | ic := i
256 | for {
257 | if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
258 | return i, false, true
259 | }
260 | p = atomic.LoadPointer(&e.p)
261 | if p == expungedPluginMap {
262 | return actual, false, false
263 | }
264 | if p != nil {
265 | return *(**Plugin)(p), true, true
266 | }
267 | }
268 | }
269 |
270 | // LoadAndDelete deletes the value for a key, returning the previous value if any.
271 | // The loaded result reports whether the key was present.
272 | func (m *PluginMap) LoadAndDelete(key string) (value *Plugin, loaded bool) {
273 | read, _ := m.read.Load().(readOnlyPluginMap)
274 | e, ok := read.m[key]
275 | if !ok && read.amended {
276 | m.mu.Lock()
277 | read, _ = m.read.Load().(readOnlyPluginMap)
278 | e, ok = read.m[key]
279 | if !ok && read.amended {
280 | e, ok = m.dirty[key]
281 | delete(m.dirty, key)
282 | // Regardless of whether the entry was present, record a miss: this key
283 | // will take the slow path until the dirty map is promoted to the read
284 | // map.
285 | m.missLocked()
286 | }
287 | m.mu.Unlock()
288 | }
289 | if ok {
290 | return e.delete()
291 | }
292 | return value, false
293 | }
294 |
295 | // Delete deletes the value for a key.
296 | func (m *PluginMap) Delete(key string) {
297 | m.LoadAndDelete(key)
298 | }
299 |
300 | func (e *entryPluginMap) delete() (value *Plugin, ok bool) {
301 | for {
302 | p := atomic.LoadPointer(&e.p)
303 | if p == nil || p == expungedPluginMap {
304 | return value, false
305 | }
306 | if atomic.CompareAndSwapPointer(&e.p, p, nil) {
307 | return *(**Plugin)(p), true
308 | }
309 | }
310 | }
311 |
312 | // Range calls f sequentially for each key and value present in the map.
313 | // If f returns false, range stops the iteration.
314 | //
315 | // Range does not necessarily correspond to any consistent snapshot of the Map's
316 | // contents: no key will be visited more than once, but if the value for any key
317 | // is stored or deleted concurrently, Range may reflect any mapping for that key
318 | // from any point during the Range call.
319 | //
320 | // Range may be O(N) with the number of elements in the map even if f returns
321 | // false after a constant number of calls.
322 | func (m *PluginMap) Range(f func(key string, value *Plugin) bool) {
323 | // We need to be able to iterate over all of the keys that were already
324 | // present at the start of the call to Range.
325 | // If read.amended is false, then read.m satisfies that property without
326 | // requiring us to hold m.mu for a long time.
327 | read, _ := m.read.Load().(readOnlyPluginMap)
328 | if read.amended {
329 | // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
330 | // (assuming the caller does not break out early), so a call to Range
331 | // amortizes an entire copy of the map: we can promote the dirty copy
332 | // immediately!
333 | m.mu.Lock()
334 | read, _ = m.read.Load().(readOnlyPluginMap)
335 | if read.amended {
336 | read = readOnlyPluginMap{m: m.dirty}
337 | m.read.Store(read)
338 | m.dirty = nil
339 | m.misses = 0
340 | }
341 | m.mu.Unlock()
342 | }
343 |
344 | for k, e := range read.m {
345 | v, ok := e.load()
346 | if !ok {
347 | continue
348 | }
349 | if !f(k, v) {
350 | break
351 | }
352 | }
353 | }
354 |
355 | func (m *PluginMap) missLocked() {
356 | m.misses++
357 | if m.misses < len(m.dirty) {
358 | return
359 | }
360 | m.read.Store(readOnlyPluginMap{m: m.dirty})
361 | m.dirty = nil
362 | m.misses = 0
363 | }
364 |
365 | func (m *PluginMap) dirtyLocked() {
366 | if m.dirty != nil {
367 | return
368 | }
369 |
370 | read, _ := m.read.Load().(readOnlyPluginMap)
371 | m.dirty = make(map[string]*entryPluginMap, len(read.m))
372 | for k, e := range read.m {
373 | if !e.tryExpungeLocked() {
374 | m.dirty[k] = e
375 | }
376 | }
377 | }
378 |
379 | func (e *entryPluginMap) tryExpungeLocked() (isExpunged bool) {
380 | p := atomic.LoadPointer(&e.p)
381 | for p == nil {
382 | if atomic.CompareAndSwapPointer(&e.p, nil, expungedPluginMap) {
383 | return true
384 | }
385 | p = atomic.LoadPointer(&e.p)
386 | }
387 | return p == expungedPluginMap
388 | }
389 |
--------------------------------------------------------------------------------
/pkg/config/setting.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
9 | "github.com/BurntSushi/toml"
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | type Setting struct {
14 | Platform string
15 | AppVersion string
16 | SignServer string
17 | }
18 |
19 | var SettingPath = "setting"
20 | var AllSetting *Setting = &Setting{}
21 |
22 | func AllSettings() *Setting {
23 | _, err := toml.DecodeFile("setting/setting.toml", AllSetting)
24 | if err != nil {
25 | return AllSetting
26 | }
27 | return AllSetting
28 | }
29 |
30 | func ReadSetting() Setting {
31 | tomlData := `# linux / macos / windows, 默认linux
32 | Platform = "linux"
33 | # linux[3.1.2-13107,3.2.10-25765] macos[6.9.20-17153] windows[9.9.12-25493]
34 | AppVersion = "3.1.2-13107"
35 | # 默认 linux 3.1.2-13107 可用 master:https://sign.lagrangecore.org/api/sign,Mirror:https://sign.0w0.ing/api/sign
36 | # linux 3.2.10-25765 暂不提供SignServer
37 | SignServer = "https://sign.lagrangecore.org/api/sign"
38 | `
39 | if !util.PathExists(SettingPath) {
40 | if err := os.MkdirAll(SettingPath, 0777); err != nil {
41 | log.Warnf("failed to mkdir")
42 | return *AllSetting
43 | }
44 | }
45 | _, err := os.Stat(fmt.Sprintf("%s/setting.toml", SettingPath))
46 | if err != nil {
47 | _ = os.WriteFile(fmt.Sprintf("%s/setting.toml", SettingPath), []byte(tomlData), 0644)
48 | log.Warn("已生成配置文件 conf.toml ,请修改后重新启动程序。")
49 | log.Info("该程序将于5秒后退出!")
50 | time.Sleep(time.Second * 5)
51 | os.Exit(1)
52 | }
53 | AllSetting = AllSettings()
54 | fmt.Println(AllSetting.AppVersion, AllSetting.Platform, AllSetting.SignServer)
55 | return *AllSetting
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/device/device.go:
--------------------------------------------------------------------------------
1 | package device
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "path"
8 |
9 | "github.com/2mf8/Go-Lagrange-Client/pkg/config"
10 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
11 | "github.com/2mf8/LagrangeGo/client/auth"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | // GetDevice
16 | // 如果设备文件夹不存在,自动创建文件夹
17 | // 使用种子生成随机设备信息
18 | // 如果已有设备文件,使用已有设备信息覆盖
19 | // 存储设备信息到文件
20 | func GetDevice(seed int64) *auth.DeviceInfo {
21 | // 默认 device/device-qq.json
22 | devicePath := path.Join("device", fmt.Sprintf("%d.json", seed))
23 |
24 | // 优先使用参数目录
25 | if config.Device != "" {
26 | devicePath = config.Device
27 | }
28 |
29 | deviceDir := path.Dir(devicePath)
30 | if !util.PathExists(deviceDir) {
31 | log.Infof("%+v 目录不存在,自动创建", deviceDir)
32 | if err := os.MkdirAll(deviceDir, 0777); err != nil {
33 | log.Warnf("failed to mkdir deviceDir, err: %+v", err)
34 | }
35 | }
36 |
37 | log.Info("生成随机设备信息")
38 | deviceInfo := auth.NewDeviceInfo(int(seed))
39 |
40 | if util.PathExists(devicePath) {
41 | log.Infof("使用 %s 内的设备信息覆盖设备信息", devicePath)
42 | fi, err := os.ReadFile(devicePath)
43 | if err != nil {
44 | util.FatalError(fmt.Errorf("failed to read device info, err: %+v", err))
45 | }
46 | err = json.Unmarshal(fi, deviceInfo)
47 | if err != nil {
48 | util.FatalError(fmt.Errorf("failed to load device info, err: %+v", err))
49 | }
50 | }
51 |
52 | log.Infof("保存设备信息到文件 %s", devicePath)
53 | data, err := json.Marshal(deviceInfo)
54 | if err != nil {
55 | log.Warnf("JSON 化设备信息文件 %s 失败", devicePath)
56 | }
57 | err = os.WriteFile(devicePath, data, 0644)
58 | if err != nil {
59 | log.Warnf("写设备信息文件 %s 失败", devicePath)
60 | }
61 | return deviceInfo
62 | }
--------------------------------------------------------------------------------
/pkg/download/download.go:
--------------------------------------------------------------------------------
1 | // Package download provide download utility functions
2 | package download
3 |
4 | import (
5 | "bufio"
6 | "compress/gzip"
7 | "crypto/tls"
8 | "fmt"
9 | "io"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "strconv"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/pkg/errors"
19 | "github.com/tidwall/gjson"
20 | )
21 |
22 | var client = &http.Client{
23 | Transport: &http.Transport{
24 | Proxy: func(request *http.Request) (*url.URL, error) {
25 | return http.ProxyFromEnvironment(request)
26 | },
27 | // Disable http2
28 | TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
29 | MaxIdleConnsPerHost: 999,
30 | },
31 | Timeout: time.Second * 5,
32 | }
33 |
34 | var clienth2 = &http.Client{
35 | Transport: &http.Transport{
36 | Proxy: func(request *http.Request) (*url.URL, error) {
37 | return http.ProxyFromEnvironment(request)
38 | },
39 | ForceAttemptHTTP2: true,
40 | MaxIdleConnsPerHost: 999,
41 | },
42 | Timeout: time.Second * 5,
43 | }
44 |
45 | // ErrOverSize 响应主体过大时返回此错误
46 | var ErrOverSize = errors.New("oversize")
47 |
48 | // UserAgent HTTP请求时使用的UA
49 | const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66"
50 |
51 | // SetTimeout set internal/download client timeout
52 | func SetTimeout(t time.Duration) {
53 | if t == 0 {
54 | t = time.Second * 10
55 | }
56 | client.Timeout = t
57 | clienth2.Timeout = t
58 | }
59 |
60 | // Request is a file download request
61 | type Request struct {
62 | Method string
63 | URL string
64 | Header map[string]string
65 | Limit int64
66 | Body io.Reader
67 | }
68 |
69 | func (r Request) client() *http.Client {
70 | if strings.Contains(r.URL, "go-cqhttp.org") {
71 | return clienth2
72 | }
73 | return client
74 | }
75 |
76 | func (r Request) do() (*http.Response, error) {
77 | if r.Method == "" {
78 | r.Method = http.MethodGet
79 | }
80 | req, err := http.NewRequest(r.Method, r.URL, r.Body)
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | req.Header["User-Agent"] = []string{UserAgent}
86 | for k, v := range r.Header {
87 | req.Header.Set(k, v)
88 | }
89 |
90 | return r.client().Do(req)
91 | }
92 |
93 | func (r Request) body() (io.ReadCloser, error) {
94 | resp, err := r.do()
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | limit := r.Limit // check file size limit
100 | if limit > 0 && resp.ContentLength > limit {
101 | _ = resp.Body.Close()
102 | return nil, ErrOverSize
103 | }
104 |
105 | if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
106 | return gzipReadCloser(resp.Body)
107 | }
108 | return resp.Body, err
109 | }
110 |
111 | // Bytes 对给定URL发送请求,返回响应主体
112 | func (r Request) Bytes() ([]byte, error) {
113 | rd, err := r.body()
114 | if err != nil {
115 | return nil, err
116 | }
117 | defer rd.Close()
118 | return io.ReadAll(rd)
119 | }
120 |
121 | // JSON 发送请求, 并转换响应为JSON
122 | func (r Request) JSON() (gjson.Result, error) {
123 | rd, err := r.body()
124 | if err != nil {
125 | return gjson.Result{}, err
126 | }
127 | defer rd.Close()
128 |
129 | var sb strings.Builder
130 | _, err = io.Copy(&sb, rd)
131 | if err != nil {
132 | return gjson.Result{}, err
133 | }
134 |
135 | return gjson.Parse(sb.String()), nil
136 | }
137 |
138 | func writeToFile(reader io.ReadCloser, path string) error {
139 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o644)
140 | if err != nil {
141 | return err
142 | }
143 | defer func() { _ = file.Close() }()
144 | _, err = file.ReadFrom(reader)
145 | return err
146 | }
147 |
148 | // WriteToFile 下载到制定目录
149 | func (r Request) WriteToFile(path string) error {
150 | rd, err := r.body()
151 | if err != nil {
152 | return err
153 | }
154 | defer rd.Close()
155 | return writeToFile(rd, path)
156 | }
157 |
158 | // WriteToFileMultiThreading 多线程下载到制定目录
159 | func (r Request) WriteToFileMultiThreading(path string, thread int) error {
160 | if thread < 2 {
161 | return r.WriteToFile(path)
162 | }
163 |
164 | limit := r.Limit
165 | type BlockMetaData struct {
166 | BeginOffset int64
167 | EndOffset int64
168 | DownloadedSize int64
169 | }
170 | var blocks []*BlockMetaData
171 | var contentLength int64
172 | errUnsupportedMultiThreading := errors.New("unsupported multi-threading")
173 | // 初始化分块或直接下载
174 | initOrDownload := func() error {
175 | header := make(map[string]string, len(r.Header))
176 | for k, v := range r.Header { // copy headers
177 | header[k] = v
178 | }
179 | header["range"] = "bytes=0-"
180 | req := Request{
181 | URL: r.URL,
182 | Header: header,
183 | }
184 | resp, err := req.do()
185 | if err != nil {
186 | return err
187 | }
188 | defer resp.Body.Close()
189 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
190 | return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
191 | }
192 | if resp.StatusCode == http.StatusOK {
193 | if limit > 0 && resp.ContentLength > limit {
194 | return ErrOverSize
195 | }
196 | if err = writeToFile(resp.Body, path); err != nil {
197 | return err
198 | }
199 | return errUnsupportedMultiThreading
200 | }
201 | if resp.StatusCode == http.StatusPartialContent {
202 | contentLength = resp.ContentLength
203 | if limit > 0 && resp.ContentLength > limit {
204 | return ErrOverSize
205 | }
206 | blockSize := contentLength
207 | if contentLength > 1024*1024 {
208 | blockSize = (contentLength / int64(thread)) - 10
209 | }
210 | if blockSize == contentLength {
211 | return writeToFile(resp.Body, path)
212 | }
213 | var tmp int64
214 | for tmp+blockSize < contentLength {
215 | blocks = append(blocks, &BlockMetaData{
216 | BeginOffset: tmp,
217 | EndOffset: tmp + blockSize - 1,
218 | })
219 | tmp += blockSize
220 | }
221 | blocks = append(blocks, &BlockMetaData{
222 | BeginOffset: tmp,
223 | EndOffset: contentLength - 1,
224 | })
225 | return nil
226 | }
227 | return errors.New("unknown status code")
228 | }
229 | // 下载分块
230 | downloadBlock := func(block *BlockMetaData) error {
231 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o666)
232 | if err != nil {
233 | return err
234 | }
235 | defer file.Close()
236 | _, _ = file.Seek(block.BeginOffset, io.SeekStart)
237 | writer := bufio.NewWriter(file)
238 | defer writer.Flush()
239 |
240 | header := make(map[string]string, len(r.Header))
241 | for k, v := range r.Header { // copy headers
242 | header[k] = v
243 | }
244 | header["range"] = fmt.Sprintf("bytes=%d-%d", block.BeginOffset, block.EndOffset)
245 | req := Request{
246 | URL: r.URL,
247 | Header: header,
248 | }
249 | resp, err := req.do()
250 | if err != nil {
251 | return err
252 | }
253 | defer resp.Body.Close()
254 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
255 | return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
256 | }
257 | buffer := make([]byte, 1024)
258 | i, err := resp.Body.Read(buffer)
259 | for {
260 | if err != nil && err != io.EOF {
261 | return err
262 | }
263 | i64 := int64(len(buffer[:i]))
264 | needSize := block.EndOffset + 1 - block.BeginOffset
265 | if i64 > needSize {
266 | i64 = needSize
267 | err = io.EOF
268 | }
269 | _, e := writer.Write(buffer[:i64])
270 | if e != nil {
271 | return e
272 | }
273 | block.BeginOffset += i64
274 | block.DownloadedSize += i64
275 | if err == io.EOF || block.BeginOffset > block.EndOffset {
276 | break
277 | }
278 | i, err = resp.Body.Read(buffer)
279 | }
280 | return nil
281 | }
282 |
283 | if err := initOrDownload(); err != nil {
284 | if err == errUnsupportedMultiThreading {
285 | return nil
286 | }
287 | return err
288 | }
289 | wg := sync.WaitGroup{}
290 | wg.Add(len(blocks))
291 | var lastErr error
292 | for i := range blocks {
293 | go func(b *BlockMetaData) {
294 | defer wg.Done()
295 | if err := downloadBlock(b); err != nil {
296 | lastErr = err
297 | }
298 | }(blocks[i])
299 | }
300 | wg.Wait()
301 | return lastErr
302 | }
303 |
304 | type gzipCloser struct {
305 | f io.Closer
306 | r *gzip.Reader
307 | }
308 |
309 | // gzipReadCloser 从 io.ReadCloser 创建 gunzip io.ReadCloser
310 | func gzipReadCloser(reader io.ReadCloser) (io.ReadCloser, error) {
311 | gzipReader, err := gzip.NewReader(reader)
312 | if err != nil {
313 | return nil, err
314 | }
315 | return &gzipCloser{
316 | f: reader,
317 | r: gzipReader,
318 | }, nil
319 | }
320 |
321 | // Read impls io.Reader
322 | func (g *gzipCloser) Read(p []byte) (n int, err error) {
323 | return g.r.Read(p)
324 | }
325 |
326 | // Close impls io.Closer
327 | func (g *gzipCloser) Close() error {
328 | _ = g.f.Close()
329 | return g.r.Close()
330 | }
331 |
--------------------------------------------------------------------------------
/pkg/gmc/gmc.go:
--------------------------------------------------------------------------------
1 | package gmc
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "os"
9 | "path"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "github.com/2mf8/Go-Lagrange-Client/pkg/bot"
15 | "github.com/2mf8/Go-Lagrange-Client/pkg/config"
16 | "github.com/2mf8/Go-Lagrange-Client/pkg/gmc/handler"
17 | "github.com/2mf8/Go-Lagrange-Client/pkg/static"
18 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
19 | "github.com/2mf8/LagrangeGo/client"
20 | auth2 "github.com/2mf8/LagrangeGo/client/auth"
21 |
22 | "github.com/gin-gonic/gin"
23 | rotatelogs "github.com/lestrrat-go/file-rotatelogs"
24 | "github.com/rifflock/lfshook"
25 | log "github.com/sirupsen/logrus"
26 | easy "github.com/t-tomalak/logrus-easy-formatter"
27 | )
28 |
29 | var (
30 | sms = false // 参数优先使用短信验证
31 | wsUrls = "" // websocket url
32 | port = 9000 // 端口号
33 | uin int64 = 0 // qq
34 | pass = "" //password
35 | device = "" // device file path
36 | help = false // help
37 | auth = ""
38 | )
39 |
40 | func init() {
41 | flag.BoolVar(&sms, "sms", false, "use sms captcha")
42 | flag.StringVar(&wsUrls, "ws_url", "", "websocket url")
43 | flag.IntVar(&port, "port", 9000, "admin http api port, 0 is random")
44 | flag.Int64Var(&uin, "uin", 0, "bot's qq")
45 | flag.StringVar(&pass, "pass", "", "bot's password")
46 | flag.StringVar(&device, "device", "", "device file")
47 | flag.BoolVar(&help, "help", false, "this help")
48 | flag.StringVar(&auth, "auth", "", "http basic auth: 'username,password'")
49 | flag.Parse()
50 | }
51 |
52 | func InitLog() {
53 | // 输出到命令行
54 | customFormatter := &log.TextFormatter{
55 | TimestampFormat: "2006-01-02 15:04:05",
56 | FullTimestamp: true,
57 | ForceColors: true,
58 | }
59 | log.SetFormatter(customFormatter)
60 | log.SetOutput(os.Stdout)
61 |
62 | // 输出到文件
63 | rotateLogs, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"),
64 | rotatelogs.WithLinkName(path.Join("logs", "latest.log")), // 最新日志软链接
65 | rotatelogs.WithRotationTime(time.Hour*24), // 每天一个新文件
66 | rotatelogs.WithMaxAge(time.Hour*24*3), // 日志保留3天
67 | )
68 | if err != nil {
69 | util.FatalError(err)
70 | return
71 | }
72 | log.AddHook(lfshook.NewHook(
73 | lfshook.WriterMap{
74 | log.InfoLevel: rotateLogs,
75 | log.WarnLevel: rotateLogs,
76 | log.ErrorLevel: rotateLogs,
77 | log.FatalLevel: rotateLogs,
78 | log.PanicLevel: rotateLogs,
79 | },
80 | &easy.Formatter{
81 | TimestampFormat: "2006-01-02 15:04:05",
82 | LogFormat: "[%time%] [%lvl%]: %msg% \r\n",
83 | },
84 | ))
85 | }
86 |
87 | func Login() {
88 | set := config.ReadSetting()
89 | appInfo := auth2.AppList[set.Platform][set.AppVersion]
90 | deviceInfo := &auth2.DeviceInfo{
91 | Guid: "cfcd208495d565ef66e7dff9f98764da",
92 | DeviceName: "Lagrange-DCFCD07E",
93 | SystemKernel: "Windows 10.0.22631",
94 | KernelVersion: "10.0.22631",
95 | }
96 |
97 | qqclient := client.NewClient(0, set.SignServer, appInfo)
98 | qqclient.UseDevice(deviceInfo)
99 | data, err := os.ReadFile("sig.bin")
100 | if err != nil {
101 | log.Warnln("read sig error:", err)
102 | } else {
103 | sig, err := auth2.UnmarshalSigInfo(data, true)
104 | if err != nil {
105 | log.Warnln("load sig error:", err)
106 | } else {
107 | qqclient.UseSig(sig)
108 | }
109 | }
110 | err = qqclient.Login("", "qrcode.png")
111 | if err != nil {
112 | log.Errorln("login err:", err)
113 | return
114 | }
115 | handler.AfterLogin(qqclient)
116 |
117 | defer qqclient.Release()
118 | select {}
119 | }
120 |
121 | func Start() {
122 | if help {
123 | flag.Usage()
124 | os.Exit(0)
125 | }
126 |
127 | InitLog() // 初始化日志
128 | config.LoadPlugins() // 如果文件存在,从文件读取gmc config
129 | LoadParamConfig() // 如果参数存在,从参数读取gmc config,并覆盖
130 | config.WritePlugins() // 内存中的gmc config写到文件
131 | config.Plugins.Range(func(key string, value *config.Plugin) bool {
132 | log.Infof("Plugin(%s): %s", value.Name, util.MustMarshal(value))
133 | return true
134 | })
135 | InitGin()
136 | //Login() // 初始化GIN HTTP管理
137 | handler.TokenLogin()
138 | }
139 |
140 | func LoadParamConfig() {
141 | // sms是true,如果本来是true,不变。如果本来是false,变true
142 | if sms {
143 | config.SMS = true
144 | }
145 |
146 | if wsUrls != "" {
147 | wsUrlList := strings.Split(wsUrls, ",")
148 | config.ClearPlugins(config.Plugins)
149 | for i, wsUrl := range wsUrlList {
150 | plugin := &config.Plugin{Name: strconv.Itoa(i), Urls: []string{wsUrl}}
151 | config.Plugins.Store(plugin.Name, plugin)
152 | }
153 | }
154 |
155 | if port != 9000 {
156 | config.Port = strconv.Itoa(port)
157 | }
158 |
159 | if device != "" {
160 | config.Device = device
161 | }
162 |
163 | if auth != "" {
164 | authSplit := strings.Split(auth, ",")
165 | if len(authSplit) == 2 {
166 | config.HttpAuth[authSplit[0]] = authSplit[1]
167 | } else {
168 | log.Warnf("auth 参数错误,正确格式: 'username,password'")
169 | }
170 | }
171 | }
172 |
173 | func InitGin() {
174 | gin.SetMode(gin.ReleaseMode)
175 | router := gin.New()
176 | router.Use(gin.Recovery())
177 | if len(config.HttpAuth) > 0 {
178 | router.Use(gin.BasicAuth(config.HttpAuth))
179 | }
180 |
181 | router.Use(handler.CORSMiddleware())
182 | router.StaticFS("/dashcard", http.FS(static.MustGetStatic()))
183 | router.POST("/dashcard/bot/delete/v1", handler.DeleteBot)
184 | router.POST("/dashcard/bot/list/v1", handler.ListBot)
185 | router.POST("/dashcard/qrcode/fetch/v1", handler.FetchQrCode)
186 | router.POST("/dashcard/qrcode/query/v1", handler.QueryQRCodeStatus)
187 | router.POST("/dashcard/plugin/list/v1", handler.ListPlugin)
188 | router.POST("/dashcard/plugin/save/v1", handler.SavePlugin)
189 | router.POST("/dashcard/plugin/delete/v1", handler.DeletePlugin)
190 | router.GET("/ui/ws", func(c *gin.Context) {
191 | if err := bot.UpgradeWebsocket(c.Writer, c.Request); err != nil {
192 | fmt.Println("创建机器人失败", err)
193 | }
194 | })
195 | realPort, err := RunGin(router, ":"+config.Port)
196 | if err != nil {
197 | for i := 9001; i <= 9020; i++ {
198 | config.Port = strconv.Itoa(i)
199 | realPort, err := RunGin(router, ":"+config.Port)
200 | if err != nil {
201 | log.Warn(fmt.Errorf("failed to run gin, err: %+v", err))
202 | continue
203 | }
204 | config.Port = realPort
205 | log.Infof("端口号 %s", realPort)
206 | log.Infof(fmt.Sprintf("浏览器打开 http://localhost:%s/dashcard 设置机器人", realPort))
207 | break
208 | }
209 | } else {
210 | config.Port = realPort
211 | log.Infof("端口号 %s", realPort)
212 | log.Infof(fmt.Sprintf("浏览器打开 http://localhost:%s/dashcard 设置机器人", realPort))
213 | }
214 | }
215 |
216 | func RunGin(engine *gin.Engine, port string) (string, error) {
217 | ln, err := net.Listen("tcp", port)
218 | if err != nil {
219 | return "", err
220 | }
221 | _, randPort, _ := net.SplitHostPort(ln.Addr().String())
222 | go func() {
223 | if err := http.Serve(ln, engine); err != nil {
224 | util.FatalError(fmt.Errorf("failed to serve http, err: %+v", err))
225 | }
226 | }()
227 | return randPort, nil
228 | }
229 |
--------------------------------------------------------------------------------
/pkg/gmc/handler/bot.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "os"
8 | "os/signal"
9 | "path"
10 | "strconv"
11 | "strings"
12 | "sync"
13 | "syscall"
14 | "time"
15 |
16 | "github.com/2mf8/Go-Lagrange-Client/pkg/bot"
17 | "github.com/2mf8/Go-Lagrange-Client/pkg/config"
18 | "github.com/2mf8/Go-Lagrange-Client/pkg/device"
19 | "github.com/2mf8/Go-Lagrange-Client/pkg/gmc/plugins"
20 | "github.com/2mf8/Go-Lagrange-Client/pkg/plugin"
21 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
22 | "github.com/2mf8/Go-Lagrange-Client/proto_gen/dto"
23 |
24 | "github.com/2mf8/LagrangeGo/client"
25 | "github.com/2mf8/LagrangeGo/client/auth"
26 | _ "github.com/BurntSushi/toml"
27 | "github.com/gin-gonic/gin"
28 | "github.com/gin-gonic/gin/binding"
29 | "github.com/golang/protobuf/proto"
30 | log "github.com/sirupsen/logrus"
31 | )
32 |
33 | var queryQRCodeMutex = &sync.RWMutex{}
34 | var qrCodeBot *client.QQClient
35 |
36 | type QRCodeResp int
37 |
38 | const (
39 | Unknown = iota
40 | QRCodeImageFetch
41 | QRCodeWaitingForScan
42 | QRCodeWaitingForConfirm
43 | QRCodeTimeout
44 | QRCodeConfirmed
45 | QRCodeCanceled
46 | )
47 |
48 | func TokenLogin() {
49 | set := config.ReadSetting()
50 | dfs, err := os.ReadDir("./device/")
51 | if err == nil {
52 | for _, v := range dfs {
53 | df := strings.Split(v.Name(), ".")
54 | uin, err := strconv.ParseInt(df[0], 10, 64)
55 | if err == nil {
56 | devi := device.GetDevice(uin)
57 | sfs, err := os.ReadDir("./sig/")
58 | if err == nil {
59 | for _, sv := range sfs {
60 | sf := strings.Split(sv.Name(), ".")
61 | if df[0] == sf[0] {
62 | sigpath := fmt.Sprintf("./sig/%s", sv.Name())
63 | data, err := os.ReadFile(sigpath)
64 | if err == nil {
65 | sig, err := auth.UnmarshalSigInfo(data, true)
66 | if err == nil {
67 | go func() {
68 | queryQRCodeMutex.Lock()
69 | defer queryQRCodeMutex.Unlock()
70 | appInfo := auth.AppList[set.Platform][set.AppVersion]
71 | cli := client.NewClient(0, set.SignServer, appInfo)
72 | cli.UseDevice(devi)
73 | cli.UseSig(sig)
74 | cli.SessionLogin()
75 | bot.Clients.Store(int64(cli.Uin), cli)
76 | go AfterLogin(cli)
77 | }()
78 | }
79 | }
80 | }
81 | }
82 | }
83 | } else {
84 | fmt.Printf("转换账号%s失败", df[0])
85 | }
86 | }
87 | }
88 | }
89 |
90 | func TokenReLogin(userId int64, retryInterval int, retryCount int) {
91 | set := config.ReadSetting()
92 | cli, ok := bot.Clients.Load(userId)
93 | if !ok {
94 | log.Warnf("%v 不存在,登录失败", userId)
95 | } else {
96 | var times = 0
97 | log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v",
98 | cli.Uin, retryInterval, times)
99 | if cli.Online.Load() {
100 | log.Warn("Bot已登录")
101 | return
102 | }
103 | if times < retryCount {
104 | times++
105 | cli.Disconnect()
106 | bot.Clients.Delete(int64(cli.Uin))
107 | bot.ReleaseClient(cli)
108 | time.Sleep(time.Second * time.Duration(retryInterval))
109 | devi := device.GetDevice(userId)
110 | sigpath := fmt.Sprintf("./sig/%v.bin", userId)
111 | fmt.Println(sigpath)
112 | data, err := os.ReadFile(sigpath)
113 | if err == nil {
114 | sig, err := auth.UnmarshalSigInfo(data, true)
115 | if err == nil {
116 | log.Warnf("%v 第 %v 次登录尝试", userId, times)
117 | appInfo := auth.AppList[set.Platform][set.AppVersion]
118 | cli := client.NewClient(0, set.SignServer, appInfo)
119 | cli.UseDevice(devi)
120 | cli.UseSig(sig)
121 | cli.SessionLogin()
122 | bot.Clients.Store(userId, cli)
123 | go AfterLogin(cli)
124 | } else {
125 | log.Warnf("%v 第 %v 次登录失败, 120秒后重试", userId, times)
126 | }
127 | } else {
128 | log.Warnf("%v 第 %v 次登录失败, 120秒后重试", userId, times)
129 | }
130 | } else {
131 | log.Errorf("failed to reconnect: 重连次数达到设置的上限值, %+v", cli.Uin)
132 | }
133 | }
134 | }
135 |
136 | func init() {
137 | log.Infof("加载日志插件 Log")
138 | plugin.AddPrivateMessagePlugin(plugins.LogPrivateMessage)
139 | plugin.AddGroupMessagePlugin(plugins.LogGroupMessage)
140 | log.Infof("加载测试插件 Hello")
141 | plugin.AddPrivateMessagePlugin(plugins.HelloPrivateMessage)
142 | plugin.AddGroupMessagePlugin(plugins.HelloGroupMessage)
143 | log.Infof("加载上报插件 Report")
144 | plugin.AddPrivateMessagePlugin(plugins.ReportPrivateMessage)
145 | plugin.AddGroupMessagePlugin(plugins.ReportGroupMessage)
146 | plugin.AddMemberJoinGroupPlugin(plugins.ReportMemberJoin)
147 | plugin.AddMemberLeaveGroupPlugin(plugins.ReportMemberLeave)
148 | plugin.AddNewFriendRequestPlugin(plugins.ReportNewFriendRequest)
149 | plugin.AddGroupInvitedRequestPlugin(plugins.ReportGroupInvitedRequest)
150 | plugin.AddGroupMessageRecalledPlugin(plugins.ReportGroupMessageRecalled)
151 | plugin.AddFriendMessageRecalledPlugin(plugins.ReportFriendMessageRecalled)
152 | plugin.AddNewFriendAddedPlugin(plugins.ReportNewFriendAdded)
153 | plugin.AddGroupMutePlugin(plugins.ReportGroupMute)
154 | }
155 |
156 | func DeleteBot(c *gin.Context) {
157 | req := &dto.DeleteBotReq{}
158 | err := Bind(c, req)
159 | if err != nil {
160 | c.String(http.StatusBadRequest, "bad request, not protobuf")
161 | return
162 | }
163 | cli, ok := bot.Clients.Load(req.BotId)
164 | if !ok {
165 | c.String(http.StatusBadRequest, "bot not exists")
166 | return
167 | }
168 | go func() {
169 | queryQRCodeMutex.Lock()
170 | defer queryQRCodeMutex.Unlock()
171 | sigpath := fmt.Sprintf("./sig/%v.bin", cli.Uin)
172 | sigDir := path.Dir(sigpath)
173 | if !util.PathExists(sigDir) {
174 | log.Infof("%+v 目录不存在,自动创建", sigDir)
175 | if err := os.MkdirAll(sigDir, 0777); err != nil {
176 | log.Warnf("failed to mkdir deviceDir, err: %+v", err)
177 | }
178 | }
179 | data, err := cli.Sig().Marshal()
180 | if err != nil {
181 | log.Errorln("marshal sig.bin err:", err)
182 | return
183 | }
184 | err = os.WriteFile(sigpath, data, 0644)
185 | if err != nil {
186 | log.Errorln("write sig.bin err:", err)
187 | return
188 | }
189 | log.Infoln("sig saved into sig.bin")
190 | }()
191 | bot.Clients.Delete(int64(cli.Uin))
192 | bot.ReleaseClient(cli)
193 | resp := &dto.DeleteBotResp{}
194 | Return(c, resp)
195 | }
196 |
197 | func ListBot(c *gin.Context) {
198 | req := &dto.ListBotReq{}
199 | err := Bind(c, req)
200 | if err != nil {
201 | c.String(http.StatusBadRequest, "bad request, not protobuf")
202 | return
203 | }
204 | var resp = &dto.ListBotResp{
205 | BotList: []*dto.Bot{},
206 | }
207 | bot.Clients.Range(func(_ int64, cli *client.QQClient) bool {
208 | resp.BotList = append(resp.BotList, &dto.Bot{
209 | BotId: int64(cli.Uin),
210 | IsOnline: cli.Online.Load(),
211 | })
212 | return true
213 | })
214 | Return(c, resp)
215 | }
216 |
217 | func FetchQrCode(c *gin.Context) {
218 | set := config.ReadSetting()
219 | req := &dto.FetchQRCodeReq{}
220 | err := Bind(c, req)
221 | if err != nil {
222 | c.String(http.StatusBadRequest, "bad request, not protobuf")
223 | return
224 | }
225 | newDeviceInfo := device.GetDevice(req.DeviceSeed)
226 | appInfo := auth.AppList[set.Platform][set.AppVersion]
227 | if err != nil {
228 | fmt.Println(err)
229 | } else {
230 | qqclient := client.NewClient(0, set.SignServer, appInfo)
231 | qqclient.UseDevice(newDeviceInfo)
232 | qrCodeBot = qqclient
233 | b, s, err := qrCodeBot.FetchQRCode(3, 4, 2)
234 | if err != nil {
235 | c.String(http.StatusInternalServerError, fmt.Sprintf("failed to fetch qrcode, %+v", err))
236 | return
237 | }
238 | resp := &dto.QRCodeLoginResp{
239 | State: dto.QRCodeLoginResp_QRCodeLoginState(http.StatusOK),
240 | ImageData: b,
241 | Sig: []byte(s),
242 | }
243 | Return(c, resp)
244 | }
245 | }
246 |
247 | func QueryQRCodeStatus(c *gin.Context) {
248 | respCode := 0
249 | ok, err := qrCodeBot.GetQRCodeResult()
250 | //fmt.Println(ok.Name(), ok.Waitable(), ok.Success(), err)
251 | if err != nil {
252 | resp := &dto.QRCodeLoginResp{
253 | State: dto.QRCodeLoginResp_QRCodeLoginState(http.StatusExpectationFailed),
254 | }
255 | Return(c, resp)
256 | }
257 | fmt.Println(ok.Name())
258 | if !ok.Success() {
259 | resp := &dto.QRCodeLoginResp{
260 | State: dto.QRCodeLoginResp_QRCodeLoginState(http.StatusExpectationFailed),
261 | }
262 | Return(c, resp)
263 | }
264 | if ok.Name() == "WaitingForConfirm" {
265 | respCode = QRCodeWaitingForScan
266 | }
267 | if ok.Name() == "Canceled" {
268 | respCode = QRCodeCanceled
269 | }
270 | if ok.Name() == "WaitingForConfirm" {
271 | respCode = QRCodeWaitingForConfirm
272 | }
273 | if ok.Name() == "Confirmed" {
274 | respCode = QRCodeConfirmed
275 | go func() {
276 | queryQRCodeMutex.Lock()
277 | defer queryQRCodeMutex.Unlock()
278 | err := qrCodeBot.QRCodeConfirmed()
279 | fmt.Println(err)
280 | if err == nil {
281 | go func() {
282 | queryQRCodeMutex.Lock()
283 | defer queryQRCodeMutex.Unlock()
284 | err := qrCodeBot.Register()
285 | if err != nil {
286 | fmt.Println(err)
287 | }
288 | time.Sleep(time.Second * 5)
289 | log.Infof("登录成功")
290 | originCli, ok := bot.Clients.Load(int64(qrCodeBot.Uin))
291 |
292 | // 重复登录,旧的断开
293 | if ok {
294 | originCli.Release()
295 | }
296 | bot.Clients.Store(int64(qrCodeBot.Uin), qrCodeBot)
297 | go AfterLogin(qrCodeBot)
298 | qrCodeBot = nil
299 | }()
300 | }
301 | }()
302 | }
303 | if ok.Name() == "Expired" {
304 | respCode = QRCodeTimeout
305 | }
306 | resp := &dto.QRCodeLoginResp{
307 | State: dto.QRCodeLoginResp_QRCodeLoginState(respCode),
308 | }
309 | Return(c, resp)
310 | }
311 |
312 | func ListPlugin(c *gin.Context) {
313 | req := &dto.ListPluginReq{}
314 | err := Bind(c, req)
315 | if err != nil {
316 | c.String(http.StatusBadRequest, "bad request")
317 | return
318 | }
319 | var resp = &dto.ListPluginResp{
320 | Plugins: []*dto.Plugin{},
321 | }
322 | config.Plugins.Range(func(key string, p *config.Plugin) bool {
323 | urls := []string{}
324 | url := strings.Join(p.Urls, ",")
325 | urls = append(urls, url)
326 | resp.Plugins = append(resp.Plugins, &dto.Plugin{
327 | Name: p.Name,
328 | Disabled: p.Disabled,
329 | Json: p.Json,
330 | Protocol: p.Protocol,
331 | Urls: urls,
332 | EventFilter: p.EventFilter,
333 | ApiFilter: p.ApiFilter,
334 | RegexFilter: p.RegexFilter,
335 | RegexReplace: p.RegexReplace,
336 | ExtraHeader: func() []*dto.Plugin_Header {
337 | headers := make([]*dto.Plugin_Header, 0)
338 | for k, v := range p.ExtraHeader {
339 | headers = append(headers, &dto.Plugin_Header{
340 | Key: k,
341 | Value: v,
342 | })
343 | }
344 | return headers
345 | }(),
346 | })
347 | return true
348 | })
349 | Return(c, resp)
350 | }
351 |
352 | func SavePlugin(c *gin.Context) {
353 | req := &dto.SavePluginReq{}
354 | urls := []string{}
355 | err := Bind(c, req)
356 | if err != nil {
357 | c.String(http.StatusBadRequest, "bad request")
358 | return
359 | }
360 | if req.Plugin == nil {
361 | c.String(http.StatusBadRequest, "plugin is nil")
362 | return
363 | }
364 | p := req.Plugin
365 | if p.ApiFilter == nil {
366 | p.ApiFilter = []int32{}
367 | }
368 | if p.EventFilter == nil {
369 | p.EventFilter = []int32{}
370 | }
371 | if p.Urls != nil {
372 | _urls := strings.Split(req.Plugin.Urls[0], ",")
373 | for _, v := range _urls {
374 | if v != "" {
375 | urls = append(urls, strings.TrimSpace(v))
376 | }
377 | }
378 | }
379 | config.Plugins.Store(p.Name, &config.Plugin{
380 | Name: p.Name,
381 | Disabled: p.Disabled,
382 | Json: p.Json,
383 | Protocol: p.Protocol,
384 | Urls: urls,
385 | EventFilter: p.EventFilter,
386 | ApiFilter: p.ApiFilter,
387 | RegexFilter: p.RegexFilter,
388 | RegexReplace: p.RegexReplace,
389 | ExtraHeader: func() map[string][]string {
390 | headers := map[string][]string{}
391 | for _, h := range p.ExtraHeader {
392 | headers[h.Key] = h.Value
393 | }
394 | return headers
395 | }(),
396 | })
397 | config.WritePlugins()
398 | resp := &dto.SavePluginResp{}
399 | Return(c, resp)
400 | }
401 |
402 | func DeletePlugin(c *gin.Context) {
403 | req := &dto.DeletePluginReq{}
404 | err := Bind(c, req)
405 | if err != nil {
406 | c.String(http.StatusBadRequest, "bad request")
407 | return
408 | }
409 | config.Plugins.Delete(req.Name)
410 | config.WritePlugins()
411 | resp := &dto.DeletePluginResp{}
412 | Return(c, resp)
413 | }
414 |
415 | func Return(c *gin.Context, resp proto.Message) {
416 | var (
417 | data []byte
418 | err error
419 | )
420 | switch c.ContentType() {
421 | case binding.MIMEPROTOBUF:
422 | data, err = proto.Marshal(resp)
423 | case binding.MIMEJSON:
424 | data, err = json.Marshal(resp)
425 | }
426 | if err != nil {
427 | c.String(http.StatusInternalServerError, "marshal resp error")
428 | return
429 | }
430 | c.Data(http.StatusOK, c.ContentType(), data)
431 | }
432 |
433 | func AfterLogin(cli *client.QQClient) {
434 | for {
435 | time.Sleep(5 * time.Second)
436 | if cli.Online.Load() {
437 | break
438 | }
439 | log.Warnf("机器人不在线,可能在等待输入验证码,或出错了。如果出错请重启。")
440 | }
441 | plugin.Serve(cli)
442 | log.Infof("插件加载完成")
443 |
444 | log.Infof("刷新好友列表")
445 | if fs, err := cli.GetFriendsData(); err != nil {
446 | util.FatalError(fmt.Errorf("failed to load friend list, err: %+v", err))
447 | } else {
448 | log.Infof("共加载 %v 个好友.", len(fs))
449 | }
450 |
451 | bot.ForwardBot = append(bot.ForwardBot, cli)
452 | bot.ConnectUniversal(cli)
453 |
454 | defer cli.Release()
455 | defer func() {
456 | sigpath := fmt.Sprintf("./sig/%v.bin", cli.Uin)
457 | sigDir := path.Dir(sigpath)
458 | if !util.PathExists(sigDir) {
459 | log.Infof("%+v 目录不存在,自动创建", sigDir)
460 | if err := os.MkdirAll(sigDir, 0777); err != nil {
461 | log.Warnf("failed to mkdir deviceDir, err: %+v", err)
462 | }
463 | }
464 | data, err := cli.Sig().Marshal()
465 | if err != nil {
466 | log.Errorln("marshal sig.bin err:", err)
467 | return
468 | }
469 | err = os.WriteFile(sigpath, data, 0644)
470 | if err != nil {
471 | log.Errorln("write sig.bin err:", err)
472 | return
473 | }
474 | log.Infoln("sig saved into sig.bin")
475 | }()
476 |
477 | // setup the main stop channel
478 | mc := make(chan os.Signal, 2)
479 | signal.Notify(mc, os.Interrupt, syscall.SIGTERM)
480 | for {
481 | switch <-mc {
482 | case os.Interrupt, syscall.SIGTERM:
483 | return
484 | }
485 | }
486 | }
487 |
--------------------------------------------------------------------------------
/pkg/gmc/handler/middlewares.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/golang/protobuf/proto"
9 | )
10 |
11 | func CORSMiddleware() gin.HandlerFunc {
12 | return func(c *gin.Context) {
13 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
14 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
15 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
16 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
17 |
18 | if c.Request.Method == "OPTIONS" {
19 | c.AbortWithStatus(204)
20 | return
21 | }
22 |
23 | c.Next()
24 | }
25 | }
26 |
27 | func Bind(c *gin.Context, req any) error {
28 | buf, err := ioutil.ReadAll(c.Request.Body)
29 | if err != nil {
30 | return err
31 | }
32 | if r, ok := req.(proto.Message); ok {
33 | if err := proto.Unmarshal(buf, r); err != nil {
34 | return err
35 | }
36 | } else {
37 | return errors.New("obj is not ProtoMessage")
38 | }
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/gmc/plugins/hello.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os"
8 | "time"
9 |
10 | "github.com/2mf8/Go-Lagrange-Client/pkg/bot"
11 | "github.com/2mf8/Go-Lagrange-Client/pkg/plugin"
12 | "github.com/2mf8/LagrangeGo/client"
13 | "github.com/2mf8/LagrangeGo/message"
14 | log "github.com/sirupsen/logrus"
15 | )
16 |
17 | func HelloPrivateMessage(cli *client.QQClient, event *message.PrivateMessage) int32 {
18 | if bot.MiraiMsgToRawMsg(cli, event.Elements) != "hi" {
19 | return plugin.MessageIgnore
20 | }
21 | elem := &message.SendingMessage{
22 | Elements: []message.IMessageElement{
23 | &message.TextElement{Content: "hello"},
24 | },
25 | }
26 | cli.SendPrivateMessage(event.Sender.Uin, elem.Elements)
27 | return plugin.MessageIgnore
28 | }
29 |
30 | func HelloGroupMessage(cli *client.QQClient, event *message.GroupMessage) int32 {
31 | if bot.MiraiMsgToRawMsg(cli, event.Elements) != "hi" {
32 | return plugin.MessageIgnore
33 | }
34 | resp, err := http.Get("https://www.2mf8.cn/static/image/cube3/b1.png")
35 | defer resp.Body.Close()
36 | fmt.Println(err)
37 | if err != nil {
38 | return plugin.MessageIgnore
39 | }
40 | imo, err := io.ReadAll(resp.Body)
41 | fmt.Println(err)
42 | if err != nil {
43 | return plugin.MessageIgnore
44 | }
45 | filename := fmt.Sprintf("%v.png", time.Now().UnixMicro())
46 | err = os.WriteFile(filename, imo, 0666)
47 | fmt.Println(err)
48 | if err != nil {
49 | return plugin.MessageIgnore
50 | }
51 | f, err := os.Open(filename)
52 | fmt.Println(err)
53 | if err != nil {
54 | return plugin.MessageIgnore
55 | }
56 | ir, err := cli.ImageUploadGroup(event.GroupUin, message.NewStreamImage(f))
57 | fmt.Println(err)
58 | if err != nil {
59 | return plugin.MessageIgnore
60 | }
61 | elem := &message.SendingMessage{}
62 | elem.Elements = append(elem.Elements, ir)
63 | elem.Elements = append(elem.Elements, &message.TextElement{
64 | Content: "测试成功",
65 | })
66 | r, e := cli.SendGroupMessage(event.GroupUin, elem.Elements)
67 | log.Warn(r, e)
68 | return plugin.MessageIgnore
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/gmc/plugins/log.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "github.com/2mf8/Go-Lagrange-Client/pkg/bot"
5 | "github.com/2mf8/Go-Lagrange-Client/pkg/plugin"
6 |
7 | "github.com/2mf8/LagrangeGo/client"
8 | "github.com/2mf8/LagrangeGo/message"
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | func LogPrivateMessage(cli *client.QQClient, event *message.PrivateMessage) int32 {
13 | log.Infof("Bot(%+v) Private(%+v) -> %+v\n", cli.Uin, event.Sender.Uin, bot.MiraiMsgToRawMsg(cli, event.Elements))
14 | return plugin.MessageIgnore
15 | }
16 |
17 | func LogGroupMessage(cli *client.QQClient, event *message.GroupMessage) int32 {
18 | //cli.MarkGroupMessageReaded(event.GroupCode, int64(event.Id)) // 标记为已读,可能可以减少风控
19 | log.Infof("Bot(%+v) Group(%+v) Sender(%+v) -> %+v\n", cli.Uin, event.GroupUin, event.Sender.Uin, bot.MiraiMsgToRawMsg(cli, event.Elements))
20 | return plugin.MessageIgnore
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/gmc/plugins/report.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/2mf8/Go-Lagrange-Client/pkg/bot"
8 | "github.com/2mf8/Go-Lagrange-Client/pkg/cache"
9 | "github.com/2mf8/Go-Lagrange-Client/pkg/plugin"
10 | "github.com/2mf8/Go-Lagrange-Client/proto_gen/onebot"
11 | log "github.com/sirupsen/logrus"
12 |
13 | "github.com/2mf8/LagrangeGo/client"
14 | "github.com/2mf8/LagrangeGo/client/event"
15 | "github.com/2mf8/LagrangeGo/message"
16 | )
17 |
18 | func ReportPrivateMessage(cli *client.QQClient, event *message.PrivateMessage) int32 {
19 | cache.PrivateMessageLru.Add(event.Id, event)
20 | eventProto := &onebot.Frame{
21 | FrameType: onebot.Frame_TPrivateMessageEvent,
22 | }
23 | eventProto.PbData = &onebot.Frame_PrivateMessageEvent{
24 | PrivateMessageEvent: &onebot.PrivateMessageEvent{
25 | Time: time.Now().Unix(),
26 | SelfId: int64(cli.Uin),
27 | PostType: "message",
28 | MessageType: "private",
29 | SubType: "normal",
30 | MessageId: event.Id,
31 | UserId: int64(event.Sender.Uin),
32 | Message: bot.MiraiMsgToProtoMsg(cli, event.Elements),
33 | RawMessage: bot.MiraiMsgToRawMsg(cli, event.Elements),
34 | Sender: &onebot.PrivateMessageEvent_Sender{
35 | UserId: int64(event.Sender.Uin),
36 | Nickname: event.Sender.Nickname,
37 | },
38 | },
39 | }
40 | bot.HandleEventFrame(cli, eventProto)
41 | return plugin.MessageIgnore
42 | }
43 |
44 | func ReportGroupMessage(cli *client.QQClient, event *message.GroupMessage) int32 {
45 | cache.GroupMessageLru.Add(event.Id, event)
46 | eventProto := &onebot.Frame{
47 | FrameType: onebot.Frame_TGroupMessageEvent,
48 | }
49 | groupMessageEvent := &onebot.GroupMessageEvent{
50 | Time: time.Now().Unix(),
51 | SelfId: int64(cli.Uin),
52 | PostType: "message",
53 | MessageType: "group",
54 | SubType: "normal",
55 | MessageId: event.Id,
56 | GroupId: int64(event.GroupUin),
57 | UserId: int64(event.Sender.Uin),
58 | Message: bot.MiraiMsgToProtoMsg(cli, event.Elements),
59 | RawMessage: bot.MiraiMsgToRawMsg(cli, event.Elements),
60 | Sender: &onebot.GroupMessageEvent_Sender{
61 | UserId: int64(event.Sender.Uin),
62 | Nickname: event.Sender.Nickname,
63 | Card: event.Sender.CardName,
64 | },
65 | }
66 |
67 | eventProto.PbData = &onebot.Frame_GroupMessageEvent{
68 | GroupMessageEvent: groupMessageEvent,
69 | }
70 | bot.HandleEventFrame(cli, eventProto)
71 | return plugin.MessageIgnore
72 | }
73 |
74 | func ReportMemberJoin(cli *client.QQClient, event *event.GroupMemberIncrease) int32 {
75 | eventProto := &onebot.Frame{
76 | FrameType: onebot.Frame_TGroupIncreaseNoticeEvent,
77 | }
78 | eventProto.PbData = &onebot.Frame_GroupIncreaseNoticeEvent{
79 | GroupIncreaseNoticeEvent: &onebot.GroupIncreaseNoticeEvent{
80 | Time: time.Now().Unix(),
81 | SelfId: int64(cli.Uin),
82 | PostType: "message",
83 | NoticeType: "group_increase",
84 | SubType: "approve",
85 | GroupId: int64(event.GroupUin),
86 | UserId: 0,
87 | OperatorId: 0,
88 | MemberUid: event.MemberUid,
89 | InvitorUid: event.InvitorUid,
90 | JoinType: event.JoinType,
91 | },
92 | }
93 | bot.HandleEventFrame(cli, eventProto)
94 | return plugin.MessageIgnore
95 | }
96 |
97 | func ReportMemberLeave(cli *client.QQClient, event *event.GroupMemberDecrease) int32 {
98 | eventProto := &onebot.Frame{
99 | FrameType: onebot.Frame_TGroupDecreaseNoticeEvent,
100 | }
101 | subType := "leave"
102 | var operatorUid string = ""
103 | if event.IsKicked() {
104 | subType = "kick"
105 | operatorUid = event.OperatorUid
106 | }
107 |
108 | eventProto.PbData = &onebot.Frame_GroupDecreaseNoticeEvent{
109 | GroupDecreaseNoticeEvent: &onebot.GroupDecreaseNoticeEvent{
110 | Time: time.Now().Unix(),
111 | SelfId: int64(cli.Uin),
112 | PostType: "message",
113 | NoticeType: "group_decrease",
114 | SubType: subType,
115 | GroupId: int64(event.GroupUin),
116 | MemberUid: event.MemberUid,
117 | OperatorUid: operatorUid,
118 | },
119 | }
120 | bot.HandleEventFrame(cli, eventProto)
121 | return plugin.MessageIgnore
122 | }
123 |
124 | func ReportJoinGroup(cli *client.QQClient, event *event.GroupMemberIncrease) int32 {
125 | eventProto := &onebot.Frame{
126 | FrameType: onebot.Frame_TGroupIncreaseNoticeEvent,
127 | }
128 | eventProto.PbData = &onebot.Frame_GroupIncreaseNoticeEvent{
129 | GroupIncreaseNoticeEvent: &onebot.GroupIncreaseNoticeEvent{
130 | Time: time.Now().Unix(),
131 | SelfId: int64(cli.Uin),
132 | PostType: "message",
133 | NoticeType: "group_increase",
134 | SubType: "approve",
135 | GroupId: int64(event.GroupUin),
136 | UserId: int64(cli.Uin),
137 | OperatorId: 0,
138 | MemberUid: event.MemberUid,
139 | JoinType: event.JoinType,
140 | InvitorUid: event.InvitorUid,
141 | },
142 | }
143 | bot.HandleEventFrame(cli, eventProto)
144 | return plugin.MessageIgnore
145 | }
146 |
147 | func ReportGroupMute(cli *client.QQClient, event *event.GroupMute) int32 {
148 | eventProto := &onebot.Frame{
149 | FrameType: onebot.Frame_TGroupBanNoticeEvent,
150 | }
151 | eventProto.PbData = &onebot.Frame_GroupBanNoticeEvent{
152 | GroupBanNoticeEvent: &onebot.GroupBanNoticeEvent{
153 | Time: time.Now().Unix(),
154 | SelfId: int64(cli.Uin),
155 | PostType: "notice",
156 | NoticeType: "group_ban",
157 | SubType: func() string {
158 | if event.Duration == 0 {
159 | return "lift_ban"
160 | }
161 | return "ban"
162 | }(),
163 | GroupId: int64(event.GroupUin),
164 | OperatorUid: event.OperatorUid,
165 | TargetUid: event.TargetUid,
166 | Duration: int64(event.Duration),
167 | },
168 | }
169 | bot.HandleEventFrame(cli, eventProto)
170 | return plugin.MessageIgnore
171 | }
172 |
173 | func ReportNewFriendRequest(cli *client.QQClient, event *event.NewFriendRequest) int32 {
174 | flag := strconv.FormatInt(int64(event.SourceUin), 10)
175 | cache.FriendRequestLru.Add(flag, event)
176 | eventProto := &onebot.Frame{
177 | FrameType: onebot.Frame_TFriendRequestEvent,
178 | }
179 | eventProto.PbData = &onebot.Frame_FriendRequestEvent{
180 | FriendRequestEvent: &onebot.FriendRequestEvent{
181 | Time: time.Now().Unix(),
182 | SelfId: int64(cli.Uin),
183 | PostType: "request",
184 | RequestType: "friend",
185 | Flag: flag,
186 | SourceUid: event.SourceUid,
187 | Msg: event.Msg,
188 | Source: event.Source,
189 | },
190 | }
191 | bot.HandleEventFrame(cli, eventProto)
192 | return plugin.MessageIgnore
193 | }
194 |
195 | func ReportUserJoinGroupRequest(cli *client.QQClient, event *event.GroupMemberJoinRequest) int32 {
196 | flag := strconv.FormatInt(int64(event.GroupUin), 10)
197 | cache.GroupRequestLru.Add(flag, event)
198 | eventProto := &onebot.Frame{
199 | FrameType: onebot.Frame_TGroupRequestEvent,
200 | }
201 | eventProto.PbData = &onebot.Frame_GroupRequestEvent{
202 | GroupRequestEvent: &onebot.GroupRequestEvent{
203 | Time: time.Now().Unix(),
204 | SelfId: int64(cli.Uin),
205 | PostType: "request",
206 | RequestType: "group",
207 | SubType: "add",
208 | GroupId: int64(event.GroupUin),
209 | Flag: flag,
210 | TargetUid: event.TargetUid,
211 | InvitorUid: event.InvitorUid,
212 | },
213 | }
214 | bot.HandleEventFrame(cli, eventProto)
215 | return plugin.MessageIgnore
216 | }
217 |
218 | func ReportGroupInvitedRequest(cli *client.QQClient, event *event.GroupInvite) int32 {
219 | flag := strconv.FormatInt(int64(event.GroupUin), 10)
220 | cache.GroupInvitedRequestLru.Add(flag, event)
221 | eventProto := &onebot.Frame{
222 | FrameType: onebot.Frame_TGroupRequestEvent,
223 | }
224 | eventProto.PbData = &onebot.Frame_GroupRequestEvent{
225 | GroupRequestEvent: &onebot.GroupRequestEvent{
226 | Time: time.Now().Unix(),
227 | SelfId: int64(cli.Uin),
228 | PostType: "request",
229 | RequestType: "group",
230 | SubType: "invite",
231 | GroupId: int64(event.GroupUin),
232 | InvitorUid: event.InvitorUid,
233 | Comment: "",
234 | Flag: flag,
235 | },
236 | }
237 | bot.HandleEventFrame(cli, eventProto)
238 | return plugin.MessageIgnore
239 | }
240 |
241 | func ReportGroupMessageRecalled(cli *client.QQClient, event *event.GroupRecall) int32 {
242 | opuin := cli.GetUin(event.OperatorUid, event.GroupUin)
243 | auuin := cli.GetUin(event.AuthorUid, event.GroupUin)
244 | if event.AuthorUid == event.OperatorUid {
245 | log.Infof("群 %v 内 %v(%s) 撤回了一条消息, 消息Id为 %v", event.GroupUin, auuin, event.AuthorUid, event.Sequence)
246 | } else {
247 | log.Infof("群 %v 内 %v(%s) 撤回了 %v(%s) 的一条消息, 消息Id为 %v", event.GroupUin, opuin, event.OperatorUid, auuin, event.AuthorUid, event.Sequence)
248 | }
249 | eventProto := &onebot.Frame{
250 | FrameType: onebot.Frame_TGroupRecallNoticeEvent,
251 | }
252 | eventProto.PbData = &onebot.Frame_GroupRecallNoticeEvent{
253 | GroupRecallNoticeEvent: &onebot.GroupRecallNoticeEvent{
254 | Time: time.Now().Unix(),
255 | SelfId: int64(cli.Uin),
256 | PostType: "notice",
257 | NoticeType: "group_recall",
258 | GroupId: int64(event.GroupUin),
259 | AuthorUid: event.AuthorUid,
260 | OperatorUid: event.OperatorUid,
261 | Sequence: event.Sequence,
262 | Random: event.Random,
263 | },
264 | }
265 | bot.HandleEventFrame(cli, eventProto)
266 | return plugin.MessageIgnore
267 | }
268 |
269 | func ReportFriendMessageRecalled(cli *client.QQClient, event *event.FriendRecall) int32 {
270 | log.Infof("好友 %s 撤回了一条消息, 消息Id为 %v", event.FromUid, event.Sequence)
271 | eventProto := &onebot.Frame{
272 | FrameType: onebot.Frame_TFriendRecallNoticeEvent,
273 | }
274 | eventProto.PbData = &onebot.Frame_FriendRecallNoticeEvent{
275 | FriendRecallNoticeEvent: &onebot.FriendRecallNoticeEvent{
276 | Time: time.Now().Unix(),
277 | SelfId: int64(cli.Uin),
278 | PostType: "notice",
279 | NoticeType: "friend_recall",
280 | FromUid: event.FromUid,
281 | MessageId: int32(event.Sequence),
282 | },
283 | }
284 | bot.HandleEventFrame(cli, eventProto)
285 | return plugin.MessageIgnore
286 | }
287 |
288 | func ReportNewFriendAdded(cli *client.QQClient, event *event.NewFriendRequest) int32 {
289 | eventProto := &onebot.Frame{
290 | FrameType: onebot.Frame_TFriendAddNoticeEvent,
291 | }
292 | eventProto.PbData = &onebot.Frame_FriendAddNoticeEvent{
293 | FriendAddNoticeEvent: &onebot.FriendAddNoticeEvent{
294 | Time: time.Now().Unix(),
295 | SelfId: int64(cli.Uin),
296 | PostType: "notice",
297 | NoticeType: "friend_add",
298 | UserId: int64(event.SourceUin),
299 | SourceUin: event.SourceUin,
300 | SourceUid: event.SourceUid,
301 | Source: event.Source,
302 | Msg: event.Msg,
303 | },
304 | }
305 | bot.HandleEventFrame(cli, eventProto)
306 | return plugin.MessageIgnore
307 | }
308 |
--------------------------------------------------------------------------------
/pkg/plugin/plugin.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/2mf8/LagrangeGo/client"
5 | "github.com/2mf8/LagrangeGo/client/event"
6 | "github.com/2mf8/LagrangeGo/message"
7 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
8 | )
9 |
10 | type (
11 | PrivateMessagePlugin = func(*client.QQClient, *message.PrivateMessage) int32
12 | GroupMessagePlugin = func(*client.QQClient, *message.GroupMessage) int32
13 | MemberJoinGroupPlugin = func(*client.QQClient, *event.GroupMemberIncrease) int32
14 | MemberLeaveGroupPlugin = func(*client.QQClient, *event.GroupMemberDecrease) int32
15 | JoinGroupPlugin = func(*client.QQClient, *event.GroupMemberJoinRequest) int32
16 | LeaveGroupPlugin = func(*client.QQClient, *event.GroupMemberDecrease) int32
17 | NewFriendRequestPlugin = func(*client.QQClient, *event.NewFriendRequest) int32
18 | UserJoinGroupRequestPlugin = func(*client.QQClient, *event.GroupMemberIncrease) int32
19 | GroupInvitedRequestPlugin = func(*client.QQClient, *event.GroupInvite) int32
20 | GroupMessageRecalledPlugin = func(*client.QQClient, *event.GroupRecall) int32
21 | FriendMessageRecalledPlugin = func(*client.QQClient, *event.FriendRecall) int32
22 | NewFriendAddedPlugin = func(*client.QQClient, *event.NewFriendRequest) int32
23 | GroupMutePlugin = func(*client.QQClient, *event.GroupMute) int32
24 | )
25 |
26 | var eclient *client.QQClient
27 |
28 | const (
29 | MessageIgnore = 0
30 | MessageBlock = 1
31 | )
32 |
33 | var PrivateMessagePluginList = make([]PrivateMessagePlugin, 0)
34 | var GroupMessagePluginList = make([]GroupMessagePlugin, 0)
35 | var MemberJoinGroupPluginList = make([]MemberJoinGroupPlugin, 0)
36 | var MemberLeaveGroupPluginList = make([]MemberLeaveGroupPlugin, 0)
37 | var JoinGroupPluginList = make([]JoinGroupPlugin, 0)
38 | var LeaveGroupPluginList = make([]LeaveGroupPlugin, 0)
39 | var NewFriendRequestPluginList = make([]NewFriendRequestPlugin, 0)
40 | var UserJoinGroupRequestPluginList = make([]UserJoinGroupRequestPlugin, 0)
41 | var GroupInvitedRequestPluginList = make([]GroupInvitedRequestPlugin, 0)
42 | var GroupMessageRecalledPluginList = make([]GroupMessageRecalledPlugin, 0)
43 | var FriendMessageRecalledPluginList = make([]FriendMessageRecalledPlugin, 0)
44 | var NewFriendAddedPluginList = make([]NewFriendAddedPlugin, 0)
45 | var GroupMutePluginList = make([]GroupMutePlugin, 0)
46 |
47 | func Serve(cli *client.QQClient) {
48 | cli.PrivateMessageEvent.Subscribe(handlePrivateMessage)
49 | cli.GroupMessageEvent.Subscribe(handleGroupMessage)
50 | cli.GroupMemberJoinEvent.Subscribe(handleMemberJoinGroup)
51 | cli.GroupMemberLeaveEvent.Subscribe(handleMemberLeaveGroup)
52 | cli.GroupMemberLeaveEvent.Subscribe(handleLeaveGroup)
53 | cli.NewFriendRequestEvent.Subscribe(handleNewFriendRequest)
54 | cli.GroupInvitedEvent.Subscribe(handleGroupInvitedRequest)
55 | cli.GroupRecallEvent.Subscribe(handleGroupMessageRecalled)
56 | cli.FriendRecallEvent.Subscribe(handleFriendMessageRecalled)
57 | cli.GroupMuteEvent.Subscribe(handleGroupMute)
58 | }
59 |
60 | // 添加私聊消息插件
61 | func AddPrivateMessagePlugin(plugin PrivateMessagePlugin) {
62 | PrivateMessagePluginList = append(PrivateMessagePluginList, plugin)
63 | }
64 |
65 | // 添加群聊消息插件
66 | func AddGroupMessagePlugin(plugin GroupMessagePlugin) {
67 | GroupMessagePluginList = append(GroupMessagePluginList, plugin)
68 | }
69 |
70 | // 添加群成员加入插件
71 | func AddMemberJoinGroupPlugin(plugin MemberJoinGroupPlugin) {
72 | MemberJoinGroupPluginList = append(MemberJoinGroupPluginList, plugin)
73 | }
74 |
75 | // 添加群成员离开插件
76 | func AddMemberLeaveGroupPlugin(plugin MemberLeaveGroupPlugin) {
77 | MemberLeaveGroupPluginList = append(MemberLeaveGroupPluginList, plugin)
78 | }
79 |
80 | // 添加机器人进群插件
81 | func AddJoinGroupPlugin(plugin JoinGroupPlugin) {
82 | JoinGroupPluginList = append(JoinGroupPluginList, plugin)
83 | }
84 |
85 | // 添加机器人离开群插件
86 | func AddLeaveGroupPlugin(plugin LeaveGroupPlugin) {
87 | LeaveGroupPluginList = append(LeaveGroupPluginList, plugin)
88 | }
89 |
90 | // 添加好友请求处理插件
91 | func AddNewFriendRequestPlugin(plugin NewFriendRequestPlugin) {
92 | NewFriendRequestPluginList = append(NewFriendRequestPluginList, plugin)
93 | }
94 |
95 | // 添加加群请求处理插件
96 | func AddUserJoinGroupRequestPlugin(plugin UserJoinGroupRequestPlugin) {
97 | UserJoinGroupRequestPluginList = append(UserJoinGroupRequestPluginList, plugin)
98 | }
99 |
100 | // 添加机器人被邀请处理插件
101 | func AddGroupInvitedRequestPlugin(plugin GroupInvitedRequestPlugin) {
102 | GroupInvitedRequestPluginList = append(GroupInvitedRequestPluginList, plugin)
103 | }
104 |
105 | // 添加群消息撤回处理插件
106 | func AddGroupMessageRecalledPlugin(plugin GroupMessageRecalledPlugin) {
107 | GroupMessageRecalledPluginList = append(GroupMessageRecalledPluginList, plugin)
108 | }
109 |
110 | // 添加好友消息撤回处理插件
111 | func AddFriendMessageRecalledPlugin(plugin FriendMessageRecalledPlugin) {
112 | FriendMessageRecalledPluginList = append(FriendMessageRecalledPluginList, plugin)
113 | }
114 |
115 | // 添加好友添加处理插件
116 | func AddNewFriendAddedPlugin(plugin NewFriendAddedPlugin) {
117 | NewFriendAddedPluginList = append(NewFriendAddedPluginList, plugin)
118 | }
119 |
120 | // 添加群成员被禁言插件
121 | func AddGroupMutePlugin(plugin GroupMutePlugin) {
122 | GroupMutePluginList = append(GroupMutePluginList, plugin)
123 | }
124 |
125 | func handlePrivateMessage(cli *client.QQClient, event *message.PrivateMessage) {
126 | util.SafeGo(func() {
127 | for _, plugin := range PrivateMessagePluginList {
128 | if result := plugin(cli, event); result == MessageBlock {
129 | break
130 | }
131 | }
132 | })
133 | }
134 |
135 | func handleGroupMessage(cli *client.QQClient, event *message.GroupMessage) {
136 | util.SafeGo(func() {
137 | for _, plugin := range GroupMessagePluginList {
138 | if result := plugin(cli, event); result == MessageBlock {
139 | break
140 | }
141 | }
142 | })
143 | }
144 |
145 | func handleMemberJoinGroup(cli *client.QQClient, event *event.GroupMemberIncrease) {
146 | util.SafeGo(func() {
147 | for _, plugin := range MemberJoinGroupPluginList {
148 | if result := plugin(cli, event); result == MessageBlock {
149 | break
150 | }
151 | }
152 | })
153 | }
154 |
155 | func handleMemberLeaveGroup(cli *client.QQClient, event *event.GroupMemberDecrease) {
156 | util.SafeGo(func() {
157 | for _, plugin := range MemberLeaveGroupPluginList {
158 | if result := plugin(cli, event); result == MessageBlock {
159 | break
160 | }
161 | }
162 | })
163 | }
164 |
165 | func handleLeaveGroup(cli *client.QQClient, event *event.GroupMemberDecrease) {
166 | util.SafeGo(func() {
167 | for _, plugin := range LeaveGroupPluginList {
168 | if result := plugin(cli, event); result == MessageBlock {
169 | break
170 | }
171 | }
172 | })
173 | }
174 |
175 | func handleNewFriendRequest(cli *client.QQClient, event *event.NewFriendRequest) {
176 | util.SafeGo(func() {
177 | for _, plugin := range NewFriendRequestPluginList {
178 | if result := plugin(cli, event); result == MessageBlock {
179 | break
180 | }
181 | }
182 | })
183 | }
184 |
185 | func handleGroupInvitedRequest(cli *client.QQClient, event *event.GroupInvite) {
186 | util.SafeGo(func() {
187 | for _, plugin := range GroupInvitedRequestPluginList {
188 | if result := plugin(cli, event); result == MessageBlock {
189 | break
190 | }
191 | }
192 | })
193 | }
194 |
195 | func handleGroupMessageRecalled(cli *client.QQClient, event *event.GroupRecall) {
196 | util.SafeGo(func() {
197 | for _, plugin := range GroupMessageRecalledPluginList {
198 | if result := plugin(cli, event); result == MessageBlock {
199 | break
200 | }
201 | }
202 | })
203 | }
204 |
205 | func handleFriendMessageRecalled(cli *client.QQClient, event *event.FriendRecall) {
206 | util.SafeGo(func() {
207 | for _, plugin := range FriendMessageRecalledPluginList {
208 | if result := plugin(cli, event); result == MessageBlock {
209 | break
210 | }
211 | }
212 | })
213 | }
214 |
215 | func handleGroupMute(cli *client.QQClient, event *event.GroupMute) {
216 | util.SafeGo(func() {
217 | for _, plugin := range GroupMutePluginList {
218 | if result := plugin(cli, event); result == MessageBlock {
219 | break
220 | }
221 | }
222 | })
223 | }
224 |
--------------------------------------------------------------------------------
/pkg/safe_ws/safe_ws.go:
--------------------------------------------------------------------------------
1 | package safe_ws
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/2mf8/Go-Lagrange-Client/pkg/util"
7 | "github.com/gorilla/websocket"
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | // safe websocket
12 | type SafeWebSocket struct {
13 | Conn *websocket.Conn
14 | SendChannel chan *WebSocketSendingMessage
15 | OnRecvMessage func(ws *SafeWebSocket, messageType int, data []byte)
16 | OnClose func()
17 | }
18 |
19 | type WebSocketSendingMessage struct {
20 | MessageType int
21 | Data []byte
22 | }
23 |
24 | func (ws *SafeWebSocket) Send(messageType int, data []byte) (e error) {
25 | defer func() {
26 | if err := recover(); err != nil { // 可能channel已被关闭,向已关闭的channel写入数据
27 | e = fmt.Errorf("failed to send websocket msg, %+v", err)
28 | log.Errorf("failed to send websocket msg, %+v", err)
29 | ws.Close()
30 | }
31 | }()
32 | ws.SendChannel <- &WebSocketSendingMessage{
33 | MessageType: messageType,
34 | Data: data,
35 | }
36 | e = nil
37 | return
38 | }
39 |
40 | func (ws *SafeWebSocket) Close() {
41 | defer func() {
42 | _ = recover() // 可能已经关闭过channel
43 | }()
44 | _ = ws.Conn.Close()
45 | ws.OnClose()
46 | close(ws.SendChannel)
47 | }
48 |
49 | func NewSafeWebSocket(conn *websocket.Conn, OnRecvMessage func(ws *SafeWebSocket, messageType int, data []byte), onClose func()) *SafeWebSocket {
50 | ws := &SafeWebSocket{
51 | Conn: conn,
52 | SendChannel: make(chan *WebSocketSendingMessage, 100),
53 | OnRecvMessage: OnRecvMessage,
54 | OnClose: onClose,
55 | }
56 |
57 | conn.SetCloseHandler(func(code int, text string) error {
58 | ws.Close()
59 | return nil
60 | })
61 |
62 | // 接受消息
63 | util.SafeGo(func() {
64 | for {
65 | messageType, data, err := conn.ReadMessage()
66 | if err != nil {
67 | log.Errorf("failed to read message, err: %+v", err)
68 | ws.Close()
69 | return
70 | }
71 | if messageType == websocket.PingMessage {
72 | if err := ws.Send(websocket.PongMessage, []byte("pong")); err != nil {
73 | ws.Close()
74 | }
75 | continue
76 | }
77 | util.SafeGo(func() {
78 | ws.OnRecvMessage(ws, messageType, data)
79 | })
80 | }
81 | })
82 |
83 | // 发送消息
84 | util.SafeGo(func() {
85 | for sendingMessage := range ws.SendChannel {
86 | if ws.Conn == nil {
87 | log.Errorf("failed to send websocket message, conn is nil")
88 | return
89 | }
90 | err := ws.Conn.WriteMessage(sendingMessage.MessageType, sendingMessage.Data)
91 | if err != nil {
92 | log.Errorf("failed to send websocket message, %+v", err)
93 | ws.Close()
94 | return
95 | }
96 | }
97 | })
98 | return ws
99 | }
100 |
--------------------------------------------------------------------------------
/pkg/static/static.go:
--------------------------------------------------------------------------------
1 | package static
2 |
3 | import (
4 | "embed"
5 | "io/fs"
6 | )
7 |
8 | // 需要把前端文件放在static文件夹
9 |
10 | //go:embed static
11 | var staticFs embed.FS
12 |
13 | func MustGetStatic() fs.FS {
14 | f, err := fs.Sub(staticFs, "static")
15 | if err != nil {
16 | panic(err)
17 | }
18 | return f
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/static/static/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "./static/css/main.13efcf60.chunk.css",
4 | "main.js": "./static/js/main.ebd0543e.chunk.js",
5 | "runtime-main.js": "./static/js/runtime-main.016cb37b.js",
6 | "static/css/2.26352ec4.chunk.css": "./static/css/2.26352ec4.chunk.css",
7 | "static/js/2.57752891.chunk.js": "./static/js/2.57752891.chunk.js",
8 | "static/js/3.a6cb6b59.chunk.js": "./static/js/3.a6cb6b59.chunk.js",
9 | "index.html": "./index.html",
10 | "static/js/2.57752891.chunk.js.LICENSE.txt": "./static/js/2.57752891.chunk.js.LICENSE.txt"
11 | },
12 | "entrypoints": [
13 | "static/js/runtime-main.016cb37b.js",
14 | "static/css/2.26352ec4.chunk.css",
15 | "static/js/2.57752891.chunk.js",
16 | "static/css/main.13efcf60.chunk.css",
17 | "static/js/main.ebd0543e.chunk.js"
18 | ]
19 | }
--------------------------------------------------------------------------------
/pkg/static/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProtobufBot/Go-Mirai-Client/c1f61084647ba0f9e41ea07673702d31a35b09a7/pkg/static/static/favicon.ico
--------------------------------------------------------------------------------
/pkg/static/static/index.html:
--------------------------------------------------------------------------------
1 |
ProtobufBot
--------------------------------------------------------------------------------
/pkg/static/static/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProtobufBot/Go-Mirai-Client/c1f61084647ba0f9e41ea07673702d31a35b09a7/pkg/static/static/logo192.png
--------------------------------------------------------------------------------
/pkg/static/static/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProtobufBot/Go-Mirai-Client/c1f61084647ba0f9e41ea07673702d31a35b09a7/pkg/static/static/logo512.png
--------------------------------------------------------------------------------
/pkg/static/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/static/static/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/pkg/static/static/static/css/main.13efcf60.chunk.css:
--------------------------------------------------------------------------------
1 | body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}.App{text-align:center;height:100vh;width:100vw}.logo{float:left;width:120px;height:31px;line-height:31px;margin:16px 24px 16px 0;color:#fff;font-size:20px;font-weight:700}.bot-card-col{display:flex;flex-direction:column;align-items:center;padding:16px}
--------------------------------------------------------------------------------
/pkg/static/static/static/js/2.57752891.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | Copyright (c) 2018 Jed Watson.
9 | Licensed under the MIT License (MIT), see
10 | http://jedwatson.github.io/classnames
11 | */
12 |
13 | /** @license React v0.20.2
14 | * scheduler.production.min.js
15 | *
16 | * Copyright (c) Facebook, Inc. and its affiliates.
17 | *
18 | * This source code is licensed under the MIT license found in the
19 | * LICENSE file in the root directory of this source tree.
20 | */
21 |
22 | /** @license React v16.13.1
23 | * react-is.production.min.js
24 | *
25 | * Copyright (c) Facebook, Inc. and its affiliates.
26 | *
27 | * This source code is licensed under the MIT license found in the
28 | * LICENSE file in the root directory of this source tree.
29 | */
30 |
31 | /** @license React v17.0.2
32 | * react-dom.production.min.js
33 | *
34 | * Copyright (c) Facebook, Inc. and its affiliates.
35 | *
36 | * This source code is licensed under the MIT license found in the
37 | * LICENSE file in the root directory of this source tree.
38 | */
39 |
40 | /** @license React v17.0.2
41 | * react-jsx-runtime.production.min.js
42 | *
43 | * Copyright (c) Facebook, Inc. and its affiliates.
44 | *
45 | * This source code is licensed under the MIT license found in the
46 | * LICENSE file in the root directory of this source tree.
47 | */
48 |
49 | /** @license React v17.0.2
50 | * react.production.min.js
51 | *
52 | * Copyright (c) Facebook, Inc. and its affiliates.
53 | *
54 | * This source code is licensed under the MIT license found in the
55 | * LICENSE file in the root directory of this source tree.
56 | */
57 |
--------------------------------------------------------------------------------
/pkg/static/static/static/js/3.a6cb6b59.chunk.js:
--------------------------------------------------------------------------------
1 | (this["webpackJsonppbbot-react-ui"]=this["webpackJsonppbbot-react-ui"]||[]).push([[3],{339:function(t,e,n){"use strict";n.r(e),n.d(e,"getCLS",(function(){return d})),n.d(e,"getFCP",(function(){return S})),n.d(e,"getFID",(function(){return F})),n.d(e,"getLCP",(function(){return k})),n.d(e,"getTTFB",(function(){return C}));var i,a,r,o,u=function(t,e){return{name:t,value:void 0===e?-1:e,delta:0,entries:[],id:"v1-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(t,e){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){if("first-input"===t&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(t){return t.getEntries().map(e)}));return n.observe({type:t,buffered:!0}),n}}catch(t){}},f=function(t,e){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(t(i),e&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(t){addEventListener("pageshow",(function(e){e.persisted&&t(e)}),!0)},p="function"==typeof WeakSet?new WeakSet:new Set,m=function(t,e,n){var i;return function(){e.value>=0&&(n||p.has(e)||"hidden"===document.visibilityState)&&(e.delta=e.value-(i||0),(e.delta||void 0===i)&&(i=e.value,t(e)))}},d=function(t,e){var n,i=u("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),n())},r=c("layout-shift",a);r&&(n=m(t,i,e),f((function(){r.takeRecords().map(a),n()})),s((function(){i=u("CLS",0),n=m(t,i,e)})))},v=-1,l=function(){return"hidden"===document.visibilityState?0:1/0},h=function(){f((function(t){var e=t.timeStamp;v=e}),!0)},g=function(){return v<0&&(v=l(),h(),s((function(){setTimeout((function(){v=l(),h()}),0)}))),{get timeStamp(){return v}}},S=function(t,e){var n,i=g(),a=u("FCP"),r=function(t){"first-contentful-paint"===t.name&&(f&&f.disconnect(),t.startTime=0&&a1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,e){var n=function(){w(t,e),a()},i=function(){a()},a=function(){removeEventListener("pointerup",n,y),removeEventListener("pointercancel",i,y)};addEventListener("pointerup",n,y),addEventListener("pointercancel",i,y)}(e,t):w(e,t)}},T=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(e){return t(e,b,y)}))},F=function(t,e){var n,r=g(),d=u("FID"),v=function(t){t.startTime 0 && resp.ContentLength > limit {
197 | return ErrOverSize
198 | }
199 | _, err = io.Copy(file, resp.Body)
200 | if err != nil {
201 | return err
202 | }
203 | return nil
204 | }
205 |
206 | func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, headers map[string]string) error {
207 | if threadCount < 2 {
208 | return DownloadFile(url, path, limit, headers)
209 | }
210 | type BlockMetaData struct {
211 | BeginOffset int64
212 | EndOffset int64
213 | DownloadedSize int64
214 | }
215 | var blocks []*BlockMetaData
216 | var contentLength int64
217 | errUnsupportedMultiThreading := errors.New("unsupported multi-threading")
218 | // 初始化分块或直接下载
219 | initOrDownload := func() error {
220 | copyStream := func(s io.ReadCloser) error {
221 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
222 | if err != nil {
223 | return err
224 | }
225 | defer file.Close()
226 | if _, err = io.Copy(file, s); err != nil {
227 | return err
228 | }
229 | return errUnsupportedMultiThreading
230 | }
231 | req, err := http.NewRequest("GET", url, nil)
232 | if err != nil {
233 | return err
234 | }
235 | if headers != nil {
236 | for k, v := range headers {
237 | req.Header.Set(k, v)
238 | }
239 | }
240 | if _, ok := headers["User-Agent"]; ok {
241 | req.Header["User-Agent"] = []string{UserAgent}
242 | }
243 | req.Header.Set("range", "bytes=0-")
244 | resp, err := client.Do(req)
245 | if err != nil {
246 | return err
247 | }
248 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
249 | return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
250 | }
251 | if resp.StatusCode == 200 {
252 | if limit > 0 && resp.ContentLength > limit {
253 | return ErrOverSize
254 | }
255 | return copyStream(resp.Body)
256 | }
257 | if resp.StatusCode == 206 {
258 | contentLength = resp.ContentLength
259 | if limit > 0 && resp.ContentLength > limit {
260 | return ErrOverSize
261 | }
262 | blockSize := func() int64 {
263 | if contentLength > 1024*1024 {
264 | return (contentLength / int64(threadCount)) - 10
265 | } else {
266 | return contentLength
267 | }
268 | }()
269 | if blockSize == contentLength {
270 | return copyStream(resp.Body)
271 | }
272 | var tmp int64
273 | for tmp+blockSize < contentLength {
274 | blocks = append(blocks, &BlockMetaData{
275 | BeginOffset: tmp,
276 | EndOffset: tmp + blockSize - 1,
277 | })
278 | tmp += blockSize
279 | }
280 | blocks = append(blocks, &BlockMetaData{
281 | BeginOffset: tmp,
282 | EndOffset: contentLength - 1,
283 | })
284 | return nil
285 | }
286 | return errors.New("unknown status code.")
287 | }
288 | // 下载分块
289 | downloadBlock := func(block *BlockMetaData) error {
290 | req, _ := http.NewRequest("GET", url, nil)
291 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
292 | if err != nil {
293 | return err
294 | }
295 | defer file.Close()
296 | _, _ = file.Seek(block.BeginOffset, io.SeekStart)
297 | writer := bufio.NewWriter(file)
298 | defer writer.Flush()
299 | if headers != nil {
300 | for k, v := range headers {
301 | req.Header.Set(k, v)
302 | }
303 | }
304 | if _, ok := headers["User-Agent"]; ok {
305 | req.Header["User-Agent"] = []string{UserAgent}
306 | }
307 | req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10))
308 | resp, err := client.Do(req)
309 | if err != nil {
310 | return err
311 | }
312 | defer resp.Body.Close()
313 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
314 | return errors.New("response status unsuccessful: " + strconv.FormatInt(int64(resp.StatusCode), 10))
315 | }
316 | var buffer = make([]byte, 1024)
317 | i, err := resp.Body.Read(buffer)
318 | for {
319 | if err != nil && err != io.EOF {
320 | return err
321 | }
322 | i64 := int64(len(buffer[:i]))
323 | needSize := block.EndOffset + 1 - block.BeginOffset
324 | if i64 > needSize {
325 | i64 = needSize
326 | err = io.EOF
327 | }
328 | _, e := writer.Write(buffer[:i64])
329 | if e != nil {
330 | return e
331 | }
332 | block.BeginOffset += i64
333 | block.DownloadedSize += i64
334 | if err == io.EOF || block.BeginOffset > block.EndOffset {
335 | break
336 | }
337 | i, err = resp.Body.Read(buffer)
338 | }
339 | return nil
340 | }
341 |
342 | if err := initOrDownload(); err != nil {
343 | if err == errUnsupportedMultiThreading {
344 | return nil
345 | }
346 | return err
347 | }
348 | wg := sync.WaitGroup{}
349 | wg.Add(len(blocks))
350 | var lastErr error
351 | for i := range blocks {
352 | go func(b *BlockMetaData) {
353 | defer wg.Done()
354 | if err := downloadBlock(b); err != nil {
355 | lastErr = err
356 | }
357 | }(blocks[i])
358 | }
359 | wg.Wait()
360 | return lastErr
361 | }
362 |
363 | // QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
364 | func QQMusicSongInfo(id string) (gjson.Result, error) {
365 | d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`)
366 | if err != nil {
367 | return gjson.Result{}, err
368 | }
369 | return gjson.ParseBytes(d).Get("songinfo.data"), nil
370 | }
371 |
372 | // NeteaseMusicSongInfo 通过给定id在wdd音乐上查找曲目信息
373 | func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
374 | d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id))
375 | if err != nil {
376 | return gjson.Result{}, err
377 | }
378 | return gjson.ParseBytes(d).Get("songs.0"), nil
379 | }
380 |
381 | func Convert(s string) string {
382 | reg := regexp.MustCompile(`"([0-9]+)"`)
383 | ss := reg.FindAllString(s, -1)
384 | for _, v := range ss {
385 | dr := regexp.MustCompile(`([0-9]+)`)
386 | ds := dr.FindAllString(v, -1)
387 | for _, jv := range ds {
388 | s = strings.ReplaceAll(s, v, jv)
389 | }
390 | }
391 | return s
392 | }
393 |
--------------------------------------------------------------------------------
/pkg/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "testing"
4 |
5 | func TestGetBytes(t *testing.T) {
6 | bytes, err := GetBytes("http://tnoodle.lz1998.xin/view/222.png?scramble=U2+R+U%27+F%27+U2+R%27+U%27+R+U2+R2+F%27")
7 | if err != nil {
8 | t.Error(err)
9 | }
10 | t.Logf("%+v", bytes)
11 | }
12 |
--------------------------------------------------------------------------------
/proto_gen/onebot/onebot_base.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.33.0
4 | // protoc v5.26.1
5 | // source: onebot_base.proto
6 |
7 | package onebot
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type Message struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 |
28 | Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
29 | Data map[string]string `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
30 | }
31 |
32 | func (x *Message) Reset() {
33 | *x = Message{}
34 | if protoimpl.UnsafeEnabled {
35 | mi := &file_onebot_base_proto_msgTypes[0]
36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
37 | ms.StoreMessageInfo(mi)
38 | }
39 | }
40 |
41 | func (x *Message) String() string {
42 | return protoimpl.X.MessageStringOf(x)
43 | }
44 |
45 | func (*Message) ProtoMessage() {}
46 |
47 | func (x *Message) ProtoReflect() protoreflect.Message {
48 | mi := &file_onebot_base_proto_msgTypes[0]
49 | if protoimpl.UnsafeEnabled && x != nil {
50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
51 | if ms.LoadMessageInfo() == nil {
52 | ms.StoreMessageInfo(mi)
53 | }
54 | return ms
55 | }
56 | return mi.MessageOf(x)
57 | }
58 |
59 | // Deprecated: Use Message.ProtoReflect.Descriptor instead.
60 | func (*Message) Descriptor() ([]byte, []int) {
61 | return file_onebot_base_proto_rawDescGZIP(), []int{0}
62 | }
63 |
64 | func (x *Message) GetType() string {
65 | if x != nil {
66 | return x.Type
67 | }
68 | return ""
69 | }
70 |
71 | func (x *Message) GetData() map[string]string {
72 | if x != nil {
73 | return x.Data
74 | }
75 | return nil
76 | }
77 |
78 | type ForwardMsg struct {
79 | state protoimpl.MessageState
80 | sizeCache protoimpl.SizeCache
81 | unknownFields protoimpl.UnknownFields
82 |
83 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
84 | Uin string `protobuf:"bytes,2,opt,name=uin,proto3" json:"uin,omitempty"`
85 | Content *IMessage `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
86 | }
87 |
88 | func (x *ForwardMsg) Reset() {
89 | *x = ForwardMsg{}
90 | if protoimpl.UnsafeEnabled {
91 | mi := &file_onebot_base_proto_msgTypes[1]
92 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
93 | ms.StoreMessageInfo(mi)
94 | }
95 | }
96 |
97 | func (x *ForwardMsg) String() string {
98 | return protoimpl.X.MessageStringOf(x)
99 | }
100 |
101 | func (*ForwardMsg) ProtoMessage() {}
102 |
103 | func (x *ForwardMsg) ProtoReflect() protoreflect.Message {
104 | mi := &file_onebot_base_proto_msgTypes[1]
105 | if protoimpl.UnsafeEnabled && x != nil {
106 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
107 | if ms.LoadMessageInfo() == nil {
108 | ms.StoreMessageInfo(mi)
109 | }
110 | return ms
111 | }
112 | return mi.MessageOf(x)
113 | }
114 |
115 | // Deprecated: Use ForwardMsg.ProtoReflect.Descriptor instead.
116 | func (*ForwardMsg) Descriptor() ([]byte, []int) {
117 | return file_onebot_base_proto_rawDescGZIP(), []int{1}
118 | }
119 |
120 | func (x *ForwardMsg) GetName() string {
121 | if x != nil {
122 | return x.Name
123 | }
124 | return ""
125 | }
126 |
127 | func (x *ForwardMsg) GetUin() string {
128 | if x != nil {
129 | return x.Uin
130 | }
131 | return ""
132 | }
133 |
134 | func (x *ForwardMsg) GetContent() *IMessage {
135 | if x != nil {
136 | return x.Content
137 | }
138 | return nil
139 | }
140 |
141 | type IMessage struct {
142 | state protoimpl.MessageState
143 | sizeCache protoimpl.SizeCache
144 | unknownFields protoimpl.UnknownFields
145 |
146 | Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
147 | Extra map[string]string `protobuf:"bytes,2,rep,name=extra,proto3" json:"extra,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
148 | }
149 |
150 | func (x *IMessage) Reset() {
151 | *x = IMessage{}
152 | if protoimpl.UnsafeEnabled {
153 | mi := &file_onebot_base_proto_msgTypes[2]
154 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
155 | ms.StoreMessageInfo(mi)
156 | }
157 | }
158 |
159 | func (x *IMessage) String() string {
160 | return protoimpl.X.MessageStringOf(x)
161 | }
162 |
163 | func (*IMessage) ProtoMessage() {}
164 |
165 | func (x *IMessage) ProtoReflect() protoreflect.Message {
166 | mi := &file_onebot_base_proto_msgTypes[2]
167 | if protoimpl.UnsafeEnabled && x != nil {
168 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
169 | if ms.LoadMessageInfo() == nil {
170 | ms.StoreMessageInfo(mi)
171 | }
172 | return ms
173 | }
174 | return mi.MessageOf(x)
175 | }
176 |
177 | // Deprecated: Use IMessage.ProtoReflect.Descriptor instead.
178 | func (*IMessage) Descriptor() ([]byte, []int) {
179 | return file_onebot_base_proto_rawDescGZIP(), []int{2}
180 | }
181 |
182 | func (x *IMessage) GetType() string {
183 | if x != nil {
184 | return x.Type
185 | }
186 | return ""
187 | }
188 |
189 | func (x *IMessage) GetExtra() map[string]string {
190 | if x != nil {
191 | return x.Extra
192 | }
193 | return nil
194 | }
195 |
196 | var File_onebot_base_proto protoreflect.FileDescriptor
197 |
198 | var file_onebot_base_proto_rawDesc = []byte{
199 | 0x0a, 0x11, 0x6f, 0x6e, 0x65, 0x62, 0x6f, 0x74, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x70, 0x72,
200 | 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6f, 0x6e, 0x65, 0x62, 0x6f, 0x74, 0x22, 0x85, 0x01, 0x0a, 0x07,
201 | 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
202 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x64,
203 | 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x6e, 0x65, 0x62,
204 | 0x6f, 0x74, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45,
205 | 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61,
206 | 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
207 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
208 | 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
209 | 0x02, 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x0a, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x4d, 0x73,
210 | 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
211 | 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01,
212 | 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65,
213 | 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x6e, 0x65, 0x62, 0x6f,
214 | 0x74, 0x2e, 0x49, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74,
215 | 0x65, 0x6e, 0x74, 0x22, 0x8b, 0x01, 0x0a, 0x08, 0x49, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
216 | 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
217 | 0x74, 0x79, 0x70, 0x65, 0x12, 0x31, 0x0a, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x18, 0x02, 0x20,
218 | 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6f, 0x6e, 0x65, 0x62, 0x6f, 0x74, 0x2e, 0x49, 0x4d, 0x65,
219 | 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
220 | 0x52, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x1a, 0x38, 0x0a, 0x0a, 0x45, 0x78, 0x74, 0x72, 0x61,
221 | 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
222 | 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
223 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
224 | 0x01, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x6f, 0x6e, 0x65, 0x62, 0x6f, 0x74, 0x62, 0x06, 0x70,
225 | 0x72, 0x6f, 0x74, 0x6f, 0x33,
226 | }
227 |
228 | var (
229 | file_onebot_base_proto_rawDescOnce sync.Once
230 | file_onebot_base_proto_rawDescData = file_onebot_base_proto_rawDesc
231 | )
232 |
233 | func file_onebot_base_proto_rawDescGZIP() []byte {
234 | file_onebot_base_proto_rawDescOnce.Do(func() {
235 | file_onebot_base_proto_rawDescData = protoimpl.X.CompressGZIP(file_onebot_base_proto_rawDescData)
236 | })
237 | return file_onebot_base_proto_rawDescData
238 | }
239 |
240 | var file_onebot_base_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
241 | var file_onebot_base_proto_goTypes = []interface{}{
242 | (*Message)(nil), // 0: onebot.Message
243 | (*ForwardMsg)(nil), // 1: onebot.ForwardMsg
244 | (*IMessage)(nil), // 2: onebot.IMessage
245 | nil, // 3: onebot.Message.DataEntry
246 | nil, // 4: onebot.IMessage.ExtraEntry
247 | }
248 | var file_onebot_base_proto_depIdxs = []int32{
249 | 3, // 0: onebot.Message.data:type_name -> onebot.Message.DataEntry
250 | 2, // 1: onebot.ForwardMsg.content:type_name -> onebot.IMessage
251 | 4, // 2: onebot.IMessage.extra:type_name -> onebot.IMessage.ExtraEntry
252 | 3, // [3:3] is the sub-list for method output_type
253 | 3, // [3:3] is the sub-list for method input_type
254 | 3, // [3:3] is the sub-list for extension type_name
255 | 3, // [3:3] is the sub-list for extension extendee
256 | 0, // [0:3] is the sub-list for field type_name
257 | }
258 |
259 | func init() { file_onebot_base_proto_init() }
260 | func file_onebot_base_proto_init() {
261 | if File_onebot_base_proto != nil {
262 | return
263 | }
264 | if !protoimpl.UnsafeEnabled {
265 | file_onebot_base_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
266 | switch v := v.(*Message); i {
267 | case 0:
268 | return &v.state
269 | case 1:
270 | return &v.sizeCache
271 | case 2:
272 | return &v.unknownFields
273 | default:
274 | return nil
275 | }
276 | }
277 | file_onebot_base_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
278 | switch v := v.(*ForwardMsg); i {
279 | case 0:
280 | return &v.state
281 | case 1:
282 | return &v.sizeCache
283 | case 2:
284 | return &v.unknownFields
285 | default:
286 | return nil
287 | }
288 | }
289 | file_onebot_base_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
290 | switch v := v.(*IMessage); i {
291 | case 0:
292 | return &v.state
293 | case 1:
294 | return &v.sizeCache
295 | case 2:
296 | return &v.unknownFields
297 | default:
298 | return nil
299 | }
300 | }
301 | }
302 | type x struct{}
303 | out := protoimpl.TypeBuilder{
304 | File: protoimpl.DescBuilder{
305 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
306 | RawDescriptor: file_onebot_base_proto_rawDesc,
307 | NumEnums: 0,
308 | NumMessages: 5,
309 | NumExtensions: 0,
310 | NumServices: 0,
311 | },
312 | GoTypes: file_onebot_base_proto_goTypes,
313 | DependencyIndexes: file_onebot_base_proto_depIdxs,
314 | MessageInfos: file_onebot_base_proto_msgTypes,
315 | }.Build()
316 | File_onebot_base_proto = out.File
317 | file_onebot_base_proto_rawDesc = nil
318 | file_onebot_base_proto_goTypes = nil
319 | file_onebot_base_proto_depIdxs = nil
320 | }
321 |
--------------------------------------------------------------------------------
/scripts/env_run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | CMD="gmc"
4 |
5 | if [ $UIN ];then
6 | CMD="$CMD -uin $UIN"
7 | fi
8 |
9 | if [ $PASS ];then
10 | CMD="$CMD -pass $PASS"
11 | fi
12 |
13 | if [ $PORT ];then
14 | CMD="$CMD -port $PORT"
15 | fi
16 |
17 | if [ $WS_URL ];then
18 | CMD="$CMD -ws_url $WS_URL"
19 | fi
20 |
21 | if [ $SMS ];then
22 | CMD="$CMD -sms $SMS"
23 | fi
24 |
25 | if [ $DEVICE ];then
26 | CMD="$CMD -device $DEVICE"
27 | fi
28 |
29 | if [ $AUTH ];then
30 | CMD="$CMD -auth $AUTH"
31 | fi
32 | echo $CMD
33 | eval $CMD
--------------------------------------------------------------------------------
/scripts/linux_run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | read -p "Enter your bot qq:" uin
4 | read -p "Enter your bot password:" pass
5 |
6 | # 根据情况修改使用哪个
7 | Go-Mirai-Client-linux-amd64 -uin "$uin" -pass "$pass"
--------------------------------------------------------------------------------
/scripts/windows_run.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | echo ================================================
4 | echo Go-Mirai-Client
5 | echo You can modify this file to login automatically.
6 | echo You can copy this file to manage many bots.
7 | echo https://github.com/ProtobufBot/go-Mirai-Client
8 | echo ================================================
9 |
10 | set port=9000
11 |
12 | set /p uin=QQ:
13 | set /p pass=Password:
14 | set /p port=Port(9000-60000):
15 |
16 |
17 | Go-Mirai-Client-windows-amd64.exe -uin %uin% -pass %pass% -port %port%
--------------------------------------------------------------------------------
/service/glc/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/2mf8/Go-Lagrange-Client/pkg/gmc"
4 |
5 | func main() {
6 | gmc.Start()
7 | select {}
8 | }
9 |
--------------------------------------------------------------------------------
/service/gmc_android/gmc.go:
--------------------------------------------------------------------------------
1 | package gmc_android
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/2mf8/Go-Lagrange-Client/pkg/config"
7 | "github.com/2mf8/Go-Lagrange-Client/pkg/gmc"
8 |
9 | log "github.com/sirupsen/logrus"
10 | _ "golang.org/x/mobile/bind"
11 | )
12 |
13 | // gomobile bind -target=android -androidapi=21 ./service/gmc_android
14 | var logger AndroidLogger
15 |
16 | func SetSms(sms bool) {
17 | config.SMS = sms
18 | }
19 |
20 | func Chdir(dir string) {
21 | _ = os.Chdir(dir)
22 | }
23 |
24 | func Start() {
25 | gmc.Start()
26 | }
27 |
28 | func SetLogger(androidLogger AndroidLogger) {
29 | logger = androidLogger
30 | log.SetOutput(&AndroidWriter{})
31 | log.SetFormatter(&AndroidFormatter{})
32 | }
33 |
--------------------------------------------------------------------------------
/service/gmc_android/logger.go:
--------------------------------------------------------------------------------
1 | package gmc_android
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 |
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | type AndroidLogger interface {
11 | Log(str string)
12 | }
13 |
14 | type AndroidWriter struct {
15 | }
16 |
17 | func (AndroidWriter) Write(p []byte) (n int, err error) {
18 | if logger != nil {
19 | logger.Log(string(p))
20 | }
21 | return 0, nil
22 | }
23 |
24 | type AndroidFormatter struct {
25 | }
26 |
27 | func (AndroidFormatter) Format(entry *log.Entry) ([]byte, error) {
28 | buf := bytes.Buffer{}
29 | buf.WriteByte('[')
30 | buf.WriteString(entry.Time.Format("2006-01-02 15:04:05"))
31 | buf.WriteString("] [")
32 | buf.WriteString(strings.ToUpper(entry.Level.String()))
33 | buf.WriteString("]: ")
34 | buf.WriteString(entry.Message)
35 | buf.WriteString(" \n")
36 | buf.Bytes()
37 | return append([]byte(nil), buf.Bytes()...), nil
38 | }
39 |
40 |
--------------------------------------------------------------------------------