├── .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(``, 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 | --------------------------------------------------------------------------------