diff --git a/.env b/.env new file mode 100644 index 00000000..920b3df9 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# DB_TYPE=mysql +# DB_DSN="root:root@tcp(127.0.0.1:3306)/nokoti?charset=utf8mb4&parseTime=True&loc=Local" + +# DB_TYPE=postgres +# DB_DSN="postgres://postgres:pinenut666@localhost:5432/nokoti?sslmode=disable" +# SQLITE IS DIFFERENT +DB_TYPE=sqlite +# DATA_DIR="./data/default" diff --git a/.github/workflows/cleanup-caches.yml b/.github/workflows/cleanup-caches.yml new file mode 100644 index 00000000..58a8611e --- /dev/null +++ b/.github/workflows/cleanup-caches.yml @@ -0,0 +1,29 @@ +name: Cleanup Caches from Pull Request +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml new file mode 100644 index 00000000..a5f56aba --- /dev/null +++ b/.github/workflows/reviewdog.yml @@ -0,0 +1,39 @@ +name: Review Dog + +on: + pull_request_target: + paths: + - '**.go' + - 'go.mod' + - '.github/workflows/reviewdog.yml' + +jobs: + review-dog: + permissions: + checks: write + contents: read + pull-requests: write + name: Review Dog + runs-on: ubuntu-latest + steps: + - name: Code + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + - run: go get + - run: go generate ./... + - name: Set Up GolangCI-Lint + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0 + - uses: reviewdog/action-setup@v1 + with: + reviewdog_version: latest + - name: Run golangci-lint & reviewdog + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + golangci-lint run | reviewdog -reporter=github-pr-review -f=golangci-lint -filter-mode=nofilter -fail-on-error \ No newline at end of file diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml new file mode 100644 index 00000000..62cf5b77 --- /dev/null +++ b/.github/workflows/test-and-lint.yml @@ -0,0 +1,37 @@ +name: Test & Lint + +on: + push: + paths: + - '**.go' + - 'go.mod' + - 'test-and-lint.yml' + pull_request: + paths: + - '**.go' + - 'go.mod' + - 'test-and-lint.yml' + +jobs: + test-and-lint: + name: Test & Lint + runs-on: ubuntu-latest + steps: + - name: Code + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + - run: go get + - run: go generate ./... + + - run: go test -v -race ./... + - run: go vet ./... + + - name: GolangCI-Lint + uses: golangci/golangci-lint-action@v6 + if: github.event_name != 'pull_request' + with: + version: 'v1.61.0' + args: '--timeout 9999s' \ No newline at end of file diff --git a/.github/workflows/test_and_lint.yml b/.github/workflows/test_and_lint.yml deleted file mode 100644 index a2f3f22a..00000000 --- a/.github/workflows/test_and_lint.yml +++ /dev/null @@ -1,49 +0,0 @@ -on: - push: - paths: - - '**.go' - - 'go.mod' - - '.github/workflows/test_and_lint.yml' - pull_request: - paths: - - '**.go' - - 'go.mod' - - '.github/workflows/test_and_lint.yml' - -name: Test & Lint - -jobs: - test-and-lint: - runs-on: ubuntu-latest - steps: - - name: Code - uses: actions/checkout@v4 - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: '1.20' - cache: false - - run: go get - - run: go generate ./... - - - run: go test -v ./... - - run: go vet ./... - - - name: GolangCI-Lint - uses: golangci/golangci-lint-action@v3 - if: github.event.name == 'pull_request' - with: - version: 'v1.55.2' - args: '--timeout 9999s' - only-new-issues: true - skip-pkg-cache: true - skip-build-cache: true - - - name: GolangCI-Lint - uses: golangci/golangci-lint-action@v3 - if: github.event.name != 'pull_request' - with: - version: 'v1.55.2' - args: '--timeout 9999s' - skip-pkg-cache: true - skip-build-cache: true diff --git a/.gitignore b/.gitignore index 61484bea..701fbea2 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ _help_cache .vscode/ !.vscode/settings.json +!.vscode/extensions.json +sealdice-lock.lock diff --git a/.golangci.yml b/.golangci.yml index 5de02ea0..c08655b0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ # This code is licensed under the terms of the MIT license https://opensource.org/license/mit # Copyright (c) 2021 Marat Reymers -## Golden config for golangci-lint v1.54.2 +## Golden config for golangci-lint v1.60.3 # # This is the best config for golangci-lint based on my experience and opinion. # It is very strict, but not extremely strict. @@ -12,7 +12,6 @@ run: # Default: 1m timeout: 3m - # This file contains only configs which differ from defaults. # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters-settings: @@ -54,9 +53,11 @@ linters-settings: - G101 - G501 - G505 + - G115 exhaustruct: - # List of regular expressions to exclude struct packages and names from check. + # List of regular expressions to exclude struct packages and their names from checks. + # Regular expressions must match complete canonical struct package/name/structname. # Default: [] exclude: # std libs @@ -128,25 +129,6 @@ linters-settings: # Default: "" local-prefixes: sealdice-core - gomnd: - # List of function patterns to exclude from analysis. - # Values always ignored: `time.Date`, - # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, - # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. - # Default: [] - ignored-functions: - - flag.Arg - - flag.Duration.* - - flag.Float.* - - flag.Int.* - - flag.Uint.* - - os.Chmod - - os.Mkdir.* - - os.OpenFile - - os.WriteFile - - prometheus.ExponentialBuckets.* - - prometheus.LinearBuckets - gomodguard: blocked: # List of blocked modules. @@ -162,8 +144,8 @@ linters-settings: reason: "satori's package is not maintained" - github.com/gofrs/uuid: recommendations: - - github.com/google/uuid - reason: "gofrs' package is not go module" + - github.com/gofrs/uuid/v5 + reason: "gofrs' package was not go module before v5" govet: # Enable all analyzers. @@ -174,7 +156,6 @@ linters-settings: # Default: [] disable: - fieldalignment # too strict - # - shadow # TOOOOOO noisy # Settings per analyzer. settings: shadow: @@ -182,6 +163,31 @@ linters-settings: # Default: false strict: false + inamedparam: + # Skips check for interface methods with only a single parameter. + # Default: false + skip-single-param: true + + mnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - args.Error + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + nakedret: # Make an issue if func has more lines of code than this setting, and it has naked returns. # Default: 30 @@ -190,7 +196,7 @@ linters-settings: nolintlint: # Exclude following linters from requiring an explanation. # Default: [] - allow-no-explanation: [ funlen, gocognit, lll ] + allow-no-explanation: [funlen, gocognit, lll] # Enable to require an explanation of nonzero length after each nolint directive. # Default: false require-explanation: false @@ -199,18 +205,102 @@ linters-settings: require-specific: false allow-unused: true + perfsprint: + # Optimizes into strings concatenation. + # Default: true + strconcat: false + rowserrcheck: # database/sql is always checked # Default: [] packages: - github.com/jmoiron/sqlx + sloglint: + # Enforce not using global loggers. + # Values: + # - "": disabled + # - "all": report all global loggers + # - "default": report only the default slog logger + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global + # Default: "" + no-global: "default" + # Enforce using methods that accept a context. + # Values: + # - "": disabled + # - "all": report all contextless calls + # - "scope": report only if a context exists in the scope of the outermost function + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only + # Default: "" + context: "scope" + tenv: # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. # Default: false all: true + stylecheck: + # STxxxx checks in https://staticcheck.io/docs/configuration/options/#checks + # Default: ["*"] + checks: + ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] + # https://staticcheck.io/docs/configuration/options/#dot_import_whitelist + # Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"] + dot-import-whitelist: + - fmt + # https://staticcheck.io/docs/configuration/options/#initialisms + # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] + initialisms: + [ + "ACL", + "ASCII", + "CPU", + "CSS", + "DNS", + "EOF", + "GUID", + "HTML", + "HTTP", + "HTTPS", + "ID", + "IP", + "JSON", + "QPS", + "RAM", + "RPC", + "SLA", + "SMTP", + "SQL", + "SSH", + "TCP", + "TLS", + "TTL", + "UDP", + "UI", + "GID", + "UID", + "UUID", + "URI", + "UTF8", + "VM", + "XML", + "XMPP", + "XSRF", + "XSS", + "SIP", + "RTP", + "AMQP", + "DB", + "TS", + ] + # https://staticcheck.io/docs/configuration/options/#http_status_code_whitelist + # Default: ["200", "400", "404", "500"] + http-status-code-whitelist: ["200", "400", "404", "500"] + + forbidigo: + exclude-godoc-examples: true + analyze-types: true linters: disable-all: true @@ -228,53 +318,64 @@ linters: - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - #TooMany - cyclop # checks function and package cyclomatic complexity - # NOTE(Xiangze Li): disabled due to existing bad code - #- dupl # tool for code clone detection + - canonicalheader # checks whether net/http.Header uses canonical header + - copyloopvar # detects places where loop variables are copied (Go 1.22+) + # - cyclop # checks function and package cyclomatic complexity + # - dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - - execinquery # checks query string in Query function which reads your Go src files and warning it finds - exhaustive # checks exhaustiveness of enum switch statements - - exportloopref # checks for pointers to enclosing loop variables - #- forbidigo # forbids identifiers - #- funlen # tool for detection of long functions + - fatcontext # detects nested contexts in loops + - forbidigo # forbids identifiers + # - funlen # tool for detection of long functions - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + # - gochecknoglobals # checks that no global variables exist - gochecknoinits # checks that no init functions are present in Go code - #TooMany - gocognit # computes and checks the cognitive complexity of functions - #TODO - goconst # finds repeated strings that could be replaced by a constant + - gochecksumtype # checks exhaustiveness on Go "sum types" + # - gocognit # computes and checks the cognitive complexity of functions + # - goconst # finds repeated strings that could be replaced by a constant - gocritic # provides diagnostics that check for bugs, performance and style issues - #TooMany - gocyclo # computes and checks the cyclomatic complexity of functions - #- godot # checks if comments end in a period + # - gocyclo # computes and checks the cyclomatic complexity of functions + # - godot # checks if comments end in a period - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt - #- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + # - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations - goprintffuncname # checks that printf-like functions are named with f at the end - gosec # inspects source code for security problems - #TooMany - lll # reports long lines + - intrange # finds places where for loops could make use of an integer range + # - lll # reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) - makezero # finds slice declarations with non-zero initial length - # - mirror # reports wrong mirror patterns of bytes/strings usage - #FP - musttag # enforces field tags in (un)marshaled structs - #- nakedret # finds naked returns in functions greater than a specified function length + - mirror # reports wrong mirror patterns of bytes/strings usage + # - mnd # detects magic numbers + # - musttag # enforces field tags in (un)marshaled structs + # - nakedret # finds naked returns in functions greater than a specified function length - nestif # reports deeply nested if statements - nilerr # finds the code that returns nil even if it checks that the error is not nil - nilnil # checks that there is no simultaneous return of nil error and an invalid value - #- noctx # finds sending http request without context.Context + # - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - #- nonamedreturns # reports all named returns + # - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used - reassign # checks that package variables are not reassigned - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint - #- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - #- stylecheck # is a replacement for golint + # - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - spancheck # checks for mistakes with OpenTelemetry/Census spans + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + # - testpackage # makes you use a separate _test package - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - #return values - unparam # reports unused function parameters + # - unparam # reports unused function parameters - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace @@ -286,7 +387,7 @@ linters: #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega #- godox # detects FIXME, TODO and other comment keywords #- goheader # checks is file header matches to pattern - #- gomnd # detects magic numbers + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters #- interfacebloat # checks the number of methods inside an interface #- ireturn # accept interfaces, return concrete types #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated @@ -294,7 +395,6 @@ linters: #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope #- wrapcheck # checks that errors returned from external packages are wrapped #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event - #- rowserrcheck # checks whether Err of rows is checked successfully ## disabled #- containedctx # detects struct contained context.Context field @@ -302,10 +402,11 @@ linters: #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) #- dupword # [useless without config] checks for duplicate words in the source code + #- err113 # [too strict] checks the errors handling expressions #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds + #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables #- forcetypeassert # [replaced by errcheck] finds forced type assertions - #- gochecknoglobals # checks that no global variables exist - #- goerr113 # [too strict] checks the errors handling expressions #- gofmt # [replaced by goimports] checks whether code was gofmt-ed #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase @@ -316,23 +417,9 @@ linters: #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test #- tagliatelle # checks the struct tags - #- testpackage # makes you use a separate _test package #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines - ## deprecated - #- deadcode # [deprecated, replaced by unused] finds unused code - #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized - #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes - #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible - #- interfacer # [deprecated] suggests narrower interface types - #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted - #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name - #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs - #- structcheck # [deprecated, replaced by unused] finds unused struct fields - #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants - - issues: # Maximum count of issues with the same text. # Set to 0 to disable. @@ -341,9 +428,9 @@ issues: exclude-rules: - source: "(noinspection|TODO)" - linters: [ godot ] + linters: [godot] - source: "//noinspection" - linters: [ gocritic ] + linters: [gocritic] - path: "_test\\.go" linters: - bodyclose diff --git a/.reviewdog.yml b/.reviewdog.yml new file mode 100644 index 00000000..e3f29d13 --- /dev/null +++ b/.reviewdog.yml @@ -0,0 +1,10 @@ +# reviewdog.yml +runner: + golint-by-project-conf: + cmd: golint $(go list ./... | grep -v /vendor/) + format: golint + level: warning + govet-by-project-conf: + cmd: go vet + format: govet + level: error \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..4d56811d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["golang.go", "task.vscode-task"] +} diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 00000000..f9cba7b7 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,36 @@ +# https://taskfile.dev + +version: '3' + +tasks: + install: + cmds: + - go mod download + - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 + - go install golang.org/x/tools/cmd/goimports@latest + - go install github.com/pointlander/peg@v1.0.1 + - go generate ./... + + run: + deps: ['test-and-lint'] + cmds: + - go run . + build: + deps: ['test-and-lint'] + cmds: + - task: build-only + + test-and-lint: + deps: ['test', 'lint'] + test: + cmds: + - go test ./... + - go vet ./... + lint: + cmds: + - goimports -w . + - golangci-lint run + + build-only: + cmds: + - go build . diff --git a/api/api_bind.go b/api/api_bind.go index 2bbeeed6..60b7947c 100644 --- a/api/api_bind.go +++ b/api/api_bind.go @@ -166,7 +166,7 @@ func forceStop(c echo.Context) error { for _, i := range diceManager.Dice { if i.IsAlreadyLoadConfig { - i.BanList.SaveChanged(i) + i.Config.BanList.SaveChanged(i) i.AttrsManager.CheckForSave() i.Save(true) for _, j := range i.ExtList { @@ -190,7 +190,11 @@ func forceStop(c echo.Context) error { dbData := d.DBData if dbData != nil { d.DBData = nil - _ = dbData.Close() + db, err := dbData.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -201,7 +205,11 @@ func forceStop(c echo.Context) error { dbLogs := d.DBLogs if dbLogs != nil { d.DBLogs = nil - _ = dbLogs.Close() + db, err := dbLogs.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -213,7 +221,11 @@ func forceStop(c echo.Context) error { if cm != nil && cm.DB != nil { dbCensor := cm.DB cm.DB = nil - _ = dbCensor.Close() + db, err := dbCensor.DB() + if err != nil { + return + } + _ = db.Close() } })() } @@ -597,7 +609,7 @@ func Bind(e *echo.Echo, _myDice *dice.DiceManager) { e.POST(prefix+"/im_connections/del", ImConnectionsDel) e.POST(prefix+"/im_connections/set_enable", ImConnectionsSetEnable) e.POST(prefix+"/im_connections/set_data", ImConnectionsSetData) - e.POST(prefix+"/im_connections/set_sign_server", ImConnectionsRWSignServerUrl) + e.GET(prefix+"/im_connections/get_lgr_signinfo", ImConnectionsGetSignInfo) e.POST(prefix+"/im_connections/gocqhttpRelogin", ImConnectionsGocqhttpRelogin) e.POST(prefix+"/im_connections/walleQRelogin", ImConnectionsWalleQRelogin) e.GET(prefix+"/im_connections/gocq_config_download.zip", ImConnectionsGocqConfigDownload) @@ -626,8 +638,10 @@ func Bind(e *echo.Echo, _myDice *dice.DiceManager) { e.GET(prefix+"/dice/cmdList", DiceAllCommand) e.POST(prefix+"/dice/upload_to_upgrade", DiceNewVersionUpload) - e.POST(prefix+"/dice/config/vm-version-for-reply-set", vmVersionForReplySet) - e.POST(prefix+"/dice/config/vm-version-for-deck-set", vmVersionForDeckSet) + e.GET(prefix+"/dice/public/info", dicePublicInfo) + e.POST(prefix+"/dice/public/set", dicePublicSet) + + e.POST(prefix+"/dice/config/vm-version-set", vmVersionSet) e.POST(prefix+"/signin", doSignIn) e.GET(prefix+"/signin/salt", doSignInGetSalt) diff --git a/api/ban.go b/api/ban.go index fc7b1855..ef637c99 100644 --- a/api/ban.go +++ b/api/ban.go @@ -20,7 +20,7 @@ func banConfigGet(c echo.Context) error { return c.JSON(http.StatusForbidden, nil) } - return c.JSON(http.StatusOK, myDice.BanList) + return c.JSON(http.StatusOK, myDice.Config.BanList) } func banConfigSet(c echo.Context) error { @@ -42,23 +42,25 @@ func banConfigSet(c echo.Context) error { v.ThresholdBan = 200 } - myDice.BanList.BanBehaviorRefuseReply = v.BanBehaviorRefuseReply - myDice.BanList.BanBehaviorRefuseInvite = v.BanBehaviorRefuseInvite - myDice.BanList.BanBehaviorQuitLastPlace = v.BanBehaviorQuitLastPlace - myDice.BanList.BanBehaviorQuitPlaceImmediately = v.BanBehaviorQuitPlaceImmediately - myDice.BanList.BanBehaviorQuitIfAdmin = v.BanBehaviorQuitIfAdmin - myDice.BanList.ScoreReducePerMinute = v.ScoreReducePerMinute - myDice.BanList.ThresholdWarn = v.ThresholdWarn - myDice.BanList.ThresholdBan = v.ThresholdBan - myDice.BanList.ScoreGroupMuted = v.ScoreGroupMuted - myDice.BanList.ScoreGroupKicked = v.ScoreGroupKicked - myDice.BanList.ScoreTooManyCommand = v.ScoreTooManyCommand - - myDice.BanList.JointScorePercentOfGroup = v.JointScorePercentOfGroup - myDice.BanList.JointScorePercentOfInviter = v.JointScorePercentOfInviter + config := &myDice.Config + config.BanList.BanBehaviorRefuseReply = v.BanBehaviorRefuseReply + config.BanList.BanBehaviorRefuseInvite = v.BanBehaviorRefuseInvite + config.BanList.BanBehaviorQuitLastPlace = v.BanBehaviorQuitLastPlace + config.BanList.BanBehaviorQuitPlaceImmediately = v.BanBehaviorQuitPlaceImmediately + config.BanList.BanBehaviorQuitIfAdmin = v.BanBehaviorQuitIfAdmin + config.BanList.BanBehaviorQuitIfAdminSilentIfNotAdmin = v.BanBehaviorQuitIfAdminSilentIfNotAdmin + config.BanList.ScoreReducePerMinute = v.ScoreReducePerMinute + config.BanList.ThresholdWarn = v.ThresholdWarn + config.BanList.ThresholdBan = v.ThresholdBan + config.BanList.ScoreGroupMuted = v.ScoreGroupMuted + config.BanList.ScoreGroupKicked = v.ScoreGroupKicked + config.BanList.ScoreTooManyCommand = v.ScoreTooManyCommand + + config.BanList.JointScorePercentOfGroup = v.JointScorePercentOfGroup + config.BanList.JointScorePercentOfInviter = v.JointScorePercentOfInviter myDice.MarkModified() - return c.JSON(http.StatusOK, myDice.BanList) + return c.JSON(http.StatusOK, myDice.Config.BanList) } func banMapList(c echo.Context) error { @@ -75,7 +77,7 @@ func banMapDeleteOne(c echo.Context) error { if err != nil { return c.String(430, err.Error()) } - myDice.BanList.DeleteByID(myDice, v.ID) + (&myDice.Config).BanList.DeleteByID(myDice, v.ID) return c.JSON(http.StatusOK, nil) } @@ -92,7 +94,7 @@ func banMapAddOne(c echo.Context) error { return c.String(430, err.Error()) } if v.Rank == dice.BanRankBanned { - score := myDice.BanList.ThresholdBan + score := myDice.Config.BanList.ThresholdBan reason := "骰主后台设置" if len(v.Reasons) > 0 { reason = v.Reasons[0] @@ -102,7 +104,7 @@ func banMapAddOne(c echo.Context) error { platform := strings.Replace(prefix, "-Group", "", 1) for _, i := range myDice.ImSession.EndPoints { if i.Platform == platform && i.Enable { - v2 := myDice.BanList.AddScoreBase(v.ID, score, "海豹后台", reason, &dice.MsgContext{Dice: myDice, EndPoint: i}) + v2 := (&myDice.Config).BanList.AddScoreBase(v.ID, score, "海豹后台", reason, &dice.MsgContext{Dice: myDice, EndPoint: i}) if v2 != nil { if v.Name != "" { v2.Name = v.Name @@ -112,7 +114,7 @@ func banMapAddOne(c echo.Context) error { } } if v.Rank == dice.BanRankTrusted { - myDice.BanList.SetTrustByID(v.ID, "海豹后台", "骰主后台设置") + (&myDice.Config).BanList.SetTrustByID(v.ID, "海豹后台", "骰主后台设置") } return c.JSON(http.StatusOK, nil) @@ -132,7 +134,7 @@ func banMapAddOne(c echo.Context) error { // return c.String(430, err.Error()) // } // -// myDice.BanList.LoadMapFromJSON(v.data) +// (&myDice.Config).BanList.LoadMapFromJSON(v.data) // return c.JSON(http.StatusOK, nil) //} @@ -199,9 +201,9 @@ func banImport(c echo.Context) error { } } item.Reasons = newReasons - myDice.BanList.Map.Store(item.ID, item) + (&myDice.Config).BanList.Map.Store(item.ID, item) } - myDice.BanList.SaveChanged(myDice) + (&myDice.Config).BanList.SaveChanged(myDice) return Success(&c, Response{}) } diff --git a/api/censor.go b/api/censor.go index c3a10e67..093ad5b5 100644 --- a/api/censor.go +++ b/api/censor.go @@ -3,7 +3,6 @@ package api import ( "bufio" "encoding/json" - "fmt" "io" "mime/multipart" "net/http" @@ -20,6 +19,7 @@ import ( "sealdice-core/dice" "sealdice-core/dice/censor" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" ) func check(c echo.Context) (bool, error) { @@ -29,7 +29,7 @@ func check(c echo.Context) (bool, error) { if dm.JustForTest { return false, Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } - if !myDice.EnableCensor { + if !myDice.Config.EnableCensor { return false, Error(&c, "未启用拦截引擎", Response{}) } if myDice.CensorManager.IsLoading { @@ -47,10 +47,11 @@ func censorRestart(c echo.Context) error { } myDice.NewCensorManager() - myDice.EnableCensor = true + (&myDice.Config).EnableCensor = true + myDice.MarkModified() return Success(&c, Response{ - "enable": myDice.EnableCensor, + "enable": myDice.Config.EnableCensor, "isLoading": myDice.CensorManager.IsLoading, }) } @@ -61,8 +62,16 @@ func censorStop(c echo.Context) error { return err } - myDice.EnableCensor = false - _ = myDice.CensorManager.DB.Close() + (&myDice.Config).EnableCensor = false + myDice.MarkModified() + db, err2 := myDice.CensorManager.DB.DB() + if err2 != nil { + return Error(&c, "关闭拦截引擎失败", Response{}) + } + err = db.Close() + if err != nil { + return err + } myDice.CensorManager = nil return Success(&c, Response{}) @@ -74,23 +83,24 @@ func censorGetStatus(c echo.Context) error { isLoading = myDice.CensorManager.IsLoading } return Success(&c, Response{ - "enable": myDice.EnableCensor, + "enable": myDice.Config.EnableCensor, "isLoading": isLoading, }) } func censorGetConfig(c echo.Context) error { + config := myDice.Config levelConfig := map[string]LevelConfig{ - "notice": getLevelConfig(censor.Notice, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores), - "caution": getLevelConfig(censor.Caution, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores), - "warning": getLevelConfig(censor.Warning, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores), - "danger": getLevelConfig(censor.Danger, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores), + "notice": getLevelConfig(censor.Notice, config.CensorThresholds, config.CensorHandlers, config.CensorScores), + "caution": getLevelConfig(censor.Caution, config.CensorThresholds, config.CensorHandlers, config.CensorScores), + "warning": getLevelConfig(censor.Warning, config.CensorThresholds, config.CensorHandlers, config.CensorScores), + "danger": getLevelConfig(censor.Danger, config.CensorThresholds, config.CensorHandlers, config.CensorScores), } return Success(&c, Response{ - "mode": myDice.CensorMode, - "caseSensitive": myDice.CensorCaseSensitive, - "matchPinyin": myDice.CensorMatchPinyin, - "filterRegex": myDice.CensorFilterRegexStr, + "mode": config.CensorMode, + "caseSensitive": config.CensorCaseSensitive, + "matchPinyin": config.CensorMatchPinyin, + "filterRegex": config.CensorFilterRegexStr, "levelConfig": levelConfig, }) } @@ -149,10 +159,11 @@ func censorSetConfig(c echo.Context) error { jsonMap := make(map[string]interface{}) err = json.NewDecoder(c.Request().Body).Decode(&jsonMap) if err != nil { - fmt.Println(err) + log.Error("censorSetConfig", err) return c.JSON(http.StatusInternalServerError, err) } + config := &myDice.Config if val, ok := jsonMap["filterRegex"]; ok { filterRegex, ok := val.(string) if ok { @@ -160,25 +171,25 @@ func censorSetConfig(c echo.Context) error { if err != nil { return Error(&c, "过滤字符正则不是合法的正则表达式", Response{}) } - myDice.CensorFilterRegexStr = filterRegex + config.CensorFilterRegexStr = filterRegex } } if val, ok := jsonMap["mode"]; ok { mode, ok := val.(float64) if ok { - myDice.CensorMode = dice.CensorMode(mode) + config.CensorMode = dice.CensorMode(mode) } } if val, ok := jsonMap["caseSensitive"]; ok { caseSensitive, ok := val.(bool) if ok { - myDice.CensorCaseSensitive = caseSensitive + config.CensorCaseSensitive = caseSensitive } } if val, ok := jsonMap["matchPinyin"]; ok { matchPinyin, ok := val.(bool) if ok { - myDice.CensorMatchPinyin = matchPinyin + config.CensorMatchPinyin = matchPinyin } } if val, ok := jsonMap["levelConfig"]; ok { //nolint:nestif @@ -212,7 +223,7 @@ func censorSetConfig(c echo.Context) error { if ok { if val, ok = confMap["threshold"]; ok { threshold := val.(float64) - myDice.CensorThresholds[level] = int(threshold) + config.CensorThresholds[level] = int(threshold) } if val, ok = confMap["handlers"]; ok { handlers := stringConvert(val) @@ -220,7 +231,7 @@ func censorSetConfig(c echo.Context) error { } if val, ok = confMap["score"]; ok { score := val.(float64) - myDice.CensorScores[level] = int(score) + config.CensorScores[level] = int(score) } } } @@ -259,7 +270,7 @@ func setLevelHandlers(level censor.Level, handlers []string) { handlerVal = newHandlerVal(handlerVal, dice.BanInviter, newHandlers) handlerVal = newHandlerVal(handlerVal, dice.AddScore, newHandlers) - myDice.CensorHandlers[level] = handlerVal + (&myDice.Config).CensorHandlers[level] = handlerVal } func newHandlerVal(val uint8, handle dice.CensorHandler, newHandlers map[dice.CensorHandler]bool) uint8 { @@ -434,7 +445,7 @@ func censorDeleteWordFiles(c echo.Context) error { }{} err = c.Bind(&v) if err != nil { - fmt.Println(err) + log.Error("censorDeleteWordFiles", err) return c.JSON(http.StatusInternalServerError, err) } @@ -509,7 +520,7 @@ func censorGetLogPage(c echo.Context) error { v := model.QueryCensorLog{} err = c.Bind(&v) if err != nil { - fmt.Println(err) + log.Error("censorGetLogPage", err) return c.JSON(http.StatusInternalServerError, err) } if v.PageNum < 1 { diff --git a/api/conn.go b/api/conn.go index deac6260..ceca01dc 100644 --- a/api/conn.go +++ b/api/conn.go @@ -2,7 +2,6 @@ package api import ( "encoding/base64" - "fmt" "net/http" "sort" "strconv" @@ -11,6 +10,7 @@ import ( "github.com/labstack/echo/v4" "sealdice-core/dice" + log "sealdice-core/utils/kratos" ) func ImConnections(c echo.Context) error { @@ -137,46 +137,18 @@ func ImConnectionsSetData(c echo.Context) error { return c.JSON(http.StatusNotFound, nil) } -func ImConnectionsRWSignServerUrl(c echo.Context) error { +func ImConnectionsGetSignInfo(c echo.Context) error { if !doAuth(c) { return c.JSON(http.StatusForbidden, nil) } - if dm.JustForTest { - return c.JSON(http.StatusOK, map[string]interface{}{ - "testMode": true, - }) - } - v := struct { - ID string `form:"id" json:"id"` - SignServerUrl string `form:"signServerUrl" json:"signServerUrl"` - W bool `form:"w" json:"w"` - SignServerVersion string `form:"signServerVersion" json:"signServerVersion"` - }{} - - err := c.Bind(&v) + data, err := dice.LagrangeGetSignInfo(myDice) if err != nil { - myDice.Save(false) - return c.JSON(http.StatusNotFound, nil) + return Error(&c, "读取SignInfo失败", Response{}) } - for _, i := range myDice.ImSession.EndPoints { - if i.ID != v.ID { - continue - } - if i.ProtocolType == "onebot" { - pa := i.Adapter.(*dice.PlatformAdapterGocq) - if pa.BuiltinMode == "lagrange" { - signServerUrl, signServerVersion := dice.RWLagrangeSignServerUrl(myDice, i, v.SignServerUrl, v.W, v.SignServerVersion) - if signServerUrl != "" { - return Success(&c, Response{ - "signServerUrl": signServerUrl, - "signServerVersion": signServerVersion, - }) - } - } - } - } - return Error(&c, "读取signServerUrl字段失败", Response{}) + return Success(&c, Response{ + "data": data, + }) } func ImConnectionsDel(c echo.Context) error { @@ -207,7 +179,7 @@ func ImConnectionsDel(c echo.Context) error { myDice.ImSession.EndPoints = append(myDice.ImSession.EndPoints[:index], myDice.ImSession.EndPoints[index+1:]...) if i.ProtocolType == "onebot" { pa := i.Adapter.(*dice.PlatformAdapterGocq) - if pa.BuiltinMode == "lagrange" { + if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" { dice.BuiltinQQServeProcessKillBase(myDice, i, true) // 经测试,若不延时,可能导致清理对应目录失败(原因:文件被占用) time.Sleep(1 * time.Second) @@ -437,7 +409,7 @@ func ImConnectionsGocqhttpRelogin(c echo.Context) error { if err == nil { for _, i := range myDice.ImSession.EndPoints { if i.ID == v.ID { - fmt.Print("!!! relogin ", v.ID) + log.Warnf("relogin %s", v.ID) i.Adapter.DoRelogin() return c.JSON(http.StatusOK, nil) } @@ -967,8 +939,9 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error { v := struct { Account string `yaml:"account" json:"account"` - SignServerUrl string `yaml:"signServerUrl" json:"signServerUrl"` + SignServerName string `yaml:"signServerName" json:"signServerName"` SignServerVersion string `yaml:"signServerVersion" json:"signServerVersion"` + IsGocq bool `yaml:"isGocq" json:"isGocq"` }{} err := c.Bind(&v) if err == nil { @@ -977,7 +950,7 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error { return nil } - conn := dice.NewLagrangeConnectInfoItem(v.Account) + conn := dice.NewLagrangeConnectInfoItem(v.Account, v.IsGocq) conn.UserID = dice.FormatDiceIDQQ(uid) conn.Session = myDice.ImSession pa := conn.Adapter.(*dice.PlatformAdapterGocq) @@ -990,9 +963,11 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error { if err != nil { return err } + pa.SignServerName = v.SignServerName + pa.SignServerVer = v.SignServerVersion dice.LagrangeServe(myDice, conn, dice.LagrangeLoginInfo{ UIN: uin, - SignServerUrl: v.SignServerUrl, + SignServerName: v.SignServerName, SignServerVersion: v.SignServerVersion, IsAsyncRun: true, }) diff --git a/api/deck.go b/api/deck.go index 9264b566..7f676466 100644 --- a/api/deck.go +++ b/api/deck.go @@ -92,19 +92,22 @@ func deckEnable(c echo.Context) error { } v := struct { - Index int `json:"index"` - Enable bool `json:"enable"` + Filename string `json:"filename"` + Enable bool `json:"enable"` }{} err := c.Bind(&v) if err == nil { - if v.Index >= 0 && v.Index < len(myDice.DeckList) { - myDice.DeckList[v.Index].Enable = v.Enable - myDice.MarkModified() + for _, deck := range myDice.DeckList { + if deck.Filename == v.Filename { + deck.Enable = v.Enable + myDice.MarkModified() + break + } } } - return c.JSON(http.StatusOK, myDice.BanList) + return c.JSON(http.StatusOK, myDice.Config.BanList) } func deckDelete(c echo.Context) error { @@ -118,14 +121,17 @@ func deckDelete(c echo.Context) error { } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.DeckList) { - dice.DeckDelete(myDice, myDice.DeckList[v.Index]) - myDice.MarkModified() + if err == nil && v.Filename != "" { + for _, deck := range myDice.DeckList { + if deck.Filename == v.Filename { + dice.DeckDelete(myDice, deck) + myDice.MarkModified() + break + } } } @@ -140,23 +146,25 @@ func deckCheckUpdate(c echo.Context) error { return Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.DeckList) { - deck := myDice.DeckList[v.Index] - oldDeck, newDeck, tempFileName, err := myDice.DeckCheckUpdate(deck) - if err != nil { - return Error(&c, err.Error(), Response{}) + if err == nil && v.Filename != "" { + for _, deck := range myDice.DeckList { + if deck.Filename == v.Filename { + oldDeck, newDeck, tempFileName, err := myDice.DeckCheckUpdate(deck) + if err != nil { + return Error(&c, err.Error(), Response{}) + } + return Success(&c, Response{ + "old": oldDeck, + "new": newDeck, + "format": deck.FileFormat, + "filename": deck.Filename, + "tempFileName": tempFileName, + }) } - return Success(&c, Response{ - "old": oldDeck, - "new": newDeck, - "format": deck.FileFormat, - "tempFileName": tempFileName, - }) } } return Success(&c, Response{}) @@ -170,18 +178,21 @@ func deckUpdate(c echo.Context) error { return Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` TempFileName string `json:"tempFileName"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.DeckList) { - err := myDice.DeckUpdate(myDice.DeckList[v.Index], v.TempFileName) - if err != nil { - return Error(&c, err.Error(), Response{}) + if err == nil && v.Filename != "" { + for _, deck := range myDice.DeckList { + if deck.Filename == v.Filename { + err := myDice.DeckUpdate(deck, v.TempFileName) + if err != nil { + return Error(&c, err.Error(), Response{}) + } + myDice.MarkModified() + break } - myDice.MarkModified() } } return Success(&c, Response{}) diff --git a/api/dice_config.go b/api/dice_config.go index 03928129..2b102ffd 100644 --- a/api/dice_config.go +++ b/api/dice_config.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "fmt" "net/http" "strconv" "strings" @@ -13,64 +12,21 @@ import ( "sealdice-core/dice" "sealdice-core/utils" + log "sealdice-core/utils/kratos" ) type DiceConfigInfo struct { - // 注:form其实不需要 - CommandPrefix []string `json:"commandPrefix" form:"commandPrefix"` // 指令前缀 - DiceMasters []string `json:"diceMasters" form:"diceMasters"` // 骰主设置,需要格式: 平台:帐号 - NoticeIds []string `json:"noticeIds"` // 通知设置,需要格式: 平台:帐号 - OnlyLogCommandInGroup bool `json:"onlyLogCommandInGroup" form:"onlyLogCommandInGroup"` // 日志中仅记录命令 - OnlyLogCommandInPrivate bool `json:"onlyLogCommandInPrivate" form:"onlyLogCommandInPrivate"` // 日志中仅记录命令 - WorkInQQChannel bool `json:"workInQQChannel"` // 在QQ频道中开启 - MessageDelayRangeStart float64 `json:"messageDelayRangeStart" form:"messageDelayRangeStart"` // 指令延迟区间 - MessageDelayRangeEnd float64 `json:"messageDelayRangeEnd" form:"messageDelayRangeEnd"` - UIPassword string `json:"uiPassword" form:"uiPassword"` - HelpDocEngineType int `json:"helpDocEngineType"` - MasterUnlockCode string `json:"masterUnlockCode" form:"masterUnlockCode"` - ServeAddress string `json:"serveAddress" form:"serveAddress"` - MasterUnlockCodeTime int64 `json:"masterUnlockCodeTime"` - LogPageItemLimit int64 `json:"logPageItemLimit"` - FriendAddComment string `json:"friendAddComment"` - QQChannelAutoOn bool `json:"QQChannelAutoOn"` - QQChannelLogMessage bool `json:"QQChannelLogMessage"` - RefuseGroupInvite bool `json:"refuseGroupInvite"` // 拒绝群组邀请 - - QuitInactiveThreshold float64 `json:"quitInactiveThreshold"` // 退出不活跃群组阈值(天) - QuitInactiveBatchSize int64 `json:"quitInactiveBatchSize"` // 退出不活跃群组的批量大小 - QuitInactiveBatchWait int64 `json:"quitInactiveBatchWait"` // 退出不活跃群组的批量等待时间(分) - - DefaultCocRuleIndex string `json:"defaultCocRuleIndex"` // 默认coc index - MaxExecuteTime string `json:"maxExecuteTime"` // 最大骰点次数 - MaxCocCardGen string `json:"maxCocCardGen"` // 最大coc制卡数 - - ExtDefaultSettings []*dice.ExtDefaultSettingItem `yaml:"extDefaultSettings" json:"extDefaultSettings"` // 新群扩展按此顺序加载 - BotExtFreeSwitch bool `json:"botExtFreeSwitch"` - TrustOnlyMode bool `json:"trustOnlyMode"` - AliveNoticeEnable bool `json:"aliveNoticeEnable"` - AliveNoticeValue string `json:"aliveNoticeValue"` - ReplyDebugMode bool `json:"replyDebugMode"` - - CustomReplyConfigEnable bool `json:"customReplyConfigEnable"` // 是否开启reply - - LogSizeNoticeEnable bool `json:"logSizeNoticeEnable"` // 开启日志数量提示 - LogSizeNoticeCount int `json:"logSizeNoticeCount"` // 日志数量提示阈值,默认500 - - TextCmdTrustOnly bool `json:"textCmdTrustOnly"` // text命令只允许信任用户和master - IgnoreUnaddressedBotCmd bool `json:"ignoreUnaddressedBotCmd"` // 不响应群聊裸bot指令 - QQEnablePoke bool `json:"QQEnablePoke"` // QQ允许戳一戳 - PlayerNameWrapEnable bool `json:"playerNameWrapEnable"` // 玩家名外框 - - MailEnable bool `json:"mailEnable"` - MailFrom string `json:"mailFrom"` // 邮箱来源 - MailPassword string `json:"mailPassword"` // 邮箱密钥/密码 - MailSMTP string `json:"mailSmtp"` // 邮箱 smtp 地址 - - RateLimitEnabled bool `json:"rateLimitEnabled"` // 是否开启限速 - PersonalReplenishRate string `json:"personalReplenishRate"` // 个人自定义速率 - PersonalReplenishBurst int64 `json:"personalBurst"` // 个人自定义上限 - GroupReplenishRate string `json:"groupReplenishRate"` // 群组自定义速率 - GroupReplenishBurst int64 `json:"groupBurst"` // 群组自定义上限 + dice.Config + + CommandPrefix []string `json:"commandPrefix" form:"commandPrefix"` + DiceMasters []string `json:"diceMasters" form:"diceMasters"` + UIPassword string `json:"uiPassword" form:"uiPassword"` + LogPageItemLimit int64 `json:"logPageItemLimit"` + DefaultCocRuleIndex string `json:"defaultCocRuleIndex"` // 默认coc index + MaxExecuteTime string `json:"maxExecuteTime"` // 最大骰点次数 + MaxCocCardGen string `json:"maxCocCardGen"` // 最大coc制卡数 + ServerAddress string `json:"serveAddress" form:"serveAddress"` + HelpDocEngineType int `json:"helpDocEngineType"` } func DiceConfig(c echo.Context) error { @@ -83,93 +39,53 @@ func DiceConfig(c echo.Context) error { password = "------" } - limit := myDice.UILogLimit + limit := myDice.Config.UILogLimit if limit == 0 { limit = 100 } myDice.UnlockCodeUpdate(false) - cocRule := strconv.FormatInt(myDice.DefaultCocRuleIndex, 10) - if myDice.DefaultCocRuleIndex == 11 { + cocRule := strconv.FormatInt(myDice.Config.DefaultCocRuleIndex, 10) + if myDice.Config.DefaultCocRuleIndex == 11 { cocRule = "dg" } - maxExec := strconv.FormatInt(myDice.MaxExecuteTime, 10) + maxExec := strconv.FormatInt(myDice.Config.MaxExecuteTime, 10) - maxCard := strconv.FormatInt(myDice.MaxCocCardGen, 10) + maxCard := strconv.FormatInt(myDice.Config.MaxCocCardGen, 10) emailPasswordMasked := "" - if myDice.MailPassword != "" { + if myDice.Config.MailPassword != "" { emailPasswordMasked = "******" } // 过滤掉未加载的: 包括关闭的和已经删除的 // TODO(Xiangze Li): 如果前端能支持区分显示未加载插件的配置(.loaded字段), 这里就的过滤就可以去掉 - extDefaultSettings := make([]*dice.ExtDefaultSettingItem, 0, len(myDice.ExtDefaultSettings)) - for _, i := range myDice.ExtDefaultSettings { + extDefaultSettings := make([]*dice.ExtDefaultSettingItem, 0, len(myDice.Config.ExtDefaultSettings)) + for _, i := range myDice.Config.ExtDefaultSettings { if i.Loaded { extDefaultSettings = append(extDefaultSettings, i) } } info := DiceConfigInfo{ - CommandPrefix: myDice.CommandPrefix, - DiceMasters: myDice.DiceMasters, - NoticeIds: myDice.NoticeIDs, - OnlyLogCommandInPrivate: myDice.OnlyLogCommandInPrivate, - OnlyLogCommandInGroup: myDice.OnlyLogCommandInGroup, - MessageDelayRangeStart: myDice.MessageDelayRangeStart, - MessageDelayRangeEnd: myDice.MessageDelayRangeEnd, - UIPassword: password, - MasterUnlockCode: myDice.MasterUnlockCode, - MasterUnlockCodeTime: myDice.MasterUnlockCodeTime, - WorkInQQChannel: myDice.WorkInQQChannel, - LogPageItemLimit: limit, - FriendAddComment: myDice.FriendAddComment, - QQChannelAutoOn: myDice.QQChannelAutoOn, - QQChannelLogMessage: myDice.QQChannelLogMessage, - RefuseGroupInvite: myDice.RefuseGroupInvite, - RateLimitEnabled: myDice.RateLimitEnabled, - - ExtDefaultSettings: extDefaultSettings, - DefaultCocRuleIndex: cocRule, + Config: myDice.Config, - QuitInactiveThreshold: myDice.QuitInactiveThreshold.Hours() / 24, - QuitInactiveBatchSize: myDice.QuitInactiveBatchSize, - QuitInactiveBatchWait: myDice.QuitInactiveBatchWait, - - BotExtFreeSwitch: myDice.BotExtFreeSwitch, - TrustOnlyMode: myDice.TrustOnlyMode, - AliveNoticeEnable: myDice.AliveNoticeEnable, - AliveNoticeValue: myDice.AliveNoticeValue, - ReplyDebugMode: myDice.ReplyDebugMode, - - ServeAddress: myDice.Parent.ServeAddress, - HelpDocEngineType: myDice.Parent.HelpDocEngineType, - - // 1.0 正式 - LogSizeNoticeEnable: myDice.LogSizeNoticeEnable, - LogSizeNoticeCount: myDice.LogSizeNoticeCount, - CustomReplyConfigEnable: myDice.CustomReplyConfigEnable, - - // 1.2 - TextCmdTrustOnly: myDice.TextCmdTrustOnly, - QQEnablePoke: myDice.QQEnablePoke, - PlayerNameWrapEnable: myDice.PlayerNameWrapEnable, - - // 1.3? - MailEnable: myDice.MailEnable, - MailFrom: myDice.MailFrom, - MailPassword: emailPasswordMasked, - MailSMTP: myDice.MailSMTP, - MaxExecuteTime: maxExec, - MaxCocCardGen: maxCard, - PersonalReplenishRate: myDice.PersonalReplenishRateStr, - PersonalReplenishBurst: myDice.PersonalBurst, - GroupReplenishRate: myDice.GroupReplenishRateStr, - GroupReplenishBurst: myDice.GroupBurst, - IgnoreUnaddressedBotCmd: myDice.IgnoreUnaddressedBotCmd, + CommandPrefix: myDice.CommandPrefix, + DiceMasters: myDice.DiceMasters, + UIPassword: password, + LogPageItemLimit: limit, + DefaultCocRuleIndex: cocRule, + ServerAddress: myDice.Parent.ServeAddress, + HelpDocEngineType: myDice.Parent.HelpDocEngineType, + MaxExecuteTime: maxExec, + MaxCocCardGen: maxCard, } + info.ExtDefaultSettings = extDefaultSettings + info.DefaultCocRuleIndex = cocRule + info.MailPassword = emailPasswordMasked + info.Config.QuitInactiveThresholdDays = info.QuitInactiveThreshold.Hours() / 24 + return c.JSON(http.StatusOK, info) } @@ -193,7 +109,7 @@ func DiceConfigSet(c echo.Context) error { } if err != nil { - fmt.Println(err) + log.Error("DiceConfigSet", err) return c.JSON(http.StatusOK, nil) } if val, ok := jsonMap["commandPrefix"]; ok { @@ -217,8 +133,9 @@ func DiceConfigSet(c echo.Context) error { myDice.DiceMasters = masters } + config := &myDice.Config if val, ok := jsonMap["noticeIds"]; ok { - myDice.NoticeIDs = stringConvert(val) + config.NoticeIDs = stringConvert(val) } if val, ok := jsonMap["defaultCocRuleIndex"]; ok { //nolint:nestif @@ -226,12 +143,12 @@ func DiceConfigSet(c echo.Context) error { if ok { valStr = strings.TrimSpace(valStr) if strings.EqualFold(valStr, "dg") { - myDice.DefaultCocRuleIndex = 11 + config.DefaultCocRuleIndex = 11 } else { - myDice.DefaultCocRuleIndex, err = strconv.ParseInt(valStr, 10, 64) + config.DefaultCocRuleIndex, err = strconv.ParseInt(valStr, 10, 64) if err == nil { - if myDice.DefaultCocRuleIndex > 5 || myDice.DefaultCocRuleIndex < 0 { - myDice.DefaultCocRuleIndex = 0 + if config.DefaultCocRuleIndex > 5 || config.DefaultCocRuleIndex < 0 { + config.DefaultCocRuleIndex = dice.DefaultConfig.DefaultCocRuleIndex } } } @@ -245,7 +162,7 @@ func DiceConfigSet(c echo.Context) error { var valInt int64 valInt, err = strconv.ParseInt(valStr, 10, 64) if err == nil && valInt > 0 { - myDice.MaxExecuteTime = valInt + config.MaxExecuteTime = valInt } /* else { Should return some error? } */ @@ -259,7 +176,7 @@ func DiceConfigSet(c echo.Context) error { var valInt int64 valInt, err = strconv.ParseInt(valStr, 10, 64) if err == nil && valInt > 0 { - myDice.MaxCocCardGen = valInt + config.MaxCocCardGen = valInt } /* else { Should return some error? } */ @@ -271,7 +188,7 @@ func DiceConfigSet(c echo.Context) error { if ok { customBurst := int64(valStr) if customBurst >= 1 { - myDice.PersonalBurst = customBurst + config.PersonalBurst = customBurst } } } @@ -282,8 +199,8 @@ func DiceConfigSet(c echo.Context) error { valStr = strings.TrimSpace(valStr) newRate, errParse := utils.ParseRate(valStr) if errParse == nil && newRate != rate.Limit(0) { - myDice.PersonalReplenishRate = newRate - myDice.PersonalReplenishRateStr = valStr + config.PersonalReplenishRate = newRate + config.PersonalReplenishRateStr = valStr } } } @@ -293,7 +210,7 @@ func DiceConfigSet(c echo.Context) error { if ok { customBurst := int64(valStr) if customBurst >= 1 { - myDice.GroupBurst = customBurst + config.GroupBurst = customBurst } } } @@ -304,54 +221,54 @@ func DiceConfigSet(c echo.Context) error { valStr = strings.TrimSpace(valStr) newRate, errParse := utils.ParseRate(valStr) if errParse == nil && newRate != rate.Limit(0) { - myDice.GroupReplenishRate = newRate - myDice.GroupReplenishRateStr = valStr + config.GroupReplenishRate = newRate + config.GroupReplenishRateStr = valStr } } } if val, ok := jsonMap["onlyLogCommandInGroup"]; ok { - myDice.OnlyLogCommandInGroup = val.(bool) + config.OnlyLogCommandInGroup = val.(bool) } if val, ok := jsonMap["onlyLogCommandInPrivate"]; ok { - myDice.OnlyLogCommandInPrivate = val.(bool) + config.OnlyLogCommandInPrivate = val.(bool) } if val, ok := jsonMap["refuseGroupInvite"]; ok { - myDice.RefuseGroupInvite = val.(bool) + config.RefuseGroupInvite = val.(bool) } if val, ok := jsonMap["workInQQChannel"]; ok { - myDice.WorkInQQChannel = val.(bool) + config.WorkInQQChannel = val.(bool) } if val, ok := jsonMap["QQChannelLogMessage"]; ok { - myDice.QQChannelLogMessage = val.(bool) + config.QQChannelLogMessage = val.(bool) } if val, ok := jsonMap["QQChannelAutoOn"]; ok { - myDice.QQChannelAutoOn = val.(bool) + config.QQChannelAutoOn = val.(bool) } if val, ok := jsonMap["botExtFreeSwitch"]; ok { - myDice.BotExtFreeSwitch = val.(bool) + config.BotExtFreeSwitch = val.(bool) } if val, ok := jsonMap["rateLimitEnabled"]; ok { - myDice.RateLimitEnabled = val.(bool) + config.RateLimitEnabled = val.(bool) } if val, ok := jsonMap["trustOnlyMode"]; ok { - myDice.TrustOnlyMode = val.(bool) + config.TrustOnlyMode = val.(bool) } aliveNoticeMod := false if val, ok := jsonMap["aliveNoticeEnable"]; ok { - myDice.AliveNoticeEnable = val.(bool) + config.AliveNoticeEnable = val.(bool) aliveNoticeMod = true } if val, ok := jsonMap["aliveNoticeValue"]; ok { - myDice.AliveNoticeValue = val.(string) + config.AliveNoticeValue = val.(string) aliveNoticeMod = true } if aliveNoticeMod { @@ -373,10 +290,10 @@ func DiceConfigSet(c echo.Context) error { if f < 0 { f = 0 } - if myDice.MessageDelayRangeEnd < f { - myDice.MessageDelayRangeEnd = f + if config.MessageDelayRangeEnd < f { + config.MessageDelayRangeEnd = f } - myDice.MessageDelayRangeStart = f + config.MessageDelayRangeStart = f } } @@ -387,14 +304,14 @@ func DiceConfigSet(c echo.Context) error { f = 0 } - if f >= myDice.MessageDelayRangeStart { - myDice.MessageDelayRangeEnd = f + if f >= config.MessageDelayRangeStart { + config.MessageDelayRangeEnd = f } } } if val, ok := jsonMap["friendAddComment"]; ok { - myDice.FriendAddComment = strings.TrimSpace(val.(string)) + config.FriendAddComment = strings.TrimSpace(val.(string)) } if val, ok := jsonMap["uiPassword"]; ok { @@ -409,7 +326,7 @@ func DiceConfigSet(c echo.Context) error { var items []*dice.ExtDefaultSettingItem err := json.Unmarshal(data, &items) if err == nil { - myDice.ExtDefaultSettings = items + config.ExtDefaultSettings = items myDice.ApplyExtDefaultSettings() } } @@ -434,57 +351,57 @@ func DiceConfigSet(c echo.Context) error { // } if val, ok := jsonMap["logSizeNoticeEnable"]; ok { - myDice.LogSizeNoticeEnable = val.(bool) + config.LogSizeNoticeEnable = val.(bool) } if val, ok := jsonMap["logSizeNoticeCount"]; ok { count, ok := val.(float64) if ok { - myDice.LogSizeNoticeCount = int(count) + config.LogSizeNoticeCount = int(count) } if !ok { if v, ok := val.(string); ok { v2, _ := strconv.ParseInt(v, 10, 64) - myDice.LogSizeNoticeCount = int(v2) + config.LogSizeNoticeCount = int(v2) } } - if myDice.LogSizeNoticeCount == 0 { + if config.LogSizeNoticeCount == 0 { // 不能为零 - myDice.LogSizeNoticeCount = 500 + config.LogSizeNoticeCount = dice.DefaultConfig.LogSizeNoticeCount } } if val, ok := jsonMap["customReplyConfigEnable"]; ok { - myDice.CustomReplyConfigEnable = val.(bool) + config.CustomReplyConfigEnable = val.(bool) } if val, ok := jsonMap["textCmdTrustOnly"]; ok { - myDice.TextCmdTrustOnly = val.(bool) + config.TextCmdTrustOnly = val.(bool) } if val, ok := jsonMap["ignoreUnaddressedBotCmd"]; ok { - myDice.IgnoreUnaddressedBotCmd = val.(bool) + config.IgnoreUnaddressedBotCmd = val.(bool) } if val, ok := jsonMap["QQEnablePoke"]; ok { - myDice.QQEnablePoke = val.(bool) + config.QQEnablePoke = val.(bool) } if val, ok := jsonMap["playerNameWrapEnable"]; ok { - myDice.PlayerNameWrapEnable = val.(bool) + config.PlayerNameWrapEnable = val.(bool) } if val, ok := jsonMap["mailEnable"]; ok { - myDice.MailEnable = val.(bool) + config.MailEnable = val.(bool) } if val, ok := jsonMap["mailFrom"]; ok { - myDice.MailFrom = val.(string) + config.MailFrom = val.(string) } if val, ok := jsonMap["mailPassword"]; ok { - myDice.MailPassword = val.(string) + config.MailPassword = val.(string) } if val, ok := jsonMap["mailSmtp"]; ok { - myDice.MailSMTP = val.(string) + config.MailSMTP = val.(string) } if val, ok := jsonMap["quitInactiveThreshold"]; ok { @@ -492,14 +409,14 @@ func DiceConfigSet(c echo.Context) error { switch v := val.(type) { case string: if vv, err := strconv.ParseFloat(v, 64); err == nil { - myDice.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * vv) + config.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * vv) set = true } case float64: - myDice.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * v) + config.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * v) set = true case int64: - myDice.QuitInactiveThreshold = 24 * time.Hour * time.Duration(v) + config.QuitInactiveThreshold = 24 * time.Hour * time.Duration(v) set = true default: // ignore @@ -512,7 +429,7 @@ func DiceConfigSet(c echo.Context) error { if val, ok := jsonMap["quitInactiveBatchSize"]; ok { if v, ok := val.(float64); ok { if vv := int64(v); vv > 0 { - myDice.QuitInactiveBatchSize = vv + config.QuitInactiveBatchSize = vv } } } @@ -520,7 +437,7 @@ func DiceConfigSet(c echo.Context) error { if val, ok := jsonMap["quitInactiveBatchWait"]; ok { if v, ok := val.(float64); ok { if vv := int64(v); vv > 0 { - myDice.QuitInactiveBatchWait = vv + config.QuitInactiveBatchWait = vv } } } @@ -546,7 +463,7 @@ func DiceMailTest(c echo.Context) error { return Success(&c, Response{}) } -func vmVersionForExtSetBase(c echo.Context, callback func(val string)) error { +func vmVersionSet(c echo.Context) error { if !doAuth(c) { return c.JSON(http.StatusForbidden, nil) } @@ -554,29 +471,42 @@ func vmVersionForExtSetBase(c echo.Context, callback func(val string)) error { return Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } - var data struct { + var req []struct { + Type string `json:"type"` Value string `json:"value"` } - err := c.Bind(&data) + err := c.Bind(&req) if err != nil { return Error(&c, err.Error(), nil) } + if len(req) == 0 { + return Error(&c, "缺少设置vm版本的参数", Response{}) + } - callback(data.Value) + var failTypes []string + for _, data := range req { + if data.Type == "" || data.Value == "" { + failTypes = append(failTypes, data.Type) + continue + } + switch data.Type { + case dice.VMVersionReply: + (&myDice.Config).VMVersionForReply = data.Value + case dice.VMVersionDeck: + (&myDice.Config).VMVersionForDeck = data.Value + case dice.VMVersionCustomText: + (&myDice.Config).VMVersionForCustomText = data.Value + case dice.VmVersionMsg: + (&myDice.Config).VMVersionForMsg = data.Value + default: + failTypes = append(failTypes, data.Type) + } + } myDice.MarkModified() - myDice.Parent.Save() - return Success(&c, Response{}) -} - -func vmVersionForReplySet(c echo.Context) error { - return vmVersionForExtSetBase(c, func(val string) { - myDice.VMVersionForReply = val - }) -} + myDice.Save(false) -func vmVersionForDeckSet(c echo.Context) error { - return vmVersionForExtSetBase(c, func(val string) { - myDice.VMVersionForDeck = val + return Success(&c, Response{ + "failTypes": failTypes, }) } diff --git a/api/dice_public.go b/api/dice_public.go new file mode 100644 index 00000000..6ac1ee1c --- /dev/null +++ b/api/dice_public.go @@ -0,0 +1,48 @@ +package api + +import ( + "net/http" + "time" + + "sealdice-core/dice" + + "github.com/labstack/echo/v4" +) + +func dicePublicInfo(c echo.Context) error { + if !doAuth(c) { + return c.JSON(http.StatusForbidden, nil) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "endpoints": myDice.ImSession.EndPoints, + "config": myDice.Config.PublicDiceConfig}) +} +func dicePublicSet(c echo.Context) error { + if !doAuth(c) { + return c.JSON(http.StatusForbidden, nil) + } + v := struct { + Config dice.PublicDiceConfig `json:"config"` + Selected []map[string]interface{} `json:"selected"` + }{} + err := c.Bind(&v) + if err != nil { + return Error(&c, err.Error(), Response{}) + } + myDice.Config.PublicDiceConfig = v.Config + for _, i := range myDice.ImSession.EndPoints { + i.IsPublic = false + for _, s := range v.Selected { + if id, ok := s["id"].(string); ok && i.ID == id { + i.IsPublic = true + } + } + } + myDice.PublicDiceInfoRegister() + myDice.PublicDiceEndpointRefresh() + myDice.PublicDiceSetupTick() + myDice.LastUpdatedTime = time.Now().Unix() + myDice.Save(false) + return Success(&c, Response{}) +} diff --git a/api/helpdoc.go b/api/helpdoc.go index df4bd9eb..4838b83e 100644 --- a/api/helpdoc.go +++ b/api/helpdoc.go @@ -47,7 +47,6 @@ func helpDocReload(c echo.Context) error { dm.Help.Close() dm.InitHelp() - dm.AddHelpWithDice(dm.Dice[0]) return Success(&c, Response{}) } return Error(&c, "帮助文档正在重新装载", Response{}) diff --git a/api/js.go b/api/js.go index 231e8269..04532ec1 100644 --- a/api/js.go +++ b/api/js.go @@ -26,7 +26,7 @@ func jsExec(c echo.Context) error { "testMode": true, }) } - if !myDice.JsEnable { + if !myDice.Config.JsEnable { resp := c.JSON(200, map[string]interface{}{ "result": false, "err": "js扩展支持已关闭", @@ -86,7 +86,7 @@ func jsGetRecord(c echo.Context) error { if !doAuth(c) { return c.JSON(http.StatusForbidden, nil) } - if !myDice.JsEnable { + if !myDice.Config.JsEnable { resp := c.JSON(200, map[string]interface{}{ "outputs": []string{}, }) @@ -109,7 +109,7 @@ func jsDelete(c echo.Context) error { "testMode": true, }) } - if !myDice.JsEnable { + if !myDice.Config.JsEnable { resp := c.JSON(200, map[string]interface{}{ "result": false, "err": "js扩展支持已关闭", @@ -118,13 +118,16 @@ func jsDelete(c echo.Context) error { } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.JsScriptList) { - dice.JsDelete(myDice, myDice.JsScriptList[v.Index]) + if err == nil && v.Filename != "" { + for _, js := range myDice.JsScriptList { + if js.Filename == v.Filename { + dice.JsDelete(myDice, js) + break + } } } @@ -179,7 +182,6 @@ func jsUpload(c echo.Context) error { // fmt.Println("????", filepath.Join("./data/decks", file.Filename)) file.Filename = strings.ReplaceAll(file.Filename, "/", "_") file.Filename = strings.ReplaceAll(file.Filename, "\\", "_") - fmt.Println("XXXX", filepath.Join(myDice.BaseConfig.DataDir, "scripts", file.Filename)) dst, err := os.Create(filepath.Join(myDice.BaseConfig.DataDir, "scripts", file.Filename)) if err != nil { return err @@ -200,7 +202,7 @@ func jsList(c echo.Context) error { if !doAuth(c) { return c.JSON(http.StatusForbidden, nil) } - if !myDice.JsEnable { + if !myDice.Config.JsEnable { resp := c.JSON(200, []*dice.JsScriptInfo{}) return resp } @@ -231,7 +233,7 @@ func jsShutdown(c echo.Context) error { }) } - if myDice.JsEnable { + if myDice.Config.JsEnable { myDice.JsShutdown() } @@ -243,7 +245,7 @@ func jsShutdown(c echo.Context) error { func jsStatus(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{ "result": true, - "status": myDice.JsEnable, + "status": myDice.Config.JsEnable, }) } @@ -304,24 +306,27 @@ func jsCheckUpdate(c echo.Context) error { return Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.JsScriptList) { - jsScript := myDice.JsScriptList[v.Index] - oldJs, newJs, tempFileName, errUpdate := myDice.JsCheckUpdate(jsScript) - if errUpdate != nil { - return Error(&c, errUpdate.Error(), Response{}) + if err == nil && v.Filename != "" { + for _, jsScript := range myDice.JsScriptList { + if jsScript.Filename == v.Filename { + oldJs, newJs, tempFileName, errUpdate := myDice.JsCheckUpdate(jsScript) + if errUpdate != nil { + return Error(&c, errUpdate.Error(), Response{}) + } + return Success(&c, Response{ + "old": oldJs, + "new": newJs, + "format": "javascript", + "filename": jsScript.Filename, + "tempFileName": tempFileName, + }) } - return Success(&c, Response{ - "old": oldJs, - "new": newJs, - "format": "javascript", - "tempFileName": tempFileName, - }) } + return Error(&c, "未找到脚本", Response{}) } return Success(&c, Response{}) } @@ -333,23 +338,26 @@ func jsUpdate(c echo.Context) error { if dm.JustForTest { return Error(&c, "展示模式不支持该操作", Response{"testMode": true}) } - if !myDice.JsEnable { + if !myDice.Config.JsEnable { return Error(&c, "js扩展支持已关闭", Response{}) } v := struct { - Index int `json:"index"` + Filename string `json:"filename"` TempFileName string `json:"tempFileName"` }{} err := c.Bind(&v) - if err == nil { - if v.Index >= 0 && v.Index < len(myDice.JsScriptList) { - err = myDice.JsUpdate(myDice.JsScriptList[v.Index], v.TempFileName) - if err != nil { - return Error(&c, err.Error(), Response{}) + if err == nil && v.Filename != "" { + for _, jsScript := range myDice.JsScriptList { + if jsScript.Filename == v.Filename { + err = myDice.JsUpdate(jsScript, v.TempFileName) + if err != nil { + return Error(&c, err.Error(), Response{}) + } + myDice.MarkModified() + break } - myDice.MarkModified() } } return Success(&c, Response{}) diff --git a/api/news.go b/api/news.go index 2a405472..681c71ed 100644 --- a/api/news.go +++ b/api/news.go @@ -29,7 +29,7 @@ func getNews(c echo.Context) error { } return c.JSON(http.StatusOK, map[string]interface{}{ "result": true, - "checked": mark == myDice.NewsMark, + "checked": mark == myDice.Config.NewsMark, "news": news, "newsMark": mark, }) @@ -55,7 +55,7 @@ func checkNews(c echo.Context) error { err := c.Bind(&v) if err == nil { - myDice.NewsMark = v.NewsMark + (&myDice.Config).NewsMark = v.NewsMark myDice.MarkModified() return c.JSON(http.StatusOK, map[string]interface{}{ "result": true, diff --git a/api/reply.go b/api/reply.go index fdc4bb1d..3f71cc93 100644 --- a/api/reply.go +++ b/api/reply.go @@ -202,7 +202,7 @@ func customReplyDebugModeGet(c echo.Context) error { } return c.JSON(http.StatusOK, map[string]interface{}{ - "value": myDice.ReplyDebugMode, + "value": myDice.Config.ReplyDebugMode, }) } @@ -219,9 +219,9 @@ func customReplyDebugModeSet(c echo.Context) error { return c.String(430, err.Error()) } - myDice.ReplyDebugMode = v.Value + myDice.Config.ReplyDebugMode = v.Value myDice.MarkModified() return c.JSON(http.StatusOK, map[string]interface{}{ - "value": myDice.ReplyDebugMode, + "value": myDice.Config.ReplyDebugMode, }) } diff --git a/api/resource.go b/api/resource.go index 63bfb44d..11265287 100644 --- a/api/resource.go +++ b/api/resource.go @@ -2,7 +2,7 @@ package api import ( "bytes" - "fmt" + "errors" "io" "io/fs" "mime/multipart" @@ -246,7 +246,7 @@ func ensurePathSafe(path string) error { if !strings.HasPrefix(abs, imagesPath) && !strings.HasPrefix(abs, audiosPath) && !strings.HasPrefix(abs, videosPath) { - return fmt.Errorf("invalid path") + return errors.New("invalid path") } return nil } diff --git a/api/story_log.go b/api/story_log.go index fab7fc7f..4553e31a 100644 --- a/api/story_log.go +++ b/api/story_log.go @@ -9,12 +9,13 @@ import ( "sealdice-core/dice" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" ) func storyGetInfo(c echo.Context) error { info, err := model.LogGetInfo(myDice.DBLogs) if err != nil { - fmt.Println(err) + log.Error("storyGetInfo", err) return c.JSON(http.StatusInternalServerError, err) } return c.JSON(http.StatusOK, info) @@ -27,7 +28,7 @@ func storyGetLogs(c echo.Context) error { } logs, err := model.LogGetLogs(myDice.DBLogs) if err != nil { - fmt.Println(err) + log.Error("storyGetLogs", err) return c.JSON(http.StatusInternalServerError, err) } return c.JSON(http.StatusOK, logs) @@ -70,7 +71,7 @@ func storyGetItems(c echo.Context) error { } lines, err := model.LogGetAllLines(myDice.DBLogs, c.QueryParam("groupId"), c.QueryParam("name")) if err != nil { - fmt.Println(err) + log.Error("storyGetItems", err) return c.JSON(http.StatusInternalServerError, err) } return c.JSON(http.StatusOK, lines) @@ -84,7 +85,7 @@ func storyGetItemPage(c echo.Context) error { v := model.QueryLogLinePage{} err := c.Bind(&v) if err != nil { - fmt.Println(err) + log.Error("storyGetItemPage", err) return c.JSON(http.StatusInternalServerError, err) } if v.PageNum < 1 { @@ -96,7 +97,7 @@ func storyGetItemPage(c echo.Context) error { lines, err := model.LogGetLinePage(myDice.DBLogs, &v) if err != nil { - fmt.Println(err) + log.Error("storyGetItemPage", err) return c.JSON(http.StatusInternalServerError, err) } return c.JSON(http.StatusOK, lines) @@ -109,12 +110,12 @@ func storyDelLog(c echo.Context) error { v := &model.LogInfo{} err := c.Bind(&v) if err != nil { - fmt.Println(err) + log.Error("storyDelLog", err) return c.JSON(http.StatusInternalServerError, err) } is := model.LogDelete(myDice.DBLogs, v.GroupID, v.Name) if !is { - fmt.Println(err) + log.Error("storyDelLog", "failed to delete") return c.JSON(http.StatusInternalServerError, false) } return c.JSON(http.StatusOK, true) @@ -128,7 +129,7 @@ func storyUploadLog(c echo.Context) error { _ = c.Bind(&v) unofficial, url, err := logSendToBackend(v.GroupID, v.Name) if err != nil { - fmt.Println(err) + log.Error("storyUploadLog", err) return c.JSON(http.StatusInternalServerError, err.Error()) } ret := fmt.Sprintf("跑团日志已上传服务器,链接如下:
%s", url) diff --git a/api/utils.go b/api/utils.go index 566861fe..e1a01456 100644 --- a/api/utils.go +++ b/api/utils.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "log" "net/http" "os" "path/filepath" @@ -17,6 +16,7 @@ import ( "github.com/monaco-io/request" "sealdice-core/dice" + log "sealdice-core/utils/kratos" ) type Response map[string]interface{} @@ -39,7 +39,7 @@ func Int64ToBytes(i int64) []byte { } func doAuth(c echo.Context) bool { - token := c.Request().Header.Get("token") + token := c.Request().Header.Get("token") //nolint:canonicalheader // private header if token == "" { token = c.QueryParam("token") } @@ -74,11 +74,11 @@ func GetHexData(c echo.Context, method string, name string) (value []byte, finis return value, false } -var times = 0 +var getAvatarCounter = 0 func getGithubAvatar(c echo.Context) error { - times++ - if times > 500 { + getAvatarCounter++ + if getAvatarCounter > 500 { // 请求次数过多 return c.JSON(http.StatusNotFound, "") } @@ -110,10 +110,10 @@ func packGocqConfig(relWorkDir string) *bytes.Buffer { zipWriter := zip.NewWriter(buf) if err := compressFile(filepath.Join(rootPath, "config.yml"), "config.yml", zipWriter); err != nil { - log.Println(err) + log.Error(err) } if err := compressFile(filepath.Join(rootPath, "device.json"), "device.json", zipWriter); err != nil { - log.Println(err) + log.Error(err) } _ = compressFile(filepath.Join(rootPath, "data/versions/1.json"), "data/versions/6.json", zipWriter) _ = compressFile(filepath.Join(rootPath, "data/versions/6.json"), "data/versions/6.json", zipWriter) @@ -148,11 +148,12 @@ func checkUidExists(c echo.Context, uid string) bool { var relWorkDir string if pa.BuiltinMode == "lagrange" { relWorkDir = "extra/lagrange-qq" + uid + } else if pa.BuiltinMode == "lagrange-gocq" { + relWorkDir = "extra/lagrange-gocq-qq" + uid } else { // 默认为gocq relWorkDir = "extra/go-cqhttp-qq" + uid } - fmt.Println(relWorkDir, i.RelWorkDir) if relWorkDir == i.RelWorkDir { // 不允许工作路径重复 _ = c.JSON(CodeAlreadyExists, i) @@ -169,7 +170,45 @@ func checkUidExists(c echo.Context, uid string) bool { return false } -var timeout = 5 * time.Second +const ( + checkTimes = 3 + checkTimeout = 5 * time.Second +) + +func checkHTTPConnectivity(url string) bool { + client := http.Client{ + Timeout: checkTimeout, + } + rsChan := make(chan bool, checkTimes) + once := func(wg *sync.WaitGroup, url string) { + defer wg.Done() + resp, err := client.Get(url) + log.Debugf("check http connectivity, url=%s", url) + if err == nil { + _ = resp.Body.Close() + rsChan <- true + } else { + log.Debugf("url can't be connected, error: %s", err) + rsChan <- false + } + } + + var wg sync.WaitGroup + wg.Add(checkTimes) + for range checkTimes { + go once(&wg, url) + } + go func() { + wg.Wait() + close(rsChan) + }() + + ok := true + for res := range rsChan { + ok = ok && res + } + return ok +} func checkNetworkHealth(c echo.Context) error { total := 5 // baidu, seal, sign, google, github @@ -178,25 +217,20 @@ func checkNetworkHealth(c echo.Context) error { wg.Add(total) rsChan := make(chan string, 5) - checkHTTPConnectivity := func(target string, urls []string) { + checkUrls := func(target string, urls []string) { defer wg.Done() - client := http.Client{ - Timeout: timeout, - } for _, url := range urls { - resp, err := client.Get(url) - if err == nil { - _ = resp.Body.Close() + if checkHTTPConnectivity(url) { rsChan <- target break } } } - go checkHTTPConnectivity("baidu", []string{"https://baidu.com"}) - go checkHTTPConnectivity("seal", dice.BackendUrls) - go checkHTTPConnectivity("sign", []string{"https://sign.lagrangecore.org/api/sign/ping"}) - go checkHTTPConnectivity("google", []string{"https://google.com"}) - go checkHTTPConnectivity("github", []string{"https://github.com"}) + go checkUrls("baidu", []string{"https://baidu.com"}) + go checkUrls("seal", dice.BackendUrls) + go checkUrls("sign", []string{"https://sign.lagrangecore.org/api/sign/ping"}) + go checkUrls("google", []string{"https://google.com"}) + go checkUrls("github", []string{"https://github.com"}) go func() { wg.Wait() diff --git a/dice/builtin_commands.go b/dice/builtin_commands.go index 1cce0bce..06379c7f 100644 --- a/dice/builtin_commands.go +++ b/dice/builtin_commands.go @@ -13,11 +13,12 @@ import ( "time" "github.com/golang-module/carbon" - "github.com/samber/lo" - "github.com/juliangruber/go-intersect" cp "github.com/otiai10/copy" + "github.com/samber/lo" ds "github.com/sealdice/dicescript" + + "sealdice-core/dice/docengine" ) /** 这几条指令不能移除 */ @@ -75,7 +76,7 @@ func (d *Dice) registerCoreCommands() { if reason == "" { reason = "骰主指令" } - d.BanList.AddScoreBase(uid, d.BanList.ThresholdBan, "骰主指令", reason, ctx) + (&d.Config).BanList.AddScoreBase(uid, (&d.Config).BanList.ThresholdBan, "骰主指令", reason, ctx) ReplyToSender(ctx, msg, fmt.Sprintf("已将用户/群组 %s 加入黑名单,原因: %s", uid, reason)) case "rm", "del": uid = getID() @@ -83,7 +84,7 @@ func (d *Dice) registerCoreCommands() { return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} } - item, ok := d.BanList.GetByID(uid) + item, ok := (&d.Config).BanList.GetByID(uid) if !ok || (item.Rank != BanRankBanned && item.Rank != BanRankTrusted && item.Rank != BanRankWarn) { ReplyToSender(ctx, msg, "找不到用户/群组") break @@ -99,14 +100,14 @@ func (d *Dice) registerCoreCommands() { return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} } - d.BanList.SetTrustByID(uid, "骰主指令", "骰主指令") + (&d.Config).BanList.SetTrustByID(uid, "骰主指令", "骰主指令") ReplyToSender(ctx, msg, fmt.Sprintf("已将用户/群组 %s 加入信任列表", uid)) case "list", "show": // ban/warn/trust var extra, text string extra = cmdArgs.GetArgN(2) - d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool { + (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool { if v.Rank == BanRankNormal { return true } @@ -133,7 +134,7 @@ func (d *Dice) registerCoreCommands() { break } - v, exists := d.BanList.Map.Load(targetID) + v, exists := (&d.Config).BanList.Map.Load(targetID) if !exists { ReplyToSender(ctx, msg, fmt.Sprintf("所查询的<%s>情况:正常(0)", targetID)) break @@ -229,10 +230,12 @@ func (d *Dice) registerCoreCommands() { var ( useGroupSearch bool group string + text string = cmdArgs.CleanArgs ) - if _group := cmdArgs.GetArgN(1); strings.HasPrefix(_group, "#") { + if rawGroup := cmdArgs.GetArgN(1); strings.HasPrefix(rawGroup, "#") { useGroupSearch = true - fakeGroup := strings.TrimPrefix(_group, "#") + fakeGroup := strings.TrimPrefix(rawGroup, "#") + text = strings.TrimPrefix(text, rawGroup+" ") // 转换 group 别名 if _g, ok := d.Parent.Help.GroupAliases[fakeGroup]; ok { @@ -248,6 +251,7 @@ func (d *Dice) registerCoreCommands() { var id string if cmdArgs.GetKwarg("rand") != nil || cmdArgs.GetKwarg("随机") != nil { + // FIXME: byd WHAT IS THAT _id := rand.Uint64()%d.Parent.Help.CurID + 1 id = strconv.FormatUint(_id, 10) } @@ -268,8 +272,10 @@ func (d *Dice) registerCoreCommands() { } if id != "" { - text, exists := d.Parent.Help.TextMap.Load(id) - if exists { + text, err := d.Parent.Help.searchEngine.GetItemByID(id) + if err == nil { + // Copied from 支援换行符 By Fripine #963 + text.Content = ctx.TranslateSplit(text.Content) content := d.Parent.Help.GetContent(text, 0) ReplyToSender(ctx, msg, fmt.Sprintf("词条: %s:%s\n%s", text.PackageName, text.Title, content)) } else { @@ -278,14 +284,16 @@ func (d *Dice) registerCoreCommands() { return CmdExecuteResult{Matched: true, Solved: true} } - var val string - if useGroupSearch { - val = cmdArgs.GetArgN(2) - } else { - val = cmdArgs.GetArgN(1) - } - if val == "" { - return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} + { // 判断是否关键字缺失 + var val string + if useGroupSearch { + val = cmdArgs.GetArgN(2) + } else { + val = cmdArgs.GetArgN(1) + } + if val == "" { + return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} + } } numLimit := 4 @@ -305,8 +313,6 @@ func (d *Dice) registerCoreCommands() { } } - text := strings.TrimPrefix(cmdArgs.CleanArgs, "#"+group+" ") - if numLimit <= 0 { numLimit = 1 } else if numLimit > 10 { @@ -319,6 +325,7 @@ func (d *Dice) registerCoreCommands() { // 未指定搜索分组时,取当前群指定的分组 group = ctx.Group.DefaultHelpGroup } + // 进行结果搜索 search, total, pgStart, pgEnd, err := d.Parent.Help.Search(ctx, text, false, numLimit, page, group) if err != nil { ReplyToSender(ctx, msg, groupStr+"搜索故障: "+err.Error()) @@ -334,20 +341,25 @@ func (d *Dice) registerCoreCommands() { } hasSecond := len(search.Hits) >= 2 - best, ok := d.Parent.Help.TextMap.Load(search.Hits[0].ID) - if !ok { - d.Logger.Errorf("加载d.Parent.Help.TextMap.Load(search.Hits[0].ID)->(%s)的数据出现错误!", search.Hits[0].ID) - ReplyToSender(ctx, msg, "未找到搜索结果,出现数据加载错误!") - return CmdExecuteResult{Matched: true, Solved: true} + // 准备接下来读取这里面的Fields + bestRaw := search.Hits[0].Fields + best := &docengine.HelpTextItem{ + Group: fmt.Sprintf("%v", bestRaw["group"]), + From: fmt.Sprintf("%v", bestRaw["from"]), + Title: fmt.Sprintf("%v", bestRaw["title"]), + Content: fmt.Sprintf("%v", bestRaw["content"]), + PackageName: fmt.Sprintf("%v", bestRaw["package"]), + // 这俩是什么东西?! + // KeyWords: "", + // RelatedExt: nil, } others := "" for _, i := range search.Hits { - t, ok := d.Parent.Help.TextMap.Load(i.ID) - if !ok { - d.Logger.Errorf("加载d.Parent.Help.TextMap.Load(search.Hits[0].ID)->(%s)的数据出现错误!", search.Hits[0].ID) - ReplyToSender(ctx, msg, "未找到搜索结果,出现数据加载错误!") - return CmdExecuteResult{Matched: true, Solved: true} + t := &docengine.HelpTextItem{ + Group: fmt.Sprintf("%v", i.Fields["group"]), + Title: fmt.Sprintf("%v", i.Fields["title"]), + PackageName: fmt.Sprintf("%v", i.Fields["package"]), } if t.Group != "" && t.Group != HelpBuiltinGroup { others += fmt.Sprintf("[%s][%s]【%s:%s】 匹配度%.2f\n", i.ID, t.Group, t.PackageName, t.Title, i.Score) @@ -375,6 +387,8 @@ func (d *Dice) registerCoreCommands() { var bestResult string if showBest { + // Copied from 支援换行符 By Fripine #963 + best.Content = ctx.TranslateSplit(best.Content) content := d.Parent.Help.GetContent(best, 0) bestResult = fmt.Sprintf("最优先结果%s:\n词条: %s:%s\n%s\n\n", groupStr, best.PackageName, best.Title, content) } @@ -428,7 +442,6 @@ func (d *Dice) registerCoreCommands() { dm.Help.Close() dm.InitHelp() - dm.AddHelpWithDice(dm.Dice[0]) ReplyToSender(ctx, msg, "帮助文档已经重新装载") } else { ReplyToSender(ctx, msg, "帮助文档正在重新装载,请稍后...") @@ -466,11 +479,16 @@ func (d *Dice) registerCoreCommands() { search, _, _, _, err := d.Parent.Help.Search(ctx, cmdArgs.CleanArgs, true, 1, 1, "") if err == nil { if len(search.Hits) > 0 { - a, ok := d.Parent.Help.TextMap.Load(search.Hits[0].ID) - if !ok { - d.Logger.Error("HELPDOC:读取ID对应的信息出现问题") - ReplyToSender(ctx, msg, "HELPDOC:读取ID对应的信息出现问题") - return CmdExecuteResult{Matched: true, Solved: true} + a := &docengine.HelpTextItem{ + Group: fmt.Sprintf("%v", search.Hits[0].Fields["group"]), + From: fmt.Sprintf("%v", search.Hits[0].Fields["from"]), + Title: fmt.Sprintf("%v", search.Hits[0].Fields["title"]), + // Edited. Original change from 支援换行符 By Fripine #963 + Content: ctx.TranslateSplit(fmt.Sprintf("%v", search.Hits[0].Fields["content"])), + PackageName: fmt.Sprintf("%v", search.Hits[0].Fields["package"]), + // 这俩是什么东西?! + KeyWords: "", + RelatedExt: nil, } content := d.Parent.Help.GetContent(a, 0) ReplyToSender(ctx, msg, fmt.Sprintf("%s:%s\n%s", a.PackageName, a.Title, content)) @@ -495,7 +513,7 @@ func (d *Dice) registerCoreCommands() { if inGroup { // 不响应裸指令选项 - if len(cmdArgs.At) < 1 && ctx.Dice.IgnoreUnaddressedBotCmd { + if len(cmdArgs.At) < 1 && ctx.Dice.Config.IgnoreUnaddressedBotCmd { return CmdExecuteResult{Matched: true, Solved: false} } // 不响应at其他人 @@ -527,7 +545,7 @@ func (d *Dice) registerCoreCommands() { } if cmdArgs.IsArgEqual(1, "on") { - if !(msg.Platform == "QQ-CH" || ctx.Dice.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) { + if !(msg.Platform == "QQ-CH" || ctx.Dice.Config.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者")) return CmdExecuteResult{Matched: true, Solved: true} } @@ -550,7 +568,7 @@ func (d *Dice) registerCoreCommands() { return CmdExecuteResult{Matched: true, Solved: true} } else if cmdArgs.IsArgEqual(1, "off") { - if !(msg.Platform == "QQ-CH" || ctx.Dice.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) { + if !(msg.Platform == "QQ-CH" || ctx.Dice.Config.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者")) return CmdExecuteResult{Matched: true, Solved: true} } @@ -1006,7 +1024,7 @@ func (d *Dice) registerCoreCommands() { dm.UpdateCheckRequestChan <- 1 // 等待获取新版本,最多10s - for i := 0; i < 5; i++ { + for range 5 { time.Sleep(2 * time.Second) if dm.AppVersionOnline != nil { break @@ -1036,11 +1054,11 @@ func (d *Dice) registerCoreCommands() { ret := <-dm.UpdateDownloadedChan if ctx.IsPrivate { - ctx.Dice.UpgradeWindowID = msg.Sender.UserID + ctx.Dice.Config.UpgradeWindowID = msg.Sender.UserID } else { - ctx.Dice.UpgradeWindowID = ctx.Group.GroupID + ctx.Dice.Config.UpgradeWindowID = ctx.Group.GroupID } - ctx.Dice.UpgradeEndpointID = ctx.EndPoint.ID + ctx.Dice.Config.UpgradeEndpointID = ctx.EndPoint.ID ctx.Dice.Save(true) bakFn, _ := ctx.Dice.Parent.Backup(BackupSelectionAll, false) @@ -1103,7 +1121,6 @@ func (d *Dice) registerCoreCommands() { dm.IsHelpReloading = true dm.Help.Close() dm.InitHelp() - dm.AddHelpWithDice(dice) ReplyToSender(ctx, msg, "帮助文档已重载") } else { ReplyToSender(ctx, msg, "帮助文档正在重新装载") @@ -1207,7 +1224,7 @@ func (d *Dice) registerCoreCommands() { } ctx.SystemTemplate = ctx.Group.GetCharTemplate(ctx.Dice) - if ctx.Dice.CommandCompatibleMode { + if ctx.Dice.Config.CommandCompatibleMode { if (cmdArgs.Command == "rd" || cmdArgs.Command == "rhd" || cmdArgs.Command == "rdh") && len(cmdArgs.Args) >= 1 { if m, _ := regexp.MatchString(`^\d|优势|劣势|\+|-`, cmdArgs.CleanArgs); m { if cmdArgs.IsSpaceBeforeArgs { @@ -1348,12 +1365,12 @@ func (d *Dice) registerCoreCommands() { if cmdArgs.SpecialExecuteTimes > 1 { VarSetValueInt64(ctx, "$t次数", int64(cmdArgs.SpecialExecuteTimes)) - if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.MaxExecuteTime) { + if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告")) return CmdExecuteResult{Matched: true, Solved: true} } var texts []string - for i := 0; i < cmdArgs.SpecialExecuteTimes; i++ { + for range cmdArgs.SpecialExecuteTimes { ret := rollOne() if ret != nil { return *ret @@ -1501,7 +1518,7 @@ func (d *Dice) registerCoreCommands() { if cmdArgs.IsArgEqual(1, "list") { showList() } else if cmdArgs.IsArgEqual(last, "on") { - if !ctx.Dice.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 { + if !ctx.Dice.Config.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者")) return CmdExecuteResult{Matched: true, Solved: true} } @@ -1524,7 +1541,7 @@ func (d *Dice) registerCoreCommands() { var extNames []string var conflictsAll []string - for index := 0; index < len(cmdArgs.Args); index++ { + for index := range len(cmdArgs.Args) { extName := strings.ToLower(cmdArgs.Args[index]) if i := d.ExtFind(extName); i != nil { extNames = append(extNames, extName) @@ -1544,14 +1561,14 @@ func (d *Dice) registerCoreCommands() { ReplyToSender(ctx, msg, text) } } else if cmdArgs.IsArgEqual(last, "off") { - if !ctx.Dice.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 { + if !ctx.Dice.Config.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者")) return CmdExecuteResult{Matched: true, Solved: true} } var closed []string var notfound []string - for index := 0; index < len(cmdArgs.Args); index++ { + for index := range len(cmdArgs.Args) { extName := cmdArgs.Args[index] extName = d.ExtAliasToName(extName) ei := ctx.Group.ExtInactiveByName(extName) @@ -1917,7 +1934,7 @@ func (d *Dice) registerCoreCommands() { setCurPlayerName(b) } attrs.LastModifiedTime = time.Now().Unix() - attrs.SaveToDB(am.db, nil) // 直接保存 + attrs.SaveToDB(am.db) // 直接保存 ReplyToSender(ctx, msg, "操作完成") } else { ReplyToSender(ctx, msg, "此角色名已存在") diff --git a/dice/censor/trie.go b/dice/censor/trie.go index 6810ff72..8cdd043c 100644 --- a/dice/censor/trie.go +++ b/dice/censor/trie.go @@ -57,7 +57,7 @@ func (t *trie) Match(text string) (sensitiveWords map[string]Level) { sensitiveWords = map[string]Level{} chars := []rune(text) - for i := 0; i < len(chars); i++ { + for i := range chars { cur := t.root.findChild(chars[i]) if cur == nil { continue diff --git a/dice/config.go b/dice/config.go index a10a3591..8c218c27 100644 --- a/dice/config.go +++ b/dice/config.go @@ -14,11 +14,11 @@ import ( "time" wr "github.com/mroth/weightedrand" - "golang.org/x/time/rate" + "github.com/samber/lo" "gopkg.in/yaml.v3" "sealdice-core/dice/model" - "sealdice-core/utils" + log "sealdice-core/utils/kratos" ) // type TextTemplateWithWeight = map[string]map[string]uint @@ -78,74 +78,94 @@ type ConfigItem struct { } func (i *ConfigItem) UnmarshalJSON(data []byte) error { - raw := map[string]any{} + raw := map[string]json.RawMessage{} if err := json.Unmarshal(data, &raw); err != nil { return err } - var ok bool - if i.Key, ok = raw["key"].(string); !ok { - return errors.New("'key' must be a string") + if err := json.Unmarshal(raw["key"], &i.Key); err != nil { + return fmt.Errorf("ConfigItem: unmarshal 'key' failed as %w", err) } - if i.Type, ok = raw["type"].(string); !ok { - return errors.New("'type' must be a string") + if err := json.Unmarshal(raw["type"], &i.Type); err != nil { + return fmt.Errorf("ConfigItem (%s): unmarshal 'type' failed as %w", i.Key, err) } - if i.Description, ok = raw["description"].(string); !ok { - return errors.New("'description' must be a string") + if err := json.Unmarshal(raw["description"], &i.Description); err != nil { + return fmt.Errorf("ConfigItem (%s): unmarshal 'description' failed as %w", i.Key, err) } if v, ok := raw["deprecated"]; ok { - if i.Deprecated, ok = v.(bool); !ok { - return errors.New("'deprecated' must be a bool") + if err := json.Unmarshal(v, &i.Deprecated); err != nil { + return fmt.Errorf("ConfigItem (%s): unmarshal 'deprecated' failed as %w", i.Key, err) } } switch i.Type { - case "string", "bool", "float": - i.DefaultValue = raw["defaultValue"] - i.Value = raw["value"] + case "string", "task:cron", "task:daily": + var stringVal string + if err := json.Unmarshal(raw["defaultValue"], &stringVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) + } + i.DefaultValue = stringVal + if err := json.Unmarshal(raw["value"], &stringVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) + } + i.Value = stringVal + case "bool": + var boolVal bool + if err := json.Unmarshal(raw["defaultValue"], &boolVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) + } + i.DefaultValue = boolVal + if err := json.Unmarshal(raw["value"], &boolVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) + } + i.Value = boolVal + case "float": + var floatVal float64 + if err := json.Unmarshal(raw["defaultValue"], &floatVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) + } + i.DefaultValue = floatVal + if err := json.Unmarshal(raw["value"], &floatVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) + } + i.Value = floatVal case "int": - // 2024.08.09 1.4.6首发版本unmarshal产生类型报错修复 - if v, ok := raw["defaultValue"].(float64); ok { - i.DefaultValue = int64(v) - } else if v, ok := raw["defaultValue"].(int64); ok { - i.DefaultValue = v + var intVal int64 + if err := json.Unmarshal(raw["defaultValue"], &intVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) } - if v, ok := raw["value"]; ok { - if v2, ok := v.(float64); ok { - i.Value = int64(v2) - } else if v2, ok := v.(int64); ok { - i.Value = v2 - } + i.DefaultValue = intVal + if err := json.Unmarshal(raw["value"], &intVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) } + i.Value = intVal case "template": - { - v := raw["defaultValue"].([]interface{}) - strarr := make([]string, len(v)) - for i, vv := range v { - strarr[i] = vv.(string) - } - i.DefaultValue = strarr + var templateVal []string + if err := json.Unmarshal(raw["defaultValue"], &templateVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) } - if v, ok := raw["value"]; ok { - vv := v.([]interface{}) - strarr := make([]string, len(vv)) - for i, vv := range vv { - strarr[i] = vv.(string) - } - i.Value = strarr + i.DefaultValue = templateVal + if err := json.Unmarshal(raw["value"], &templateVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) } + i.Value = templateVal case "option": - i.DefaultValue = raw["defaultValue"] - i.Value = raw["value"] - v := raw["option"].([]interface{}) - strarr := make([]string, len(v)) - for i, vv := range v { - strarr[i] = vv.(string) + var stringVal string + var optionVal []string + if err := json.Unmarshal(raw["defaultValue"], &stringVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err) } - i.Option = strarr + i.DefaultValue = stringVal + if err := json.Unmarshal(raw["value"], &stringVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err) + } + i.Value = stringVal + if err := json.Unmarshal(raw["option"], &optionVal); err != nil { + return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'option' failed as %w", i.Key, i.Type, err) + } + i.Option = optionVal default: - return errors.New("unsupported type " + i.Type) + return errors.New("ConfigItem.UnmarshalJSON: unsupported type " + i.Type) } - return nil } @@ -326,16 +346,16 @@ func (cm *ConfigManager) getConfig(pluginName, key string) *ConfigItem { func (cm *ConfigManager) ResetConfigToDefault(pluginName, key string) { cm.lock.Lock() defer cm.lock.Unlock() - fmt.Println("try reset config to default", pluginName, key) + log.Debug("try reset config to default", pluginName, key) plugin, ok := cm.Plugins[pluginName] if !ok { - fmt.Println("plugin not found", pluginName) + log.Debug("plugin not found", pluginName) return } configItem, exists := plugin.Configs[key] if exists { - fmt.Println("reset config to default", pluginName, key) + log.Debug("reset config to default", pluginName, key) configItem.Value = configItem.DefaultValue plugin.Configs[key] = configItem if strings.HasPrefix(configItem.Type, "task:") { @@ -755,9 +775,19 @@ func setupBaseTextTemplate(d *Dice) { "制卡_分隔符": { {`\n`, 1}, }, + "检定_单项结果文本": { + {`{$t检定过程文本} = {$t检定结果}`, 1}, + }, "检定": { {`{$t玩家}的"{$t技能}"检定(DND5E)结果为: {$t检定过程文本} = {$t检定结果}`, 1}, }, + "检定_多轮": { + {`对{$t玩家}的"{$t技能}"进行了{$t次数}次检定(DND5E),结果为:\n{$t结果文本}`, 1}, + }, + "检定_轮数过多警告": { + {`你真的需要这么多轮检定?{核心:骰子名字}将对你提高警惕!`, 1}, + {`不支持连续检定{$t次数}次,{核心:骰子名字}觉得这太多了。`, 1}, + }, }, "核心": { "骰子名字": { @@ -1003,6 +1033,9 @@ func setupBaseTextTemplate(d *Dice) { "鸽子理由": guguReason, }, "其它": { + "抽牌_牌堆列表": { + {"载入并开启的牌堆:\n{$t牌堆列表}", 1}, + }, "抽牌_列表": { {"{$t原始列表}", 1}, }, @@ -1463,9 +1496,18 @@ func setupBaseTextTemplate(d *Dice) { SubType: ".dndx", ExtraText: "带属性名", }, + "检定_单项结果文本": { + SubType: ".rc", + }, "检定": { SubType: ".rc 力量", }, + "检定_多轮": { + SubType: ".rc 3#力量", + }, + "检定_轮数过多警告": { + SubType: ".rc 30#", + }, }, "核心": { "骰子名字": { @@ -1724,6 +1766,9 @@ func setupBaseTextTemplate(d *Dice) { }, }, "其它": { + "抽牌_牌堆列表": { + SubType: ".draw list", + }, "抽牌_列表": { SubType: ".draw keys", }, @@ -2057,156 +2102,32 @@ func getNumVal(i interface{}) uint { } func (d *Dice) loads() { + config := NewConfig(d) data, err := os.ReadFile(filepath.Join(d.BaseConfig.DataDir, "serve.yaml")) - - // 配置这块弄得比较屎,有机会换个方案。。。 - // TODO(Xiangze Li): 不管谁都好 赶紧重写吧, 谁能想起来加了配置还要在这里添一行才能Load出来哇 if err == nil { //nolint:nestif + err3 := config.LoadYamlConfig(data) + if err3 != nil { + d.Logger.Error("serve.yaml parse failed") + panic(err3) + } + + // 有一些配置项被用 jsbind 导出了,只能先留在 Dice 不迁移了 dNew := Dice{} err2 := yaml.Unmarshal(data, &dNew) if err2 != nil { d.Logger.Error("serve.yaml parse failed") panic(err2) } - d.CommandCompatibleMode = true // 一直为true即可 d.ImSession.EndPoints = dNew.ImSession.EndPoints - d.CommandPrefix = dNew.CommandPrefix d.DiceMasters = dNew.DiceMasters - d.VersionCode = dNew.VersionCode - d.MessageDelayRangeStart = dNew.MessageDelayRangeStart - d.MessageDelayRangeEnd = dNew.MessageDelayRangeEnd - d.WorkInQQChannel = dNew.WorkInQQChannel - d.QQChannelLogMessage = dNew.QQChannelLogMessage - d.QQChannelAutoOn = dNew.QQChannelAutoOn - d.QQEnablePoke = dNew.QQEnablePoke - d.TextCmdTrustOnly = dNew.TextCmdTrustOnly - d.IgnoreUnaddressedBotCmd = dNew.IgnoreUnaddressedBotCmd - d.UILogLimit = dNew.UILogLimit - d.FriendAddComment = dNew.FriendAddComment - d.NoticeIDs = dNew.NoticeIDs - d.ExtDefaultSettings = dNew.ExtDefaultSettings - d.CustomReplyConfigEnable = dNew.CustomReplyConfigEnable - d.RefuseGroupInvite = dNew.RefuseGroupInvite - d.DefaultCocRuleIndex = dNew.DefaultCocRuleIndex - d.UpgradeWindowID = dNew.UpgradeWindowID - d.UpgradeEndpointID = dNew.UpgradeEndpointID - d.BotExtFreeSwitch = dNew.BotExtFreeSwitch - d.RateLimitEnabled = dNew.RateLimitEnabled - d.TrustOnlyMode = dNew.TrustOnlyMode - d.AliveNoticeEnable = dNew.AliveNoticeEnable - d.AliveNoticeValue = dNew.AliveNoticeValue - d.ReplyDebugMode = dNew.ReplyDebugMode - d.LogSizeNoticeCount = dNew.LogSizeNoticeCount - d.LogSizeNoticeEnable = dNew.LogSizeNoticeEnable - d.PlayerNameWrapEnable = dNew.PlayerNameWrapEnable - d.MailEnable = dNew.MailEnable - d.MailFrom = dNew.MailFrom - d.MailPassword = dNew.MailPassword - d.MailSMTP = dNew.MailSMTP - d.JsEnable = dNew.JsEnable - d.DisabledJsScripts = dNew.DisabledJsScripts - d.NewsMark = dNew.NewsMark - - d.QuitInactiveThreshold = dNew.QuitInactiveThreshold - d.QuitInactiveBatchSize = dNew.QuitInactiveBatchSize - if d.QuitInactiveBatchSize == 0 { - d.QuitInactiveBatchSize = 10 - } - d.QuitInactiveBatchWait = dNew.QuitInactiveBatchWait - if d.QuitInactiveBatchWait == 0 { - d.QuitInactiveBatchWait = 30 - } - - d.EnableCensor = dNew.EnableCensor - d.CensorMode = dNew.CensorMode - d.CensorThresholds = dNew.CensorThresholds - d.CensorHandlers = dNew.CensorHandlers - d.CensorScores = dNew.CensorScores - d.CensorCaseSensitive = dNew.CensorCaseSensitive - d.CensorMatchPinyin = dNew.CensorMatchPinyin - d.CensorFilterRegexStr = dNew.CensorFilterRegexStr - - d.VMVersionForDeck = dNew.VMVersionForDeck - d.VMVersionForReply = dNew.VMVersionForReply - - if d.VMVersionForDeck == "" { - d.VMVersionForDeck = "v2" - } - - if d.VMVersionForReply == "" { - d.VMVersionForReply = "v1" - } - - if dNew.BanList != nil { - d.BanList.BanBehaviorRefuseReply = dNew.BanList.BanBehaviorRefuseReply - d.BanList.BanBehaviorRefuseInvite = dNew.BanList.BanBehaviorRefuseInvite - d.BanList.BanBehaviorQuitLastPlace = dNew.BanList.BanBehaviorQuitLastPlace - d.BanList.BanBehaviorQuitPlaceImmediately = dNew.BanList.BanBehaviorQuitPlaceImmediately - d.BanList.BanBehaviorQuitIfAdmin = dNew.BanList.BanBehaviorQuitIfAdmin - - d.BanList.ScoreReducePerMinute = dNew.BanList.ScoreReducePerMinute - - d.BanList.ThresholdWarn = dNew.BanList.ThresholdWarn - d.BanList.ThresholdBan = dNew.BanList.ThresholdBan - d.BanList.ScoreGroupMuted = dNew.BanList.ScoreGroupMuted - d.BanList.ScoreGroupKicked = dNew.BanList.ScoreGroupKicked - d.BanList.ScoreTooManyCommand = dNew.BanList.ScoreTooManyCommand - - d.BanList.JointScorePercentOfGroup = dNew.BanList.JointScorePercentOfGroup - d.BanList.JointScorePercentOfInviter = dNew.BanList.JointScorePercentOfInviter - } - - d.MaxExecuteTime = dNew.MaxExecuteTime - if d.MaxExecuteTime == 0 { - d.MaxExecuteTime = 12 - } - - d.MaxCocCardGen = dNew.MaxCocCardGen - if d.MaxCocCardGen == 0 { - d.MaxCocCardGen = 5 - } - - d.PersonalReplenishRateStr = dNew.PersonalReplenishRateStr - if d.PersonalReplenishRateStr == "" { - d.PersonalReplenishRateStr = "@every 3s" - d.PersonalReplenishRate = rate.Every(time.Second * 3) - } else { - if parsed, errParse := utils.ParseRate(d.PersonalReplenishRateStr); errParse == nil { - d.PersonalReplenishRate = parsed - } else { - d.Logger.Errorf("解析PersonalReplenishRate失败: %v", errParse) - d.PersonalReplenishRateStr = "@every 3s" - d.PersonalReplenishRate = rate.Every(time.Second * 3) - } - } - - d.PersonalBurst = dNew.PersonalBurst - if d.PersonalBurst == 0 { - d.PersonalBurst = 3 + if len(d.DiceMasters) == 0 { + d.DiceMasters = DefaultConfig.DiceMasters } - - d.GroupReplenishRateStr = dNew.GroupReplenishRateStr - if d.GroupReplenishRateStr == "" { - d.GroupReplenishRateStr = "@every 3s" - d.GroupReplenishRate = rate.Every(time.Second * 3) - } else { - if parsed, errParse := utils.ParseRate(d.GroupReplenishRateStr); errParse == nil { - d.GroupReplenishRate = parsed - } else { - d.Logger.Errorf("解析GroupReplenishRate失败: %v", errParse) - d.GroupReplenishRateStr = "@every 3s" - d.GroupReplenishRate = rate.Every(time.Second * 3) - } - } - - d.GroupBurst = dNew.GroupBurst - if d.GroupBurst == 0 { - d.GroupBurst = 3 - } - - if d.DiceMasters == nil || len(d.DiceMasters) == 0 { - d.DiceMasters = []string{"UI:1001"} + d.CommandPrefix = dNew.CommandPrefix + if len(d.CommandPrefix) == 0 { + d.CommandPrefix = DefaultConfig.CommandPrefix } + d.DeckList = dNew.DeckList var newDiceMasters []string for _, i := range d.DiceMasters { if i != "<平台,如QQ>:<帐号,如QQ号>" { @@ -2252,6 +2173,9 @@ func (d *Dice) loads() { d.ImSession.ServiceAtNew.Range(func(_ string, groupInfo *GroupInfo) bool { // Pinenutn: ServiceAtNew重构 var tmp []*ExtInfo + groupInfo.ExtListSnapshot = lo.Map(groupInfo.ActivatedExtList, func(item *ExtInfo, index int) string { + return item.Name + }) for _, i := range groupInfo.ActivatedExtList { if m[i.Name] != nil { tmp = append(tmp, m[i.Name]) @@ -2277,22 +2201,7 @@ func (d *Dice) loads() { return true }) - if d.VersionCode != 0 && d.VersionCode < 10000 { - d.CustomReplyConfigEnable = false - } - - if d.VersionCode != 0 && d.VersionCode < 10001 { - d.AliveNoticeValue = "@every 3h" - } - - if d.VersionCode != 0 && d.VersionCode < 10003 { - d.Logger.Infof("进行配置文件版本升级: %d -> %d", d.VersionCode, 10003) - d.LogSizeNoticeCount = 500 - d.LogSizeNoticeEnable = true - d.CustomReplyConfigEnable = true - } - - if d.VersionCode != 0 && d.VersionCode < 10005 { + if config.VersionCode != 0 && config.VersionCode < 10005 { d.RunAfterLoaded = append(d.RunAfterLoaded, func() { d.Logger.Info("正在自动升级自定义文案文件") for index, text := range d.TextMapRaw["核心"]["昵称_重置"] { @@ -2315,10 +2224,10 @@ func (d *Dice) loads() { } // 1.2 版本 - if d.VersionCode != 0 && d.VersionCode < 10200 { - d.TextCmdTrustOnly = true - d.QQEnablePoke = true - d.PlayerNameWrapEnable = true + if config.VersionCode != 0 && config.VersionCode < 10200 { + config.TextCmdTrustOnly = DefaultConfig.TextCmdTrustOnly + config.QQEnablePoke = DefaultConfig.QQEnablePoke + config.PlayerNameWrapEnable = DefaultConfig.PlayerNameWrapEnable isUI1001Master := false for _, i := range d.DiceMasters { @@ -2346,7 +2255,7 @@ func (d *Dice) loads() { } // 1.2 版本 - if d.VersionCode != 0 && d.VersionCode < 10203 { + if config.VersionCode != 0 && config.VersionCode < 10203 { d.RunAfterLoaded = append(d.RunAfterLoaded, func() { // 更正写反的部分 d.Logger.Info("正在自动升级自定义文案文件") @@ -2362,8 +2271,8 @@ func (d *Dice) loads() { } // 1.3 版本 - if d.VersionCode != 0 && d.VersionCode < 10300 { - d.JsEnable = true + if config.VersionCode != 0 && config.VersionCode < 10300 { + config.JsEnable = DefaultConfig.JsEnable d.RunAfterLoaded = append(d.RunAfterLoaded, func() { // 更正写反的部分 @@ -2394,21 +2303,23 @@ func (d *Dice) loads() { d.SaveText() }) + d.Config = config + // 1.4.5 版本 - 覆写lagrange配置 - for _, i := range d.ImSession.EndPoints { - if i.ProtocolType == "onebot" { - pa := i.Adapter.(*PlatformAdapterGocq) - if pa.BuiltinMode == "lagrange" { - signServerUrl, signServerVersion := RWLagrangeSignServerUrl(d, i, "sealdice", false, "25765") - if signServerUrl != "" { - // 版本为空,覆写为 "25765" - if signServerVersion == "" { - RWLagrangeSignServerUrl(d, i, "sealdice", true, "25765") - } - } - } - } - } + // for _, i := range d.ImSession.EndPoints { + // if i.ProtocolType == "onebot" { + // pa := i.Adapter.(*PlatformAdapterGocq) + // if pa.BuiltinMode == "lagrange" { + // signServerUrl, signServerVersion := RWLagrangeSignServerUrl(d, i, "sealdice", false, "30366") + // if signServerUrl != "" { + // // 版本为空,覆写为 "30366" + // if signServerVersion == "" { + // RWLagrangeSignServerUrl(d, i, "sealdice", true, "30366") + // } + // } + // } + // } + // } // 设置全局群名缓存和用户名缓存 dm := d.Parent @@ -2422,34 +2333,12 @@ func (d *Dice) loads() { }) d.Logger.Info("serve.yaml loaded") } else { - // 这里是没有加载到配置文件,所以写默认设置项 - d.WorkInQQChannel = true - d.CustomReplyConfigEnable = false - d.AliveNoticeValue = "@every 3h" d.Logger.Info("serve.yaml not found") - - d.LogSizeNoticeCount = 500 - d.LogSizeNoticeEnable = true - - // 1.2 - d.QQEnablePoke = true - d.TextCmdTrustOnly = true - d.PlayerNameWrapEnable = true - d.DiceMasters = []string{"UI:1001"} - - // 1.3 - d.JsEnable = true - - // 1.4 - d.MaxExecuteTime = 12 - d.MaxCocCardGen = 5 - - d.QuitInactiveBatchSize = 10 - d.QuitInactiveBatchWait = 30 - - // 1.5 - d.VMVersionForDeck = "v2" - d.VMVersionForReply = "v1" + // 这里是没有加载到配置文件,所以写默认设置项 + d.DeckList = config.DeckList + d.CommandPrefix = config.CommandPrefix + d.DiceMasters = config.DiceMasters + d.Config = config } _ = model.BanItemList(d.DBData, func(id string, banUpdatedAt int64, data []byte) { @@ -2457,7 +2346,7 @@ func (d *Dice) loads() { err := json.Unmarshal(data, &v) if err == nil { v.BanUpdatedAt = banUpdatedAt - d.BanList.Map.Store(id, &v) + (&d.Config).BanList.Map.Store(id, &v) } }) @@ -2466,21 +2355,7 @@ func (d *Dice) loads() { i.AdapterSetup() } - if d.NoticeIDs == nil { - d.NoticeIDs = []string{} - } - - if len(d.CommandPrefix) == 0 { - d.CommandPrefix = []string{ - "!", - ".", - "。", - "/", - } - } - - d.VersionCode = 10300 // TODO: 记得修改!!! - d.LogWriter.LogLimit = d.UILogLimit + d.LogWriter.LogLimit = d.Config.UILogLimit // 设置扩展选项 d.ApplyExtDefaultSettings() @@ -2518,7 +2393,7 @@ func (d *Dice) loadAdvanced() { func (d *Dice) SaveText() { buf, err := yaml.Marshal(d.TextMapRaw) if err != nil { - fmt.Println(err) + log.Error("Dice.SaveText", err) } else { newFn := filepath.Join(d.BaseConfig.DataDir, "configs/text-template.yaml") bakFn := filepath.Join(d.BaseConfig.DataDir, "configs/text-template.yaml.bak") @@ -2536,7 +2411,7 @@ func (d *Dice) SaveText() { func (d *Dice) ApplyExtDefaultSettings() { // 遍历两个列表 exts1 := map[string]*ExtDefaultSettingItem{} - for _, i := range d.ExtDefaultSettings { + for _, i := range d.Config.ExtDefaultSettings { exts1[i.Name] = i } @@ -2549,7 +2424,7 @@ func (d *Dice) ApplyExtDefaultSettings() { for k, v := range exts2 { if _, exists := exts1[k]; !exists { item := &ExtDefaultSettingItem{Name: k, AutoActive: v.AutoActive, DisabledCommand: map[string]bool{}} - d.ExtDefaultSettings = append(d.ExtDefaultSettings, item) + (&d.Config).ExtDefaultSettings = append((&d.Config).ExtDefaultSettings, item) exts1[k] = item } } @@ -2597,7 +2472,24 @@ func (d *Dice) ApplyExtDefaultSettings() { func (d *Dice) Save(isAuto bool) { if d.LastUpdatedTime != 0 { - a, err1 := yaml.Marshal(d) + totalConf := &struct { + // copy from Dice + ImSession *IMSession `yaml:"imSession" jsbind:"imSession" json:"-"` + DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息 + CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导 + DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号 + + Config `yaml:",inline"` + }{ + // 这些都是由于导出到 goja 无法拆分的字段 + d.ImSession, + d.DeckList, + d.CommandPrefix, + d.DiceMasters, + + d.Config, + } + a, err1 := yaml.Marshal(totalConf) advancedData, err2 := yaml.Marshal(d.AdvancedConfig) if err1 == nil && err2 == nil { @@ -2605,7 +2497,7 @@ func (d *Dice) Save(isAuto bool) { err2 := os.WriteFile(filepath.Join(d.BaseConfig.DataDir, "advanced.yaml"), advancedData, 0o644) if err1 == nil && err2 == nil { now := time.Now() - d.LastSavedTime = &now + d.Config.LastSavedTime = &now if isAuto { d.Logger.Info("自动保存") } else { @@ -2613,11 +2505,11 @@ func (d *Dice) Save(isAuto bool) { } d.LastUpdatedTime = 0 } else if err1 != nil && err2 != nil { - d.Logger.Errorln("保存 serve.yaml 和 advanced.yaml 出错", err2) + d.Logger.Error("保存 serve.yaml 和 advanced.yaml 出错", err2) } else if err1 != nil { - d.Logger.Errorln("保存 serve.yaml 出错", err1) + d.Logger.Error("保存 serve.yaml 出错", err1) } else { - d.Logger.Errorln("保存 advanced.yaml 出错", err2) + d.Logger.Error("保存 advanced.yaml 出错", err2) } } } @@ -2629,7 +2521,12 @@ func (d *Dice) Save(isAuto bool) { if groupInfo.Players != nil { groupInfo.Players.Range(func(key string, value *GroupPlayerInfo) bool { if value.UpdatedAtTime != 0 { - _ = model.GroupPlayerInfoSave(d.DBData, groupInfo.GroupID, key, (*model.GroupPlayerInfoBase)(value)) + // 解离数据库层的操作到调用处,设置对应的信息 + now := int(time.Now().Unix()) + value.UserID = key + value.GroupID = groupInfo.GroupID + value.UpdatedAt = now // 更新当前时间为 UpdatedAt + _ = model.GroupPlayerInfoSave(d.DBData, (*model.GroupPlayerInfoBase)(value)) value.UpdatedAtTime = 0 } return true @@ -2654,7 +2551,7 @@ func (d *Dice) Save(isAuto bool) { // 保存黑名单数据 // TODO: 增加更新时间检测 - // model.BanMapSet(d.DBData, d.BanList.MapToJSON()) + // model.BanMapSet(d.DBData, d.Config.BanList.MapToJSON()) // endpoint数据额外更新到数据库 for _, ep := range d.ImSession.EndPoints { diff --git a/dice/dice.go b/dice/dice.go index 960040de..c098ae7a 100644 --- a/dice/dice.go +++ b/dice/dice.go @@ -14,20 +14,18 @@ import ( "github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/require" "github.com/go-creed/sat" - "github.com/jmoiron/sqlx" wr "github.com/mroth/weightedrand" "github.com/robfig/cron/v3" ds "github.com/sealdice/dicescript" "github.com/tidwall/buntdb" - "go.uber.org/zap" - rand2 "golang.org/x/exp/rand" "golang.org/x/exp/slices" - "golang.org/x/time/rate" + "gorm.io/gorm" - "sealdice-core/dice/censor" "sealdice-core/dice/logger" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" + "sealdice-core/utils/public_dice" ) type CmdExecuteResult struct { @@ -104,10 +102,10 @@ type ExtInfo struct { OnLoad func() `yaml:"-" json:"-" jsbind:"onLoad"` } -type DiceConfig struct { //nolint:revive - Name string `yaml:"name"` // 名称,默认为default - DataDir string `yaml:"dataDir"` // 数据路径,为./data/{name},例如data/default - IsLogPrint bool `yaml:"isLogPrint"` // 是否在控制台打印log +// RootConfig TODO:历史遗留问题,由于不输出DICE日志效果过差,已经抹除日志输出选项,剩余两个选项,私以为可以想办法也抹除掉。 +type RootConfig struct { //nolint:revive + Name string `yaml:"name"` // 名称,默认为default + DataDir string `yaml:"dataDir"` // 数据路径,为./data/{name},例如data/default } type ExtDefaultSettingItem struct { @@ -127,71 +125,29 @@ func (x ExtDefaultSettingItemSlice) Less(i, _ int) bool { return x[i].Name == "c func (x ExtDefaultSettingItemSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] } type Dice struct { - ImSession *IMSession `yaml:"imSession" jsbind:"imSession"` - CmdMap CmdMapCls `yaml:"-" json:"-"` - ExtList []*ExtInfo `yaml:"-"` - RollParser *DiceRollParser `yaml:"-"` - CommandCompatibleMode bool `yaml:"commandCompatibleMode"` - LastSavedTime *time.Time `yaml:"lastSavedTime"` - LastUpdatedTime int64 `yaml:"-"` - TextMap map[string]*wr.Chooser `yaml:"-"` - BaseConfig DiceConfig `yaml:"-"` - DBData *sqlx.DB `yaml:"-"` // 数据库对象 - DBLogs *sqlx.DB `yaml:"-"` // 数据库对象 - Logger *zap.SugaredLogger `yaml:"-"` // 日志 - LogWriter *logger.WriterX `yaml:"-"` // 用于api的log对象 - IsDeckLoading bool `yaml:"-"` // 正在加载中 - DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息 - CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导 - DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号 - NoticeIDs []string `yaml:"noticeIds"` // 通知ID - OnlyLogCommandInGroup bool `yaml:"onlyLogCommandInGroup"` // 日志中仅记录命令 - OnlyLogCommandInPrivate bool `yaml:"onlyLogCommandInPrivate"` // 日志中仅记录命令 - VersionCode int `json:"versionCode"` // 版本ID(配置文件) - MessageDelayRangeStart float64 `yaml:"messageDelayRangeStart"` // 指令延迟区间 - MessageDelayRangeEnd float64 `yaml:"messageDelayRangeEnd"` - WorkInQQChannel bool `yaml:"workInQQChannel"` - QQChannelAutoOn bool `yaml:"QQChannelAutoOn"` // QQ频道中自动开启(默认不开) - QQChannelLogMessage bool `yaml:"QQChannelLogMessage"` // QQ频道中记录消息(默认不开) - QQEnablePoke bool `yaml:"QQEnablePoke"` // 启用戳一戳 - TextCmdTrustOnly bool `yaml:"textCmdTrustOnly"` // 只允许信任用户或master使用text指令 - IgnoreUnaddressedBotCmd bool `yaml:"ignoreUnaddressedBotCmd"` // 不响应群聊裸bot指令 - UILogLimit int64 `yaml:"UILogLimit"` - FriendAddComment string `yaml:"friendAddComment"` // 加好友验证信息 - MasterUnlockCode string `yaml:"-"` // 解锁码,每20分钟变化一次,使用后立即变化 - MasterUnlockCodeTime int64 `yaml:"-"` - CustomReplyConfigEnable bool `yaml:"customReplyConfigEnable"` - CustomReplyConfig []*ReplyConfig `yaml:"-"` - RefuseGroupInvite bool `yaml:"refuseGroupInvite"` // 拒绝加入新群 - UpgradeWindowID string `yaml:"upgradeWindowId"` // 执行升级指令的窗口 - UpgradeEndpointID string `yaml:"upgradeEndpointId"` // 执行升级指令的端点 - BotExtFreeSwitch bool `yaml:"botExtFreeSwitch"` // 允许任意人员开关: 否则邀请者、群主、管理员、master有权限 - TrustOnlyMode bool `yaml:"trustOnlyMode"` // 只有信任的用户/master可以拉群和使用 - AliveNoticeEnable bool `yaml:"aliveNoticeEnable"` // 定时通知 - AliveNoticeValue string `yaml:"aliveNoticeValue"` // 定时通知间隔 - ReplyDebugMode bool `yaml:"replyDebugMode"` // 回复调试 - PlayerNameWrapEnable bool `yaml:"playerNameWrapEnable"` // 启用玩家名称外框 - - RateLimitEnabled bool `yaml:"rateLimitEnabled"` // 启用频率限制 (刷屏限制) - PersonalReplenishRateStr string `yaml:"personalReplenishRate"` // 个人刷屏警告速率,字符串格式 - PersonalReplenishRate rate.Limit `yaml:"-"` // 个人刷屏警告速率 - GroupReplenishRateStr string `yaml:"groupReplenishRate"` // 群组刷屏警告速率,字符串格式 - GroupReplenishRate rate.Limit `yaml:"-"` // 群组刷屏警告速率 - PersonalBurst int64 `yaml:"personalBurst"` // 个人自定义上限 - GroupBurst int64 `yaml:"groupBurst"` // 群组自定义上限 - - QuitInactiveThreshold time.Duration `yaml:"quitInactiveThreshold"` // 退出不活跃群组的时间阈值 - quitInactiveCronEntry cron.EntryID - QuitInactiveBatchSize int64 `yaml:"quitInactiveBatchSize"` // 退出不活跃群组的批量大小 - QuitInactiveBatchWait int64 `yaml:"quitInactiveBatchWait"` // 退出不活跃群组的批量等待时间(分) - - DefaultCocRuleIndex int64 `yaml:"defaultCocRuleIndex" jsbind:"defaultCocRuleIndex"` // 默认coc index - MaxExecuteTime int64 `yaml:"maxExecuteTime" jsbind:"maxExecuteTime"` // 最大骰点次数 - MaxCocCardGen int64 `yaml:"maxCocCardGen" jsbind:"maxCocCardGen"` // 最大coc制卡数 - - ExtDefaultSettings []*ExtDefaultSettingItem `yaml:"extDefaultSettings"` // 新群扩展按此顺序加载 - - BanList *BanListInfo `yaml:"banList"` // + // 由于被导出的原因,暂时不迁移至 config + ImSession *IMSession `yaml:"imSession" jsbind:"imSession" json:"-"` + + CmdMap CmdMapCls `yaml:"-" json:"-"` + ExtList []*ExtInfo `yaml:"-"` + RollParser *DiceRollParser `yaml:"-"` + LastUpdatedTime int64 `yaml:"-"` + TextMap map[string]*wr.Chooser `yaml:"-"` + BaseConfig BaseConfig `yaml:"-"` + DBData *gorm.DB `yaml:"-"` // 数据库对象 + DBLogs *gorm.DB `yaml:"-"` // 数据库对象 + Logger *log.Helper `yaml:"-"` // 日志 + LogWriter *log.WriterX `yaml:"-"` // 用于api的log对象 + IsDeckLoading bool `yaml:"-"` // 正在加载中 + + // 由于被导出的原因,暂时不迁移至 config + DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息 + CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导 + DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号 + + MasterUnlockCode string `yaml:"-" json:"masterUnlockCode"` // 解锁码,每20分钟变化一次,使用后立即变化 + MasterUnlockCodeTime int64 `yaml:"-" json:"masterUnlockCodeTime"` + CustomReplyConfig []*ReplyConfig `yaml:"-" json:"-"` TextMapRaw TextTemplateWithWeightDict `yaml:"-"` TextMapHelpInfo TextTemplateWithHelpDict `yaml:"-"` @@ -200,20 +156,18 @@ type Dice struct { ConfigManager *ConfigManager `yaml:"-"` Parent *DiceManager `yaml:"-"` - CocExtraRules map[int]*CocRuleInfo `yaml:"-" json:"cocExtraRules"` - Cron *cron.Cron `yaml:"-" json:"-"` - AliveNoticeEntry cron.EntryID `yaml:"-" json:"-"` - JsEnable bool `yaml:"jsEnable" json:"jsEnable"` - DisabledJsScripts map[string]bool `yaml:"disabledJsScripts" json:"disabledJsScripts"` // 作为set - JsPrinter *PrinterFunc `yaml:"-" json:"-"` - JsRequire *require.RequireModule `yaml:"-" json:"-"` - JsLoop *eventloop.EventLoop `yaml:"-" json:"-"` - JsScriptList []*JsScriptInfo `yaml:"-" json:"-"` - JsScriptCron *cron.Cron `yaml:"-" json:"-"` - JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"` + CocExtraRules map[int]*CocRuleInfo `yaml:"-" json:"cocExtraRules"` + Cron *cron.Cron `yaml:"-" json:"-"` + AliveNoticeEntry cron.EntryID `yaml:"-" json:"-"` + JsPrinter *PrinterFunc `yaml:"-" json:"-"` + JsRequire *require.RequireModule `yaml:"-" json:"-"` + + JsLoop *eventloop.EventLoop `yaml:"-" json:"-"` + JsScriptList []*JsScriptInfo `yaml:"-" json:"-"` + JsScriptCron *cron.Cron `yaml:"-" json:"-"` + JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"` // 重载使用的互斥锁 JsReloadLock sync.Mutex `yaml:"-" json:"-"` - // 内置脚本摘要表,用于判断内置脚本是否有更新 JsBuiltinDigestSet map[string]bool `yaml:"-" json:"-"` // 当前在加载的脚本路径,用于关联 jsScriptInfo 和 ExtInfo @@ -224,75 +178,26 @@ type Dice struct { RunAfterLoaded []func() `yaml:"-" json:"-"` - LogSizeNoticeEnable bool `yaml:"logSizeNoticeEnable"` // 开启日志数量提示 - LogSizeNoticeCount int `yaml:"LogSizeNoticeCount"` // 日志数量提示阈值,默认500 - - IsAlreadyLoadConfig bool `yaml:"-"` // 如果在loads前崩溃,那么不写入配置,防止覆盖为空的 deckCommandItemsList DeckCommandListItems // 牌堆key信息,辅助作为模糊搜索使用 UIEndpoint *EndPointInfo `yaml:"-" json:"-"` // UI Endpoint - MailEnable bool `json:"mailEnable" yaml:"mailEnable"` // 是否启用 - MailFrom string `json:"mailFrom" yaml:"mailFrom"` // 邮箱来源 - MailPassword string `json:"mailPassword" yaml:"mailPassword"` // 邮箱密钥/密码 - MailSMTP string `json:"mailSmtp" yaml:"mailSmtp"` // 邮箱 smtp 地址 - - NewsMark string `json:"newsMark" yaml:"newsMark"` // 已读新闻的md5 - - EnableCensor bool `json:"enableCensor" yaml:"enableCensor"` // 启用敏感词审查 - CensorManager *CensorManager `json:"-" yaml:"-"` - CensorMode CensorMode `json:"censorMode" yaml:"censorMode"` - CensorThresholds map[censor.Level]int `json:"censorThresholds" yaml:"censorThresholds"` // 敏感词阈值 - CensorHandlers map[censor.Level]uint8 `json:"censorHandlers" yaml:"censorHandlers"` - CensorScores map[censor.Level]int `json:"censorScores" yaml:"censorScores"` // 敏感词怒气值 - CensorCaseSensitive bool `json:"censorCaseSensitive" yaml:"censorCaseSensitive"` // 敏感词大小写敏感 - CensorMatchPinyin bool `json:"censorMatchPinyin" yaml:"censorMatchPinyin"` // 敏感词匹配拼音 - CensorFilterRegexStr string `json:"censorFilterRegexStr" yaml:"censorFilterRegexStr"` // 敏感词过滤字符正则 - - VMVersionForReply string `json:"VMVersionForReply" yaml:"VMVersionForReply"` // 自定义回复使用的vm版本 - VMVersionForDeck string `json:"VMVersionForDeck" yaml:"VMVersionForDeck"` // 牌堆使用的vm版本 + CensorManager *CensorManager `json:"-" yaml:"-"` AttrsManager *AttrsManager `json:"-" yaml:"-"` - AdvancedConfig AdvancedConfig `json:"-" yaml:"-"` - - ContainerMode bool `yaml:"-" json:"-"` // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq -} + Config Config `json:"-" yaml:"-"` -type CensorMode int + AdvancedConfig AdvancedConfig `json:"-" yaml:"-"` -const ( - OnlyOutputReply CensorMode = iota - OnlyInputCommand - AllInput -) + PublicDice *public_dice.PublicDiceClient `json:"-" yaml:"-"` + PublicDiceTimerId cron.EntryID `json:"-" yaml:"-"` -const ( - // SendWarning 发送警告 - SendWarning CensorHandler = iota - // SendNotice 向通知列表/邮件发送通知 - SendNotice - // BanUser 拉黑用户 - BanUser - // BanGroup 拉黑群 - BanGroup - // BanInviter 拉黑邀请人 - BanInviter - // AddScore 增加怒气值 - AddScore -) + ContainerMode bool `yaml:"-" json:"-"` // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq -var CensorHandlerText = map[CensorHandler]string{ - SendWarning: "SendWarning", - SendNotice: "SendNotice", - BanUser: "BanUser", - BanGroup: "BanGroup", - BanInviter: "BanInviter", - AddScore: "AddScore", + IsAlreadyLoadConfig bool `yaml:"-"` // 如果在loads前崩溃,那么不写入配置,防止覆盖为空的 } -type CensorHandler int - func (d *Dice) MarkModified() { d.LastUpdatedTime = time.Now().Unix() } @@ -314,7 +219,7 @@ func (d *Dice) Init() { _ = os.MkdirAll(filepath.Join(d.BaseConfig.DataDir, "extra"), 0o755) _ = os.MkdirAll(filepath.Join(d.BaseConfig.DataDir, "scripts"), 0o755) - log := logger.Init(filepath.Join(d.BaseConfig.DataDir, "record.log"), d.BaseConfig.Name, d.BaseConfig.IsLogPrint) + log := logger.Init() d.Logger = log.Logger d.LogWriter = log.WX @@ -324,21 +229,20 @@ func (d *Dice) Init() { d.CocExtraRules = map[int]*CocRuleInfo{} var err error - d.DBData, d.DBLogs, err = model.SQLiteDBInit(d.BaseConfig.DataDir) + d.DBData, d.DBLogs, err = model.DatabaseInit() if err != nil { - fmt.Println(err) d.Logger.Errorf("Failed to init database: %v", err) } d.AttrsManager = &AttrsManager{} d.AttrsManager.Init(d) - d.BanList = &BanListInfo{Parent: d} - d.BanList.Init() + (&d.Config).BanList = &BanListInfo{Parent: d} + (&d.Config).BanList.Init() initVerify() - d.CommandCompatibleMode = true + d.BaseConfig.CommandCompatibleMode = true // Pinenutn: 预先初始化对应的SyncMap d.ImSession = &IMSession{} d.ImSession.Parent = d @@ -355,16 +259,18 @@ func (d *Dice) Init() { d.RegisterBuiltinExt() d.loads() d.loadAdvanced() - d.BanList.Loads() - d.BanList.AfterLoads() + (&d.Config).BanList.Loads() + (&d.Config).BanList.AfterLoads() d.IsAlreadyLoadConfig = true - if d.EnableCensor { + if d.Config.EnableCensor { d.NewCensorManager() } + go d.PublicDiceSetup() + // 创建js运行时 - if d.JsEnable { + if d.Config.JsEnable { d.Logger.Info("js扩展支持:开启") d.JsInit() } else { @@ -456,20 +362,20 @@ func (d *Dice) Init() { go refreshGroupInfo() d.ApplyAliveNotice() - if d.JsEnable { + if d.Config.JsEnable { d.JsBuiltinDigestSet = make(map[string]bool) d.JsLoadScripts() } else { d.Logger.Info("js扩展支持已关闭,跳过js脚本的加载") } - if d.UpgradeWindowID != "" { + if d.Config.UpgradeWindowID != "" { go func() { defer ErrorLogAndContinue(d) var ep *EndPointInfo for _, _ep := range d.ImSession.EndPoints { - if _ep.ID == d.UpgradeEndpointID { + if _ep.ID == d.Config.UpgradeEndpointID { ep = _ep break } @@ -491,16 +397,16 @@ func (d *Dice) Init() { // 可以了,发送消息 ctx := &MsgContext{Dice: d, EndPoint: ep, Session: d.ImSession} - isGroup := strings.Contains(d.UpgradeWindowID, "-Group:") + isGroup := strings.Contains(d.Config.UpgradeWindowID, "-Group:") if isGroup { - ReplyGroup(ctx, &Message{GroupID: d.UpgradeWindowID}, text) + ReplyGroup(ctx, &Message{GroupID: d.Config.UpgradeWindowID}, text) } else { - ReplyPerson(ctx, &Message{Sender: SenderBase{UserID: d.UpgradeWindowID}}, text) + ReplyPerson(ctx, &Message{Sender: SenderBase{UserID: d.Config.UpgradeWindowID}}, text) } d.Logger.Infof("升级完成,当前版本: %s", VERSION.String()) - d.UpgradeWindowID = "" - d.UpgradeEndpointID = "" + (&d.Config).UpgradeWindowID = "" + (&d.Config).UpgradeEndpointID = "" d.MarkModified() d.Save(false) break @@ -567,7 +473,7 @@ func (d *Dice) _ExprTextBaseV1(buffer string, ctx *MsgContext, flags RollExtraFl // 隐藏的内置字符串符号 \x1e val, detail, err := d._ExprEvalBaseV1("\x1e"+buffer+"\x1e", ctx, flags) if err != nil { - fmt.Println("脚本执行出错: ", buffer, "->", err) + log.Warnf("脚本执行出错: %s -> %v", buffer, err) } if err == nil && (val.TypeID == VMTypeString || val.TypeID == VMTypeNone) { @@ -710,8 +616,8 @@ func (d *Dice) ApplyAliveNotice() { if d.Cron != nil && d.AliveNoticeEntry != 0 { d.Cron.Remove(d.AliveNoticeEntry) } - if d.AliveNoticeEnable { - entry, err := d.Cron.AddFunc(d.AliveNoticeValue, func() { + if d.Config.AliveNoticeEnable { + entry, err := d.Cron.AddFunc((&d.Config).AliveNoticeValue, func() { d.NoticeForEveryEndpoint(fmt.Sprintf("存活, D100=%d", DiceRoll64(100)), false) }) if err == nil { @@ -723,9 +629,10 @@ func (d *Dice) ApplyAliveNotice() { } } -// GameSystemTemplateAdd 应用一个角色模板 -func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool { - if _, exists := d.GameSystemMap.Load(tmpl.Name); !exists { +// GameSystemTemplateAddEx 应用一个角色模板 +func (d *Dice) GameSystemTemplateAddEx(tmpl *GameSystemTemplate, overwrite bool) bool { + _, exists := d.GameSystemMap.Load(tmpl.Name) + if !exists || overwrite { d.GameSystemMap.Store(tmpl.Name, tmpl) // sn 从这里读取 // set 时从这里读取对应System名字的模板 @@ -744,6 +651,11 @@ func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool { return false } +// GameSystemTemplateAdd 应用一个角色模板,当已存在时返回false +func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool { + return d.GameSystemTemplateAddEx(tmpl, false) +} + // var randSource = rand2.NewSource(uint64(time.Now().Unix())) var randSource = &rand2.PCGSource{} @@ -784,25 +696,124 @@ func ErrorLogAndContinue(d *Dice) { } var chsS2T = sat.DefaultDict() +var taskId cron.EntryID +var quitMutex sync.Mutex func (d *Dice) ResetQuitInactiveCron() { + // TODO: 这里加锁是否有必要? + quitMutex.Lock() + defer quitMutex.Unlock() dm := d.Parent - if d.quitInactiveCronEntry > 0 { - dm.Cron.Remove(d.quitInactiveCronEntry) - d.quitInactiveCronEntry = 0 - } - - if d.QuitInactiveThreshold > 0 { - var err error - d.quitInactiveCronEntry, err = dm.Cron.AddFunc("0 4 * * *", func() { - thr := time.Now().Add(-d.QuitInactiveThreshold) - hint := thr.Add(d.QuitInactiveThreshold / 10) // 进入退出判定线的9/10开始提醒 - d.ImSession.LongTimeQuitInactiveGroup(thr, hint, - int(d.QuitInactiveBatchWait), - int(d.QuitInactiveBatchSize)) + if d.Config.quitInactiveCronEntry > 0 { + dm.Cron.Remove(d.Config.quitInactiveCronEntry) + (&d.Config).quitInactiveCronEntry = DefaultConfig.quitInactiveCronEntry + } + // 如果退群功能开启,那么设定退群的Cron + if d.Config.QuitInactiveThreshold > 0 { + duration := time.Duration(d.Config.QuitInactiveBatchWait) * time.Minute + // 每隔上面的退群时间,执行一次函数 + if taskId != 0 { + dm.Cron.Remove(taskId) + } + taskId = dm.Cron.Schedule(cron.Every(duration), cron.FuncJob(func() { + thr := time.Now().Add(-d.Config.QuitInactiveThreshold) + // 进入退出判定线的9/10开始提醒, 但是目前来看,原版退群只有一个提示,提示会被大量刷屏然后消失不见。同时并没有告知对应的群 + // 或许也不应该告知对应的群,因为群可能被解散了,大量告知容易出问题? + // hint := thr.Add(d.Config.QuitInactiveThreshold / 10) + d.ImSession.LongTimeQuitInactiveGroupReborn(thr, int(d.Config.QuitInactiveBatchSize)) + })) + d.Logger.Infof("退群功能已启动,每 %s 执行一次退群判定", duration.String()) + } +} + +func (d *Dice) PublicDiceEndpointRefresh() { + cfg := &d.Config.PublicDiceConfig + + var endpointItems []*public_dice.Endpoint + for _, i := range d.ImSession.EndPoints { + if !i.IsPublic { + continue + } + endpointItems = append(endpointItems, &public_dice.Endpoint{ + Platform: i.Platform, + UID: i.UserID, + IsOnline: i.State == 1, }) - if err != nil { - d.Logger.Errorf("创建自动清理群聊cron任务失败: %v", err) + } + + _, code := d.PublicDice.EndpointUpdate(&public_dice.EndpointUpdateRequest{ + DiceID: cfg.ID, + Endpoints: endpointItems, + }, GenerateVerificationKeyForPublicDice) + if code != 200 { + log.Warn("[公骰]无法通过服务器校验,不再进行更新") + return + } +} + +func (d *Dice) PublicDiceInfoRegister() { + cfg := &d.Config.PublicDiceConfig + pd, code := d.PublicDice.Register(&public_dice.RegisterRequest{ + ID: cfg.ID, + Name: cfg.Name, + Brief: cfg.Brief, + Note: cfg.Note, + }, GenerateVerificationKeyForPublicDice) + if code != 200 { + log.Warn("[公骰]无法通过服务器校验,不再进行骰号注册") + return + } + // ID为空时才将注册好的ID覆写配置 + if pd.Item.ID != "" && cfg.ID == "" { + cfg.ID = pd.Item.ID + } +} + +func (d *Dice) PublicDiceSetupTick() { + cfg := &d.Config.PublicDiceConfig + + doTickUpdate := func() { + if !cfg.Enable { + d.Cron.Remove(d.PublicDiceTimerId) + return + } + var tickEndpointItems []*public_dice.TickEndpoint + for _, i := range d.ImSession.EndPoints { + if !i.IsPublic { + continue + } + tickEndpointItems = append(tickEndpointItems, &public_dice.TickEndpoint{ + UID: i.UserID, + IsOnline: i.State == 1, + }) } + d.PublicDice.TickUpdate(&public_dice.TickUpdateRequest{ + ID: cfg.ID, + Endpoints: tickEndpointItems, + }, GenerateVerificationKeyForPublicDice) + } + + if d.PublicDiceTimerId != 0 { + d.Cron.Remove(d.PublicDiceTimerId) + } + + go func() { + // 20s后进行第一次调用,此后3min进行一次更新 + time.Sleep(20 * time.Second) + doTickUpdate() + }() + + d.PublicDiceTimerId, _ = d.Cron.AddFunc("@every 3m", doTickUpdate) +} + +func (d *Dice) PublicDiceSetup() { + d.PublicDice = public_dice.NewClient("https://dice.weizaima.com", "") + + cfg := &d.Config.PublicDiceConfig + if !cfg.Enable { + return } + d.PublicDiceInfoRegister() + d.PublicDiceEndpointRefresh() + d.PublicDiceSetupTick() } diff --git a/dice/dice_attrs_manager.go b/dice/dice_attrs_manager.go index ba7e4019..85938d43 100644 --- a/dice/dice_attrs_manager.go +++ b/dice/dice_attrs_manager.go @@ -1,24 +1,30 @@ package dice import ( - "database/sql" + "context" "errors" "fmt" "time" - "github.com/jmoiron/sqlx" ds "github.com/sealdice/dicescript" - "go.uber.org/zap" + "gorm.io/gorm" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" ) type AttrsManager struct { - db *sqlx.DB - logger *zap.SugaredLogger + db *gorm.DB + logger *log.Helper + cancel context.CancelFunc m SyncMap[string, *AttributesItem] } +func (am *AttrsManager) Stop() { + log.Info("结束数据库保存程序...") + am.cancel() +} + // LoadByCtx 获取当前角色,如有绑定,则获取绑定的角色,若无绑定,获取群内默认卡 func (am *AttrsManager) LoadByCtx(ctx *MsgContext) (*AttributesItem, error) { return am.Load(ctx.Group.GroupID, ctx.Player.UserID) @@ -149,14 +155,25 @@ func (am *AttrsManager) LoadById(id string) (*AttributesItem, error) { func (am *AttrsManager) Init(d *Dice) { am.db = d.DBData am.logger = d.Logger + // 创建一个 context 用于取消 goroutine + ctx, cancel := context.WithCancel(context.Background()) + // 确保程序退出时取消上下文 go func() { // NOTE(Xiangze Li): 这种不退出的goroutine不利于平稳结束程序 for { - am.CheckForSave() - am.CheckAndFreeUnused() - time.Sleep(15 * time.Second) + select { + case <-ctx.Done(): + // 检测到取消信号后退出循环 + return + default: + // 正常工作 + am.CheckForSave() + am.CheckAndFreeUnused() + time.Sleep(15 * time.Second) + } } }() + am.cancel = cancel } func (am *AttrsManager) CheckForSave() (int, int) { @@ -169,24 +186,18 @@ func (am *AttrsManager) CheckForSave() (int, int) { return 0, 0 } - tx, err := db.Begin() - if err != nil { - if am.logger != nil { - am.logger.Errorf("定期写入用户数据出错(创建事务): %v", err) - } - return 0, 0 - } + tx := db.Begin() am.m.Range(func(key string, value *AttributesItem) bool { if !value.IsSaved { saved += 1 - value.SaveToDB(db, tx) + value.SaveToDB(tx) } times += 1 return true }) - err = tx.Commit() + err := tx.Commit().Error if err != nil { if am.logger != nil { am.logger.Errorf("定期写入用户数据出错(提交事务): %v", err) @@ -207,14 +218,22 @@ func (am *AttrsManager) CheckAndFreeUnused() { prepareToFree := map[string]int{} currentTime := time.Now().Unix() + tx := db.Begin() am.m.Range(func(key string, value *AttributesItem) bool { if value.LastUsedTime-currentTime > 60*10 { prepareToFree[key] = 1 - value.SaveToDB(am.db, nil) + // 直接保存 + value.SaveToDB(tx) } return true }) - + err := tx.Commit().Error + if err != nil { + if am.logger != nil { + am.logger.Errorf("定期清理无用用户数据出错(提交事务): %v", err) + } + _ = tx.Rollback() + } for key := range prepareToFree { am.m.Delete(key) } @@ -279,15 +298,15 @@ type AttributesItem struct { SheetType string } -func (i *AttributesItem) SaveToDB(db *sqlx.DB, tx *sql.Tx) { +func (i *AttributesItem) SaveToDB(db *gorm.DB) { // 使用事务写入 rawData, err := ds.NewDictVal(i.valueMap).V().ToJSON() if err != nil { return } - err = model.AttrsPutById(db, tx, i.ID, rawData, i.Name, i.SheetType) + err = model.AttrsPutById(db, i.ID, rawData, i.Name, i.SheetType) if err != nil { - fmt.Println("保存数据失败", err.Error()) + log.Error("保存数据失败", err.Error()) return } i.IsSaved = true diff --git a/dice/dice_backup.go b/dice/dice_backup.go index 8ce8c138..51e78846 100644 --- a/dice/dice_backup.go +++ b/dice/dice_backup.go @@ -2,7 +2,9 @@ package dice import ( "encoding/json" + "errors" "fmt" + "io" "io/fs" "os" "path/filepath" @@ -16,6 +18,7 @@ import ( "sealdice-core/dice/model" "sealdice-core/utils" "sealdice-core/utils/crypto" + log "sealdice-core/utils/kratos" ) const BackupDir = "./backups" @@ -123,7 +126,7 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error } backup := func(d *Dice, fn string) { - data, err := os.ReadFile(fn) + file, err := os.Open(fn) if err != nil && !strings.Contains(fn, "session.token") { if d != nil { d.Logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error()) @@ -132,6 +135,7 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error } return } + defer file.Close() h := &zip.FileHeader{Name: fn, Method: zip.Deflate, Flags: 0x800} fileWriter, err := writer.CreateHeader(h) @@ -143,7 +147,15 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error } return } - _, _ = fileWriter.Write(data) + + _, err = io.Copy(fileWriter, file) + if err != nil { + if d != nil { + d.Logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error()) + } else { + logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error()) + } + } } backupDir := func(path string, info fs.FileInfo, _ error) error { @@ -226,20 +238,20 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error err := model.FlushWAL(d.DBData) if err != nil { - d.Logger.Warnln("备份时data数据库flush出错", err.Error()) + d.Logger.Errorf("备份时data数据库flush出错 错误为:%v", err.Error()) } else { backup(d, filepath.Join(dataDir, "data.db")) } err = model.FlushWAL(d.DBLogs) if err != nil { - d.Logger.Warnln("备份时logs数据库flush出错", err.Error()) + d.Logger.Errorf("备份时logs数据库flush出错 错误为:%v", err.Error()) } else { backup(d, filepath.Join(dataDir, "data-logs.db")) } if d.CensorManager != nil && d.CensorManager.DB != nil { err = model.FlushWAL(d.CensorManager.DB) if err != nil { - d.Logger.Warnln("备份时censor数据库flush出错", err.Error()) + d.Logger.Errorf("备份时censor数据库flush出错 %v", err.Error()) } else { backup(d, filepath.Join(dataDir, "data-censor.db")) } @@ -340,7 +352,7 @@ func (dm *DiceManager) BackupClean(fromAuto bool) (err error) { return nil } - // fmt.Println("开始定时清理备份", fromAuto) + log.Info("开始清理备份文件") backupDir, err := os.Open(BackupDir) if err != nil { @@ -369,31 +381,42 @@ func (dm *DiceManager) BackupClean(fromAuto bool) (err error) { sort.Sort(utils.ByModtime(fileInfos)) var fileInfoOld []os.FileInfo + + logMsg := strings.Builder{} + logMsg.WriteString(fmt.Sprintf("现有备份文件 %d 个, 清理模式为 ", len(fileInfos))) + switch dm.BackupCleanStrategy { case BackupCleanStrategyByCount: + logMsg.WriteString(fmt.Sprintf("保留一定数量(%d)", dm.BackupCleanKeepCount)) if len(fileInfos) > dm.BackupCleanKeepCount { fileInfoOld = fileInfos[:len(fileInfos)-dm.BackupCleanKeepCount] } case BackupCleanStrategyByTime: threshold := time.Now().Add(-dm.BackupCleanKeepDur) + logMsg.WriteString(fmt.Sprintf("保留一定时间(%v, %s)", dm.BackupCleanKeepDur, threshold.Format(time.DateTime))) idx, _ := sort.Find(len(fileInfos), func(i int) int { return threshold.Compare(fileInfos[i].ModTime()) }) - fileInfoOld = fileInfos[:idx+1] + fileInfoOld = fileInfos[:idx] default: // no-op } + logMsg.WriteString(fmt.Sprintf(", 有以下 %d 个将要被删除", len(fileInfoOld))) + errDel := []string{} - for _, fi := range fileInfoOld { + for i, fi := range fileInfoOld { + logMsg.WriteString(fmt.Sprintf("\n%d. %s", i+1, fi.Name())) errDelete := os.Remove(filepath.Join(BackupDir, fi.Name())) if errDelete != nil { errDel = append(errDel, errDelete.Error()) } } + log.Info(logMsg.String()) + if len(errDel) > 0 { - return fmt.Errorf("error(s) occured when deleting files:\n" + strings.Join(errDel, "\n")) + return errors.New("error(s) occured when deleting files:\n" + strings.Join(errDel, "\n")) } return nil } diff --git a/dice/dice_ban.go b/dice/dice_ban.go index aeb0095e..89068239 100644 --- a/dice/dice_ban.go +++ b/dice/dice_ban.go @@ -53,16 +53,17 @@ func (i *BanListInfoItem) toText(_ *Dice) string { } type BanListInfo struct { - Parent *Dice `yaml:"-" json:"-"` - Map *SyncMap[string, *BanListInfoItem] `yaml:"-" json:"-"` - BanBehaviorRefuseReply bool `yaml:"banBehaviorRefuseReply" json:"banBehaviorRefuseReply"` // 拉黑行为: 拒绝回复 - BanBehaviorRefuseInvite bool `yaml:"banBehaviorRefuseInvite" json:"banBehaviorRefuseInvite"` // 拉黑行为: 拒绝邀请 - BanBehaviorQuitLastPlace bool `yaml:"banBehaviorQuitLastPlace" json:"banBehaviorQuitLastPlace"` // 拉黑行为: 退出事发群 - BanBehaviorQuitPlaceImmediately bool `yaml:"banBehaviorQuitPlaceImmediately" json:"banBehaviorQuitPlaceImmediately"` // 拉黑行为: 使用时立即退出群 - BanBehaviorQuitIfAdmin bool `yaml:"banBehaviorQuitIfAdmin" json:"banBehaviorQuitIfAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则发出警告信息 - ThresholdWarn int64 `yaml:"thresholdWarn" json:"thresholdWarn"` // 警告阈值 - ThresholdBan int64 `yaml:"thresholdBan" json:"thresholdBan"` // 错误阈值 - AutoBanMinutes int64 `yaml:"autoBanMinutes" json:"autoBanMinutes"` // 自动禁止时长 + Parent *Dice `yaml:"-" json:"-"` + Map *SyncMap[string, *BanListInfoItem] `yaml:"-" json:"-"` + BanBehaviorRefuseReply bool `yaml:"banBehaviorRefuseReply" json:"banBehaviorRefuseReply"` // 拉黑行为: 拒绝回复 + BanBehaviorRefuseInvite bool `yaml:"banBehaviorRefuseInvite" json:"banBehaviorRefuseInvite"` // 拉黑行为: 拒绝邀请 + BanBehaviorQuitLastPlace bool `yaml:"banBehaviorQuitLastPlace" json:"banBehaviorQuitLastPlace"` // 拉黑行为: 退出事发群 + BanBehaviorQuitPlaceImmediately bool `yaml:"banBehaviorQuitPlaceImmediately" json:"banBehaviorQuitPlaceImmediately"` // 拉黑行为: 使用时立即退出群 + BanBehaviorQuitIfAdmin bool `yaml:"banBehaviorQuitIfAdmin" json:"banBehaviorQuitIfAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则发出警告信息 + BanBehaviorQuitIfAdminSilentIfNotAdmin bool `yaml:"banBehaviorQuitIfAdminSilentIfNotAdmin" json:"banBehaviorQuitIfAdminSilentIfNotAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则仅拒绝回复 + ThresholdWarn int64 `yaml:"thresholdWarn" json:"thresholdWarn"` // 警告阈值 + ThresholdBan int64 `yaml:"thresholdBan" json:"thresholdBan"` // 错误阈值 + AutoBanMinutes int64 `yaml:"autoBanMinutes" json:"autoBanMinutes"` // 自动禁止时长 ScoreReducePerMinute int64 `yaml:"scoreReducePerMinute" json:"scoreReducePerMinute"` // 每分钟下降 ScoreGroupMuted int64 `yaml:"scoreGroupMuted" json:"scoreGroupMuted"` // 群组禁言 @@ -105,7 +106,7 @@ func (i *BanListInfo) AfterLoads() { return } var toDelete []string - d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool { + (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool { if v.Rank == BanRankNormal || v.Rank == BanRankWarn { v.Score -= i.ScoreReducePerMinute if v.Score <= 0 { @@ -121,7 +122,7 @@ func (i *BanListInfo) AfterLoads() { _ = model.BanItemDel(d.DBData, j) } - d.BanList.SaveChanged(d) + (&d.Config).BanList.SaveChanged(d) }) } @@ -191,6 +192,7 @@ func (i *BanListInfo) AddScoreBase(uid string, score int64, place string, reason // 警告: XXX 因为等行为,进入警告列表 // 黑名单: XXX 因为等行为,进入黑名单。将作出以下惩罚:拒绝回复、拒绝邀请、退出事发群 // TODO + //nolint:forbidigo // that is a todo fmt.Println("TODO Alert") } @@ -281,7 +283,7 @@ func (i *BanListInfo) NoticeCheck(uid string, place string, oldRank BanRankType, if ctx != nil { var isWhiteGroup bool d := ctx.Dice - value, exists := d.BanList.Map.Load(place) + value, exists := (&d.Config).BanList.Map.Load(place) if exists { if value.Rank == BanRankTrusted { isWhiteGroup = true @@ -403,7 +405,7 @@ func (d *Dice) GetBanList() []*BanListInfoItem { } func (i *BanListInfo) SaveChanged(d *Dice) { - d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool { + (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool { if v.UpdatedAt != 0 { data, err := json.Marshal(v) if err == nil { diff --git a/dice/dice_censor.go b/dice/dice_censor.go index eff905d5..55f49194 100644 --- a/dice/dice_censor.go +++ b/dice/dice_censor.go @@ -1,6 +1,7 @@ package dice import ( + "errors" "fmt" "io/fs" "os" @@ -8,43 +9,78 @@ import ( "sort" "strings" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" "sealdice-core/dice/censor" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" ) +type CensorMode int + +const ( + OnlyOutputReply CensorMode = iota + OnlyInputCommand + AllInput +) + +const ( + // SendWarning 发送警告 + SendWarning CensorHandler = iota + // SendNotice 向通知列表/邮件发送通知 + SendNotice + // BanUser 拉黑用户 + BanUser + // BanGroup 拉黑群 + BanGroup + // BanInviter 拉黑邀请人 + BanInviter + // AddScore 增加怒气值 + AddScore +) + +var CensorHandlerText = map[CensorHandler]string{ + SendWarning: "SendWarning", + SendNotice: "SendNotice", + BanUser: "BanUser", + BanGroup: "BanGroup", + BanInviter: "BanInviter", + AddScore: "AddScore", +} + +type CensorHandler int + type CensorManager struct { IsLoading bool Parent *Dice Censor *censor.Censor - DB *sqlx.DB + DB *gorm.DB SensitiveWordsFiles map[string]*censor.WordFile } func (d *Dice) NewCensorManager() { - db, err := model.SQLiteCensorDBInit(d.BaseConfig.DataDir) + db, err := model.CensorDBInit() if err != nil { panic(err) } cm := CensorManager{ Censor: &censor.Censor{ - CaseSensitive: d.CensorCaseSensitive, - MatchPinyin: d.CensorMatchPinyin, - FilterRegexStr: d.CensorFilterRegexStr, + CaseSensitive: d.Config.CensorCaseSensitive, + MatchPinyin: d.Config.CensorMatchPinyin, + FilterRegexStr: d.Config.CensorFilterRegexStr, }, DB: db, } cm.Parent = d d.CensorManager = &cm - if d.CensorThresholds == nil { - d.CensorThresholds = make(map[censor.Level]int) + if d.Config.CensorThresholds == nil { + (&d.Config).CensorThresholds = make(map[censor.Level]int) } - if d.CensorHandlers == nil { - d.CensorHandlers = make(map[censor.Level]uint8) + if d.Config.CensorHandlers == nil { + (&d.Config).CensorHandlers = make(map[censor.Level]uint8) } - if d.CensorScores == nil { - d.CensorScores = make(map[censor.Level]int) + if d.Config.CensorScores == nil { + (&d.Config).CensorScores = make(map[censor.Level]int) } cm.Load(d) } @@ -60,7 +96,7 @@ func (cm *CensorManager) Load(_ *Dice) { cm.Parent.Logger.Infof("正在读取敏感词文件:%s\n", path) fileInfo, e := cm.Censor.PreloadFile(path) if e != nil { - fmt.Printf("censor: unable to read %s, %s\n", path, e.Error()) + log.Errorf("censor: unable to read %s, %v", path, e) } if cm.SensitiveWordsFiles == nil { cm.SensitiveWordsFiles = make(map[string]*censor.WordFile) @@ -71,14 +107,14 @@ func (cm *CensorManager) Load(_ *Dice) { }) err := cm.Censor.Load() if err != nil { - fmt.Printf("censor: load fail, %s\n", err.Error()) + log.Errorf("censor: load fail, %v", err) } cm.IsLoading = false } func (cm *CensorManager) Check(ctx *MsgContext, msg *Message, checkContent string) (*MsgCheckResult, error) { if cm.IsLoading { - return nil, fmt.Errorf("censor is loading") + return nil, errors.New("censor is loading") } res := cm.Censor.Check(checkContent) if !ctx.Censored && res.HighestLevel > censor.Ignore { @@ -137,7 +173,7 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se if !ok { d.Logger.Warn("Dice CenSor获取GroupInfo失败") } - thresholds := d.CensorThresholds + thresholds := d.Config.CensorThresholds // 保证按程度依次降低来处理 var tempLevels censor.Levels @@ -155,7 +191,7 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se // 清空此用户该等级计数 model.CensorClearLevelCount(d.CensorManager.DB, msg.Sender.UserID, level) // 该等级敏感词超过阈值,执行操作 - handler := d.CensorHandlers[level] + handler := d.Config.CensorHandlers[level] levelText := censor.LevelText[level] if handler&(1<敏感词", mctx, @@ -195,9 +231,9 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se if handler&(1<敏感词", mctx, @@ -207,9 +243,9 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se if handler&(1<敏感词", mctx, @@ -217,13 +253,13 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se } } if handler&(1< 0 { key += "/" + row[1+j] } } content := row[synonymCount+1] - _ = m.AddItem(HelpTextItem{ + _ = m.AddItem(docengine.HelpTextItem{ Group: group, From: path, Title: key, @@ -370,7 +358,7 @@ func (m *HelpManager) loadHelpDoc(group string, path string) bool { // Close the spreadsheet. if err := f.Close(); err != nil { - fmt.Println(err) + log.Error("HelpManager.loadHelpDoc", err) } return true } @@ -380,7 +368,7 @@ func (m *HelpManager) loadHelpDoc(group string, path string) bool { // validateXlsxHeaders 验证 xlsx 格式 helpdoc 的表头是否是 Key Synonym(可能有多列) Content Description Catalogue Tag func validateXlsxHeaders(headers []string) (int, error) { if len(headers) < 3 { - return 0, fmt.Errorf("helpdoc格式错误,缺少必须列 Key Synonym Content") + return 0, errors.New("helpdoc格式错误,缺少必须列 Key Synonym Content") } var ( @@ -442,205 +430,83 @@ out: return synonymCount, nil } -func (dm *DiceManager) AddHelpWithDice(dice *Dice) { - m := dm.Help - - addCmdMap := func(packageName string, cmdMap CmdMapCls) { - for k, v := range cmdMap { - content := v.Help - if content == "" { - content = v.ShortHelp - } - _ = m.AddItem(HelpTextItem{ - Group: HelpBuiltinGroup, - Title: k, - Content: content, - PackageName: packageName, - }) +func (m *HelpManager) addCmdMap(packageName string, cmdMap CmdMapCls) error { + for k, v := range cmdMap { + content := v.Help + if content == "" { + content = v.ShortHelp } - } - - addCmdMap("核心指令", dice.CmdMap) - for _, i := range dice.ExtList { - _ = m.AddItem(HelpTextItem{ + err := m.AddItem(docengine.HelpTextItem{ Group: HelpBuiltinGroup, - Title: i.Name, - Content: i.GetDescText(i), - PackageName: "扩展模块", + Title: k, + Content: content, + PackageName: packageName, }) - addCmdMap(i.Name, i.CmdMap) - } - _ = m.AddItemApply() -} - -func (m *HelpManager) AddItem(item HelpTextItem) error { - data := map[string]string{ - "group": item.Group, - "from": item.From, - "title": item.Title, - "content": item.Content, - "package": item.PackageName, - "_type": "helpdoc", - } - - id := m.GetNextID() - m.TextMap.Store(id, &item) - - if m.EngineType == 0 { - if m.batch == nil { - m.batch = m.Index.NewBatch() - } - if m.batchNum >= 50 { - err := m.Index.Batch(m.batch) - if err != nil { - return err - } - m.batch.Reset() - m.batchNum = 0 + if err != nil { + log.Errorf("AddCmdMapItem err:%v", err) + return err } - - m.batchNum++ - return m.batch.Index(id, data) } return nil } -func (m *HelpManager) AddItemApply() error { - if m.batch != nil { - err := m.Index.Batch(m.batch) - m.batch.Reset() - m.batch = nil +func (m *HelpManager) addInternalCmdHelp(cmdMap CmdMapCls) error { + err := m.addCmdMap("核心指令", cmdMap) + if err != nil { return err } return nil } -func (m *HelpManager) searchBleve(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (*bleve.SearchResult, int, int, int, error) { - // 在标题中查找 - queryTitle := query.NewMatchPhraseQuery(text) - queryTitle.SetField("title") - - titleOrContent := bleve.NewDisjunctionQuery(queryTitle) - - // 在正文中查找 - if !titleOnly { - for _, i := range reSpace.Split(text, -1) { - queryContent := query.NewMatchPhraseQuery(i) - queryContent.SetField("content") - titleOrContent.AddQuery(queryContent) +func (m *HelpManager) addExternalCmdHelp(ext []*ExtInfo) error { + for _, i := range ext { + err := m.AddItem(docengine.HelpTextItem{ + Group: HelpBuiltinGroup, + Title: i.Name, + Content: i.GetDescText(i), + PackageName: "扩展模块", + }) + if err != nil { + return err + } + err = m.addCmdMap(i.Name, i.CmdMap) + if err != nil { + return err } } + return nil +} - andQuery := bleve.NewConjunctionQuery(titleOrContent) - - // 限制查询组 - for _, i := range ctx.Group.HelpPackages { - queryPack := query.NewMatchPhraseQuery(i) - queryPack.SetField("package") - andQuery.AddQuery(queryPack) - } - - // 查询指定文档组 - if group != "" { - queryPack := query.NewMatchPhraseQuery(group) - queryPack.SetField("group") - andQuery.AddQuery(queryPack) - } - - req := bleve.NewSearchRequestOptions(andQuery, pageSize, (pageNum-1)*pageSize, false) +func (m *HelpManager) AddItem(item docengine.HelpTextItem) error { + _, err := m.searchEngine.AddItem(item) + return err +} - index := m.Index - res, err := index.Search(req) +func (m *HelpManager) AddItemApply(end bool) error { + err := m.searchEngine.AddItemApply(end) if err != nil { - return res, 0, 0, 0, err + return err } - - total := int(res.Total) - pageStart := (pageNum - 1) * pageSize - pageEnd := pageStart + len(res.Hits) - return res, total, pageStart, pageEnd, nil + return nil } -func (m *HelpManager) Search(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (res *bleve.SearchResult, total, pageStart, pageEnd int, err error) { - if pageSize <= 0 || pageNum <= 0 { - // 为了使Search的结果完全忠实于分页参数, 而不产生有结果但与分页不相符的情况 - return nil, 0, 0, 0, fmt.Errorf("分页参数错误") - } - - if m.EngineType == 0 { - return m.searchBleve(ctx, text, titleOnly, pageSize, pageNum, group) - } - - // 不是很好的做法,待优化 - items := HelpTextItems{} - var idLst []string - - m.TextMap.Range(func(id string, v *HelpTextItem) bool { - items = append(items, v) - idLst = append(idLst, id) - return true - }) - - hits := search.DocumentMatchCollection{} - matches := fuzzy.FindFrom(text, items) - - total = len(matches) - pageStart = (pageNum - 1) * pageSize - pageEnd = pageNum * pageSize - - if pageStart < total { - if pageEnd > total { - pageEnd = total - } - - for _, i := range matches[pageStart:pageEnd] { - hits = append(hits, &search.DocumentMatch{ - ID: idLst[i.Index], - Score: float64(i.Score), - }) - } - } else { - // 分页超出范围, 返回空结果 - pageStart = -1 - pageEnd = -1 - } - - return &bleve.SearchResult{ - Status: nil, - Request: nil, - Hits: hits, - Total: uint64(total), - }, total, pageStart, pageEnd, nil +func (m *HelpManager) Search(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (res *docengine.GeneralSearchResult, total, pageStart, pageEnd int, err error) { + return m.searchEngine.Search(ctx.Group.HelpPackages, text, titleOnly, pageSize, pageNum, group) } func (m *HelpManager) GetSuffixText() string { - switch m.EngineType { - case 0: - return "(本次搜索由全文搜索完成)" - default: - return "(本次搜索由快速文档查找完成)" - } + return m.searchEngine.GetSuffixText() } func (m *HelpManager) GetPrefixText() string { - switch m.EngineType { - case 0: - return "[全文搜索]" - default: - return "[快速文档查找]" - } + return m.searchEngine.GetPrefixText() } func (m *HelpManager) GetShowBestOffset() int { - switch m.EngineType { - case 0: - return 1 - default: - return 15 - } + return m.searchEngine.GetShowBestOffset() } -func (m *HelpManager) GetContent(item *HelpTextItem, depth int) string { +func (m *HelpManager) GetContent(item *docengine.HelpTextItem, depth int) string { if depth > 7 { return "{递归层数过多,不予显示}" } @@ -672,20 +538,15 @@ func (m *HelpManager) GetContent(item *HelpTextItem, depth int) string { result.WriteString(txt[formattedIdx:left]) formattedIdx = right name := txt[left+1 : right-1] - matched := false - // 注意: 效率更加不高 - m.TextMap.Range(func(key string, v *HelpTextItem) bool { - if v.Title == name { - result.WriteString(m.GetContent(v, depth+1)) - matched = true - return false - } - return true - }) - if !matched { + // 搜索TitleOnly,严格匹配Title的情形 + // 如果查询到对应数据,那么就调用m.GetContent + valueResult, err := m.searchEngine.GetHelpTextItemByTermTitle(name) + if err != nil { result.WriteByte('{') result.WriteString(name) result.WriteString(" - 未能找到}") + } else { + result.WriteString(m.GetContent(valueResult, depth+1)) } } result.WriteString(txt[formattedIdx:]) @@ -697,45 +558,56 @@ func generateHelpDocKey() string { return key } +// 修改 buildHelpDocTree 函数签名,添加进度参数 func buildHelpDocTree(node *HelpDoc, fn func(d *HelpDoc)) { - p, err := os.Stat(node.Path) - if err != nil { - return - } - - fn(node) + // 收集所有节点 + allNodes := []*HelpDoc{node} - if !p.IsDir() { - return - } + for i := 0; i < len(allNodes); i++ { + current := allNodes[i] - subs, err := os.ReadDir(node.Path) - if err != nil { - return - } + p, err := os.Stat(current.Path) + if err != nil { + continue + } - for _, sub := range subs { - if strings.HasPrefix(sub.Name(), ".") { + if !p.IsDir() { continue } - var child HelpDoc - child.Key = generateHelpDocKey() - child.Name = sub.Name() - child.Path = path.Join(node.Path, sub.Name()) - child.Group = node.Group - child.IsDir = sub.IsDir() - if sub.IsDir() { - child.Type = "dir" - child.Children = make([]*HelpDoc, 0) - } else { - child.Type = filepath.Ext(sub.Name()) + + subs, err := os.ReadDir(current.Path) + if err != nil { + continue } - fn(&child) - if sub.IsDir() { - buildHelpDocTree(&child, fn) + current.Children = make([]*HelpDoc, 0) + + for _, sub := range subs { + if strings.HasPrefix(sub.Name(), ".") { + continue + } + + var child HelpDoc + child.Key = generateHelpDocKey() + child.Name = sub.Name() + child.Path = path.Join(current.Path, sub.Name()) + child.Group = current.Group + child.IsDir = sub.IsDir() + + if sub.IsDir() { + child.Type = "dir" + child.Children = make([]*HelpDoc, 0) + } else { + child.Type = filepath.Ext(sub.Name()) + } + + allNodes = append(allNodes, &child) + current.Children = append(current.Children, &child) } - node.Children = append(node.Children, &child) + } + for _, current := range allNodes { + // 调用处理函数 + fn(current) } } @@ -922,12 +794,13 @@ func (m *HelpManager) GetHelpItemPage(pageNum, pageSize int, id, group, from, ti return 0, HelpTextVos{} } + // 如果ID不为空 if id != "" { - item, ok := m.TextMap.Load(id) - if ok && - strings.Contains(item.Group, group) && - strings.Contains(item.From, from) && - strings.Contains(item.Title, title) { + // 加载对应ID的数据 + item, err := m.searchEngine.GetItemByID(id) + // 若成功 + if err == nil { + // 返回这条数据 vo := HelpTextVo{ Group: item.Group, From: item.From, @@ -941,36 +814,25 @@ func (m *HelpManager) GetHelpItemPage(pageNum, pageSize int, id, group, from, ti } return 0, HelpTextVos{} } - temp := make(HelpTextVos, 0, m.TextMap.Len()) - m.TextMap.Range(func(i string, item *HelpTextItem) bool { - if strings.Contains(item.Group, group) && - strings.Contains(item.From, from) && - strings.Contains(item.Title, title) { - vo := HelpTextVo{ - Group: item.Group, - From: item.From, - Title: item.Title, - Content: item.Content, - PackageName: item.PackageName, - KeyWords: item.KeyWords, - } - vo.ID, _ = strconv.Atoi(i) - temp = append(temp, vo) + // ID为空的情形,分页查询数据 + total, result, err := m.searchEngine.PaginateDocuments(pageSize, pageNum, group, from, title) + if err != nil { + return 0, nil + } + var items = make(HelpTextVos, 0) + for _, item := range result { + vo := HelpTextVo{ + Group: item.Group, + From: item.From, + Title: item.Title, + Content: item.Content, + PackageName: item.PackageName, + KeyWords: item.KeyWords, } - return true - }) - - sort.Sort(temp) - - start := (pageNum - 1) * pageSize - end := start + pageSize - total := len(temp) - if start >= total { - return total, HelpTextVos{} - } else if end < total { - return total, temp[start:end] + vo.ID, _ = strconv.Atoi(id) + items = append(items, vo) } - return total, temp[start:] + return int(total), items } // SetDefaultHelpGroup 设置群默认搜索分组 diff --git a/dice/dice_jsvm.go b/dice/dice_jsvm.go index cc27debd..87439955 100644 --- a/dice/dice_jsvm.go +++ b/dice/dice_jsvm.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "regexp" + "runtime/debug" "strconv" "strings" "sync" @@ -28,12 +29,12 @@ import ( "github.com/pkg/errors" "github.com/robfig/cron/v3" "github.com/samber/lo" - "go.uber.org/zap" "gopkg.in/elazarl/goproxy.v1" "gopkg.in/yaml.v3" "sealdice-core/static" "sealdice-core/utils/crypto" + log "sealdice-core/utils/kratos" ) var ( @@ -98,7 +99,6 @@ func (d *Dice) JsInit() { d.JsScriptCron = cron.New() d.JsScriptCronLock = &sync.Mutex{} d.JsScriptCron.Start() - // 初始化 loop.Run(func(vm *goja.Runtime) { vm.SetFieldNameMapper(goja.TagFieldNameMapper("jsbind", true)) @@ -121,30 +121,30 @@ func (d *Dice) JsInit() { ban := vm.NewObject() _ = seal.Set("ban", ban) _ = ban.Set("addBan", func(ctx *MsgContext, id string, place string, reason string) { - d.BanList.AddScoreBase(id, d.BanList.ThresholdBan, place, reason, ctx) - d.BanList.SaveChanged(d) + (&d.Config).BanList.AddScoreBase(id, d.Config.BanList.ThresholdBan, place, reason, ctx) + (&d.Config).BanList.SaveChanged(d) }) _ = ban.Set("addTrust", func(ctx *MsgContext, id string, place string, reason string) { - d.BanList.SetTrustByID(id, place, reason) - d.BanList.SaveChanged(d) + (&d.Config).BanList.SetTrustByID(id, place, reason) + (&d.Config).BanList.SaveChanged(d) }) _ = ban.Set("remove", func(ctx *MsgContext, id string) { - _, ok := d.BanList.GetByID(id) + _, ok := (&d.Config).BanList.GetByID(id) if !ok { return } - d.BanList.DeleteByID(d, id) + (&d.Config).BanList.DeleteByID(d, id) }) _ = ban.Set("getList", func() []BanListInfoItem { var list []BanListInfoItem - d.BanList.Map.Range(func(key string, value *BanListInfoItem) bool { + (&d.Config).BanList.Map.Range(func(key string, value *BanListInfoItem) bool { list = append(list, *value) return true }) return list }) _ = ban.Set("getUser", func(id string) *BanListInfoItem { - i, ok := d.BanList.GetByID(id) + i, ok := (&d.Config).BanList.GetByID(id) if !ok { return nil } @@ -203,7 +203,7 @@ func (d *Dice) JsInit() { // Pinenutn: Range模板 ServiceAtNew重构代码 d.ImSession.ServiceAtNew.Range(func(key string, groupInfo *GroupInfo) bool { // Pinenutn: ServiceAtNew重构 - groupInfo.ExtActive(ei) + groupInfo.ExtActiveBySnapshotOrder(ei, true) return true }) }) @@ -511,7 +511,7 @@ func (d *Dice) JsInit() { if err != nil { return errors.New("解析失败:" + err.Error()) } - ret := d.GameSystemTemplateAdd(tmpl) + ret := d.GameSystemTemplateAddEx(tmpl, true) if !ret { return errors.New("已存在同名模板") } @@ -523,7 +523,7 @@ func (d *Dice) JsInit() { if err != nil { return errors.New("解析失败:" + err.Error()) } - ret := d.GameSystemTemplateAdd(tmpl) + ret := d.GameSystemTemplateAddEx(tmpl, true) if !ret { return errors.New("已存在同名模板") } @@ -598,15 +598,27 @@ func (d *Dice) JsInit() { // `) _, _ = vm.RunString(`Object.freeze(seal);Object.freeze(seal.deck);Object.freeze(seal.coc);Object.freeze(seal.ext);Object.freeze(seal.vars);`) }) - loop.Start() - d.JsEnable = true + go func() { + defer func() { + if r := recover(); r != nil { + log.Errorf("JS核心执行异常: %v 堆栈: %v", r, string(debug.Stack())) + } + }() + loop.StartInForeground() + }() + // loop.Start() + (&d.Config).JsEnable = true d.Logger.Info("已加载JS环境") + d.MarkModified() + d.Save(false) } func (d *Dice) JsShutdown() { - d.JsEnable = false + (&d.Config).JsEnable = false d.jsClear() d.Logger.Info("已关闭JS环境") + d.MarkModified() + d.Save(false) } func (d *Dice) jsClear() { @@ -630,7 +642,7 @@ func (d *Dice) jsClear() { d.RegisterBuiltinSystemTemplate() // 关闭js vm if d.JsLoop != nil { - d.JsLoop.Stop() + d.JsLoop.Terminate() d.JsLoop = nil } } @@ -774,9 +786,20 @@ func (d *Dice) JsReload() { d.JsScriptCron.Stop() d.JsScriptCron = nil } + + // 记录扩展快照 + d.ImSession.ServiceAtNew.Range(func(key string, groupInfo *GroupInfo) bool { + groupInfo.ExtListSnapshot = lo.Map(groupInfo.ActivatedExtList, func(item *ExtInfo, index int) string { + return item.Name + }) + return true + }) + d.JsInit() _ = d.ConfigManager.Load() d.JsLoadScripts() + d.MarkModified() + d.Save(false) } // JsExtSettingVacuum 清理已被删除的脚本对应的插件配置 @@ -795,7 +818,7 @@ func (d *Dice) JsExtSettingVacuum() { } idxToDel := []int{} - for k, v := range d.ExtDefaultSettings { + for k, v := range d.Config.ExtDefaultSettings { if !v.ExtItem.IsJsExt { continue } @@ -806,7 +829,7 @@ func (d *Dice) JsExtSettingVacuum() { for i := len(idxToDel) - 1; i >= 0; i-- { idx := idxToDel[i] - d.ExtDefaultSettings = append(d.ExtDefaultSettings[:idx], d.ExtDefaultSettings[idx+1:]...) + (&d.Config).ExtDefaultSettings = append((&d.Config).ExtDefaultSettings[:idx], (&d.Config).ExtDefaultSettings[idx+1:]...) } panic("DONT USE ME") @@ -1004,7 +1027,7 @@ func (d *Dice) JsParseMeta(s string, installTime time.Time, rawData []byte, buil jsInfo.ErrText = strings.Join(errMsg, "\n") return nil, errors.New(strings.Join(errMsg, "|")) } - jsInfo.Enable = !d.DisabledJsScripts[jsInfo.Name] + jsInfo.Enable = !(&d.Config).DisabledJsScripts[jsInfo.Name] return jsInfo, nil } @@ -1109,27 +1132,31 @@ func JsDelete(_ *Dice, jsInfo *JsScriptInfo) { } func JsEnable(d *Dice, jsInfoName string) { - delete(d.DisabledJsScripts, jsInfoName) + delete((&d.Config).DisabledJsScripts, jsInfoName) for _, jsInfo := range d.JsScriptList { if jsInfo.Name == jsInfoName { jsInfo.Enable = true } } + d.LastUpdatedTime = time.Now().Unix() + d.Save(false) } func JsDisable(d *Dice, jsInfoName string) { - d.DisabledJsScripts[jsInfoName] = true + (&d.Config).DisabledJsScripts[jsInfoName] = true for _, jsInfo := range d.JsScriptList { if jsInfo.Name == jsInfoName { jsInfo.Enable = false } } + d.LastUpdatedTime = time.Now().Unix() + d.Save(false) } func (d *Dice) JsCheckUpdate(jsScriptInfo *JsScriptInfo) (string, string, string, error) { // FIXME: dirty, copy from check deck update. if len(jsScriptInfo.UpdateUrls) == 0 { - return "", "", "", fmt.Errorf("插件未提供更新链接") + return "", "", "", errors.New("插件未提供更新链接") } statusCode, newData, err := GetCloudContent(jsScriptInfo.UpdateUrls, jsScriptInfo.Etag) @@ -1137,10 +1164,10 @@ func (d *Dice) JsCheckUpdate(jsScriptInfo *JsScriptInfo) (string, string, string return "", "", "", err } if statusCode == http.StatusNotModified { - return "", "", "", fmt.Errorf("插件没有更新") + return "", "", "", errors.New("插件没有更新") } if statusCode != http.StatusOK { - return "", "", "", fmt.Errorf("未获取到插件更新") + return "", "", "", errors.New("未获取到插件更新") } oldData, err := os.ReadFile(jsScriptInfo.Filename) if err != nil { @@ -1179,7 +1206,7 @@ func (d *Dice) JsUpdate(jsScriptInfo *JsScriptInfo, tempFileName string) error { return err } if len(newData) == 0 { - return fmt.Errorf("new data is empty") + return errors.New("new data is empty") } // 更新插件 err = os.WriteFile(jsScriptInfo.Filename, newData, 0o755) @@ -1335,7 +1362,7 @@ type JsScriptTask struct { entryID *cron.EntryID lock *sync.Mutex - logger *zap.SugaredLogger + logger *log.Helper } type JsScriptTaskCtx struct { @@ -1407,7 +1434,7 @@ func (t *JsScriptTask) reset(expr string) error { func parseTaskTime(taskTimeStr string) (string, error) { match := taskTimeRe.MatchString(taskTimeStr) if !match { - return "", fmt.Errorf("仅接受 24 小时表示的时间作为每天的执行时间,如 0:05 13:30") + return "", errors.New("仅接受 24 小时表示的时间作为每天的执行时间,如 0:05 13:30") } time, err := time.Parse("15:04", taskTimeStr) if err != nil { diff --git a/dice/dice_jsvm_test.go b/dice/dice_jsvm_test.go index 621a21de..749c3b71 100644 --- a/dice/dice_jsvm_test.go +++ b/dice/dice_jsvm_test.go @@ -247,7 +247,7 @@ func sameScriptInfos(a []*JsScriptInfo, b []*JsScriptInfo) bool { if len(a) != len(b) { return false } - for i := 0; i < len(a); i++ { + for i := range a { if !sameScriptInfo(a[i], b[i]) { return false } diff --git a/dice/dice_manager.go b/dice/dice_manager.go index 612a97f5..b5dc0b85 100644 --- a/dice/dice_manager.go +++ b/dice/dice_manager.go @@ -1,15 +1,15 @@ package dice import ( - "fmt" "os" "sync/atomic" "time" "github.com/dop251/goja_nodejs/require" "github.com/robfig/cron/v3" - "go.uber.org/zap" "gopkg.in/yaml.v3" + + log "sealdice-core/utils/kratos" ) type VersionInfo struct { @@ -75,14 +75,14 @@ type DiceManager struct { //nolint:revive ServiceName string JustForTest bool JsRegistry *require.Registry - UpdateSealdiceByFile func(packName string, log *zap.SugaredLogger) bool // 使用指定压缩包升级海豹,如果出错返回false,如果成功进程会自动结束 + UpdateSealdiceByFile func(packName string, log *log.Helper) bool // 使用指定压缩包升级海豹,如果出错返回false,如果成功进程会自动结束 ContainerMode bool // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq CleanupFlag atomic.Uint32 // 1 为正在清理,0为普通状态 } -type DiceConfigs struct { //nolint:revive - DiceConfigs []DiceConfig `yaml:"diceConfigs"` +type Configs struct { //nolint:revive + DiceConfigs []BaseConfig `yaml:"diceConfigs"` ServeAddress string `yaml:"serveAddress"` WebUIAddress string `yaml:"webUIAddress"` HelpDocEngineType int `yaml:"helpDocEngineType"` @@ -112,9 +112,12 @@ func (dm *DiceManager) InitHelp() { dm.IsHelpReloading = true _ = os.MkdirAll("./data/helpdoc", 0755) dm.Help = new(HelpManager) - dm.Help.Parent = dm - dm.Help.EngineType = dm.HelpDocEngineType - dm.Help.Load() + dm.Help.EngineType = EngineType(dm.HelpDocEngineType) + if len(dm.Dice) == 0 { + log.Fatalf("Dice实例不存在!") + return + } + dm.Help.Load(dm.Dice[0].CmdMap, dm.Dice[0].ExtList) dm.IsHelpReloading = false } @@ -149,10 +152,10 @@ func (dm *DiceManager) LoadDice() { return } - var dc DiceConfigs + var dc Configs err = yaml.Unmarshal(data, &dc) if err != nil { - fmt.Println("读取 data/dice.yaml 发生错误: 配置文件格式不正确") + log.Error("读取 data/dice.yaml 发生错误: 配置文件格式不正确", err) panic(err) } @@ -195,7 +198,7 @@ func (dm *DiceManager) LoadDice() { } func (dm *DiceManager) Save() { - var dc DiceConfigs + var dc Configs dc.ServeAddress = dm.ServeAddress dc.HelpDocEngineType = dm.HelpDocEngineType dc.UIPasswordSalt = dm.UIPasswordSalt @@ -236,7 +239,7 @@ func (dm *DiceManager) InitDice() { g, err := NewProcessExitGroup() if err != nil { - fmt.Println("进程组创建失败,若进程崩溃,gocqhttp进程可能需要手动结束。") + log.Warn("进程组创建失败,若进程崩溃,gocqhttp进程可能需要手动结束。") } else { dm.progressExitGroupWin = g } @@ -249,18 +252,14 @@ func (dm *DiceManager) InitDice() { go func() { defer func() { if r := recover(); r != nil { - fmt.Println("帮助文档加载失败。可能是由于退出程序过快,帮助文档还未加载完成所致", r) + log.Warn("帮助文档加载失败。可能是由于退出程序过快,帮助文档还未加载完成所致", r) if dm.Help != nil { - fmt.Println("帮助文件加载失败:", dm.Help.LoadingFn) + log.Warn("帮助文件加载失败:", dm.Help.LoadingFn) } } }() - // 加载帮助 dm.InitHelp() - if len(dm.Dice) >= 1 { - dm.AddHelpWithDice(dm.Dice[0]) - } }() dm.ResetAutoBackup() @@ -277,17 +276,15 @@ func (dm *DiceManager) ResetAutoBackup() { dm.backupEntryID, err = dm.Cron.AddFunc(dm.AutoBackupTime, func() { errBackup := dm.BackupAuto() if errBackup != nil { - fmt.Println("自动备份失败: ", errBackup.Error()) + log.Errorf("自动备份失败: %v", errBackup) return } if errBackup = dm.BackupClean(true); errBackup != nil { - fmt.Println("滚动清理备份失败: ", errBackup.Error()) + log.Errorf("滚动清理备份失败: %v", errBackup) } }) if err != nil { - if len(dm.Dice) > 0 { - dm.Dice[0].Logger.Errorf("设定的自动备份间隔有误: %v", err.Error()) - } + log.Errorf("设定的自动备份间隔有误: %v", err) return } } @@ -304,14 +301,11 @@ func (dm *DiceManager) ResetBackupClean() { dm.backupCleanCronID, err = dm.Cron.AddFunc(dm.BackupCleanCron, func() { errBackup := dm.BackupClean(false) if errBackup != nil { - fmt.Println("定时清理备份失败: ", errBackup.Error()) + log.Errorf("定时清理备份失败: %v", errBackup) } }) - if err != nil { - if len(dm.Dice) > 0 { - dm.Dice[0].Logger.Errorf("设定的备份清理cron有误: %q %v", dm.BackupCleanCron, err) - } + log.Errorf("设定的备份清理cron有误: %q %v", dm.BackupCleanCron, err) return } } @@ -325,9 +319,8 @@ func (dm *DiceManager) TryCreateDefault() { if len(dm.Dice) == 0 { defaultDice := new(Dice) defaultDice.BaseConfig.Name = "default" - defaultDice.BaseConfig.IsLogPrint = true - defaultDice.MessageDelayRangeStart = 0.4 - defaultDice.MessageDelayRangeEnd = 0.9 + defaultDice.Config.MessageDelayRangeStart = DefaultConfig.MessageDelayRangeStart + defaultDice.Config.MessageDelayRangeEnd = DefaultConfig.MessageDelayRangeEnd defaultDice.MarkModified() defaultDice.ContainerMode = dm.ContainerMode dm.Dice = append(dm.Dice, defaultDice) diff --git a/dice/dice_name_generator.go b/dice/dice_name_generator.go index cc0c0924..45f51e25 100644 --- a/dice/dice_name_generator.go +++ b/dice/dice_name_generator.go @@ -9,6 +9,8 @@ import ( wr "github.com/mroth/weightedrand" "github.com/xuri/excelize/v2" + + log "sealdice-core/utils/kratos" ) type NamesGenerator struct { @@ -24,7 +26,7 @@ func (ng *NamesGenerator) Load() { for _, fn := range []string{"./data/names/names.xlsx", "./data/names/names-dnd.xlsx"} { f, err := excelize.OpenFile(fn) if err != nil { - fmt.Println("加载names信息出错", fn, err) + log.Warn("加载names信息出错", fn, err) continue } @@ -53,7 +55,7 @@ func (ng *NamesGenerator) Load() { } if err := f.Close(); err != nil { - fmt.Println(err) + log.Error("NamesGenerator.Load", err) } } } @@ -112,7 +114,7 @@ func (ng *NamesGenerator) NameGenerate(rule string) string { length = length2 } - for index := 0; index < length; index++ { + for index := range length { choices = append(choices, wr.NewChoice(index, uint(weightLst[index]))) } restText = sp[0] diff --git a/dice/docengine/bleve.go b/dice/docengine/bleve.go new file mode 100644 index 00000000..492f9f0c --- /dev/null +++ b/dice/docengine/bleve.go @@ -0,0 +1,321 @@ +package docengine + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "sync/atomic" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" + + log "sealdice-core/utils/kratos" +) + +type BleveSearchEngine struct { + Index bleve.Index + batch *bleve.Batch + batchSize int + CurID uint64 +} + +var indexDir = "./data/_index" +var reSpace = regexp.MustCompile(`\s+`) + +// getNextID 使用原子操作,避免并发问题 +func (d *BleveSearchEngine) getNextID() string { + // 使用原子操作安全递增 CurID + nextID := atomic.AddUint64(&d.CurID, 1) + return strconv.FormatUint(nextID, 10) +} + +// NewEngine 创建并初始化 BleveSearchEngine +func NewBleveSearchEngine() (*BleveSearchEngine, error) { + engine := &BleveSearchEngine{} + err := engine.Init() + if err != nil { + return nil, err + } + return engine, nil +} + +func (d *BleveSearchEngine) GetSuffixText() string { + return "(本次搜索由全文搜索完成)" +} + +func (d *BleveSearchEngine) GetPrefixText() string { + return "[全文搜索]" +} + +func (d *BleveSearchEngine) GetShowBestOffset() int { + return 1 +} + +func (d *BleveSearchEngine) Init() error { + mapping := bleve.NewIndexMapping() + docMapping := bleve.NewDocumentMapping() + contentFieldMapping := bleve.NewTextFieldMapping() + keywordMapping := bleve.NewKeywordFieldMapping() + // 注意: 这里group,from,title,package都是keywordMapping,这样就能进行精确搜索。 + docMapping.AddFieldMappingsAt("group", keywordMapping) + docMapping.AddFieldMappingsAt("from", keywordMapping) + docMapping.AddFieldMappingsAt("title", contentFieldMapping) + // Content才是真正的文档 + docMapping.AddFieldMappingsAt("content", contentFieldMapping) + docMapping.AddFieldMappingsAt("package", keywordMapping) + mapping.AddDocumentMapping("helpdoc", docMapping) + mapping.TypeField = "_type" + i, err := bleve.New(indexDir, mapping) + if err != nil { + return err + } + d.Index = i + // 初始化ID列表 + d.CurID = 0 + // 初始化新的batch + d.batch = d.Index.NewBatch() + return nil +} + +func (d *BleveSearchEngine) Close() { + if d.Index != nil { + _ = d.Index.Close() + d.Index = nil + } +} + +func (d *BleveSearchEngine) GetTotalID() uint64 { + return d.CurID +} + +// AddItem 这里引用了dice,其实不妥,应该将它单独拆出来的。 +func (d *BleveSearchEngine) AddItem(item HelpTextItem) (string, error) { + // 如果batch为空,初始化一个batch + if d.batch == nil { + return "", errors.New("已通过end参数执行AddItemApply,不允许新增文档。请检查代码逻辑") + } + id := d.getNextID() + data := map[string]string{ + "group": item.Group, + "from": item.From, + "title": item.Title, + "content": item.Content, + "package": item.PackageName, + "_type": "helpdoc", + } + d.batchSize++ + // 五十一次执行 + if d.batchSize >= 50 { + err := d.AddItemApply(false) + d.batchSize = 0 + if err != nil { + return "", err + } + } + return id, d.batch.Index(id, data) +} + +// AddItemApply 这里认为是真正执行插入文档的逻辑 +// 由于现在已经将执行函数改为了可按文件执行,所以可以按文件进行Apply,这应当不会有太大的量级。 +// end代表是否是最后一次执行,一般用在所有的数据都处理完之后,关闭逻辑的时候使用,如bleve batch重复利用后最后销毁 +func (d *BleveSearchEngine) AddItemApply(end bool) error { + if d.batch != nil { + // 执行batch + err := d.Index.Batch(d.batch) + if err != nil { + return err + } + // 如果是最后一批 + if end { + d.batch.Reset() + d.batch = nil + } else { + // 否则仅重置batch + d.batch.Reset() + } + return err + } + return nil +} + +func (d *BleveSearchEngine) Search(helpPackages []string, text string, titleOnly bool, pageSize, pageNum int, group string) (*GeneralSearchResult, int, int, int, error) { + // 在标题中查找 + queryTitle := query.NewMatchPhraseQuery(text) + queryTitle.SetField("title") + + titleOrContent := bleve.NewDisjunctionQuery(queryTitle) + + // 在正文中查找 + if !titleOnly { + for _, i := range reSpace.Split(text, -1) { + queryContent := query.NewMatchPhraseQuery(i) + queryContent.SetField("content") + titleOrContent.AddQuery(queryContent) + } + } + + andQuery := bleve.NewConjunctionQuery(titleOrContent) + + // 限制查询组 + for _, i := range helpPackages { + queryPack := query.NewMatchPhraseQuery(i) + queryPack.SetField("package") + andQuery.AddQuery(queryPack) + } + + // 查询指定文档组 + if group != "" { + queryPack := query.NewMatchPhraseQuery(group) + queryPack.SetField("group") + andQuery.AddQuery(queryPack) + } + + req := bleve.NewSearchRequestOptions(andQuery, pageSize, (pageNum-1)*pageSize, false) + // 设置要被返回的数据 + req.Fields = []string{"*"} + res, err := d.Index.Search(req) + if err != nil { + return nil, 0, 0, 0, err + } + var resultList = make(MatchCollection, 0) + for _, hit := range res.Hits { + result := MatchResult{ + ID: hit.ID, + Fields: hit.Fields, + Score: hit.Score, + } + resultList = append(resultList, &result) + } + // 转换搜索格式 + responseResult := GeneralSearchResult{ + Hits: resultList, + Total: res.Total, + } + total := int(res.Total) + pageStart := (pageNum - 1) * pageSize + pageEnd := pageStart + len(res.Hits) + return &responseResult, total, pageStart, pageEnd, nil +} + +// 下面的代码都应该重构,因为它们返回的不是我们想要的结果 +// PaginateAllDocuments 分页查询所有文档 +// TODO:这里坏了,没有办法用,本来应该是精确匹配NewMatchQuery +func (d *BleveSearchEngine) PaginateDocuments(pageSize, pageNum int, group, from, title string) (uint64, []*HelpTextItem, error) { + var items []*HelpTextItem + // 只有Keyword才支持NewTermQuery + conjunctionQuery := bleve.NewConjunctionQuery() + if group != "" { + groupQuery := bleve.NewTermQuery(group) + groupQuery.SetField("group") + conjunctionQuery.AddQuery(groupQuery) + } + if from != "" { + fromQuery := bleve.NewTermQuery(from) + fromQuery.SetField("from") + conjunctionQuery.AddQuery(fromQuery) + } + if title != "" { + titleQuery := bleve.NewTermQuery(title) + titleQuery.SetField("title") + conjunctionQuery.AddQuery(titleQuery) + } + + // 计算分页参数 + fromInt := (pageNum - 1) * pageSize // 起始位置 + if fromInt < 0 { + fromInt = 0 + } + var searchRequest *bleve.SearchRequest + // 创建查询请求,设置分页参数 + if group == "" && from == "" && title == "" { + searchRequest = bleve.NewSearchRequestOptions(bleve.NewMatchAllQuery(), pageSize, fromInt, false) + } else { + searchRequest = bleve.NewSearchRequestOptions(conjunctionQuery, pageSize, fromInt, true) + } + searchRequest.Fields = []string{"*"} // 设置需要返回的字段 + + // 执行查询 + searchResult, err := d.Index.Search(searchRequest) + if err != nil { + return 0, nil, err + } + + // 处理结果 + for _, hit := range searchResult.Hits { + fields := hit.Fields + item := &HelpTextItem{ + Group: fmt.Sprintf("%v", fields["group"]), + From: fmt.Sprintf("%v", fields["from"]), + Title: fmt.Sprintf("%v", fields["title"]), + Content: fmt.Sprintf("%v", fields["content"]), + PackageName: fmt.Sprintf("%v", fields["package"]), + KeyWords: "", // 暂时空值 + RelatedExt: nil, // 暂时空值 + } + items = append(items, item) + } + return searchResult.Total, items, nil +} + +func (d *BleveSearchEngine) GetItemByID(id string) (*HelpTextItem, error) { + document, err := d.Index.Document(id) + if err != nil { + return nil, err + } + // 检查是否找到文档 + if document == nil { + return nil, errors.New("未找到匹配的文档") + } + item := HelpTextItem{} + // 看了看源码,意思就是这样访问文档内的所有fields + document.VisitFields(func(field index.Field) { + name := field.Name() + value := string(field.Value()) + // 这里的代码有点抽象…… + switch name { + case "group": + item.Group = value + case "from": + item.From = value + case "title": + item.Title = value + case "content": + item.Content = value + case "package": + item.PackageName = value + // 好像会碰到Type的参数? + default: + log.Debugf("这是个什么参数 %s", name) + } + }) + return &item, nil +} + +// 精确查询title +func (d *BleveSearchEngine) GetHelpTextItemByTermTitle(title string) (*HelpTextItem, error) { + newTermQuery := query.NewTermQuery(title) + newTermQuery.SetField("title") // 精确匹配title + req := bleve.NewSearchRequest(newTermQuery) + req.Fields = []string{"*"} + res, err := d.Index.Search(req) + if err != nil { + return nil, err + } + // 取出结果 + if len(res.Hits) > 0 { + fields := res.Hits[0].Fields + return &HelpTextItem{ + Group: fmt.Sprintf("%v", fields["group"]), + From: fmt.Sprintf("%v", fields["from"]), + Title: fmt.Sprintf("%v", fields["title"]), + Content: fmt.Sprintf("%v", fields["content"]), + PackageName: fmt.Sprintf("%v", fields["package"]), + // 这俩是什么东西?! + KeyWords: "", + RelatedExt: nil, + }, nil + } + return nil, errors.New("查询失败,未查询到数据") +} diff --git a/dice/docengine/enter.go b/dice/docengine/enter.go new file mode 100644 index 00000000..faa5795a --- /dev/null +++ b/dice/docengine/enter.go @@ -0,0 +1,54 @@ +package docengine + +type MatchResult struct { + ID string `json:"id"` + Fields map[string]interface{} `json:"fields"` + Score float64 `json:"score"` +} + +type Fields struct { +} + +type MatchCollection []*MatchResult + +// GeneralSearchResult Copied from bleve +type GeneralSearchResult struct { + Hits MatchCollection + Total uint64 +} + +type HelpTextItem struct { + Group string + From string + Title string + Content string + PackageName string + // 这俩玩意有用? + KeyWords string + RelatedExt []string +} + +// SearchEngine TODO: 进一步优化结构,封装成通用的搜索 +type SearchEngine interface { + GetSuffixText() string + GetPrefixText() string + GetShowBestOffset() int + // Init 初始化搜索引擎 + Init() error + // Close 关闭搜索引擎 + Close() + // AddItem 添加文档条目,返回添加文档的ID + AddItem(item HelpTextItem) (string, error) + // AddItemApply 提交文档条目 + AddItemApply(end bool) error + // Search 搜索文档条目 + Search(helpPackages []string, text string, titleOnly bool, pageSize, pageNum int, group string) (*GeneralSearchResult, int, int, int, error) + // GetHelpTextItemByTermTitle 精确查询title,用于嵌套获取数据的情形 + GetHelpTextItemByTermTitle(title string) (*HelpTextItem, error) + // GetItemByID 通过ID获取Item数据的方案 + GetItemByID(id string) (*HelpTextItem, error) + // PaginateDocuments 分页获取数据 + PaginateDocuments(pageSize, pageNum int, group, from, title string) (uint64, []*HelpTextItem, error) + // GetTotalID 获取当前ID总数,注意,ID必须是顺序排列的 + GetTotalID() uint64 +} diff --git a/dice/ext.go b/dice/ext.go index 3aba0a0f..04d0dd79 100644 --- a/dice/ext.go +++ b/dice/ext.go @@ -110,7 +110,7 @@ func GetExtensionDesc(ei *ExtInfo) string { func (i *ExtInfo) callWithJsCheck(d *Dice, f func()) { if i.IsJsExt { - if d.JsEnable { + if d.Config.JsEnable { waitRun := make(chan int, 1) d.JsLoop.RunOnLoop(func(vm *goja.Runtime) { defer func() { @@ -142,9 +142,7 @@ func (i *ExtInfo) StorageInit() error { // 使用互斥锁保护初始化过程,确保只初始化一次 i.dbMu.Lock() defer i.dbMu.Unlock() - d.Logger.Debugf("[扩展]:%s 正在尝试获取锁进行初始化", i.Name) if i.init { - d.Logger.Debug("[扩展]:初始化调用,但数据库已经加载") // 如果已经初始化,则直接返回 return nil } diff --git a/dice/ext_coc7.go b/dice/ext_coc7.go index f6f96f10..f96f5b7f 100644 --- a/dice/ext_coc7.go +++ b/dice/ext_coc7.go @@ -420,6 +420,7 @@ func RegisterBuiltinExtCoc7(self *Dice) { VarSetValueInt64(mctx, "$tD100", outcome) VarSetValueInt64(mctx, "$t判定值", checkVal) VarSetValueInt64(mctx, "$tSuccessRank", int64(successRank)) + VarSetValueStr(mctx, "$t属性表达式文本", expr2Text) var suffix string var suffixFull string @@ -460,24 +461,22 @@ func RegisterBuiltinExtCoc7(self *Dice) { commandInfoItems = append(commandInfoItems, infoItem) VarSetValueStr(mctx, "$t检定表达式文本", expr1Text) - VarSetValueStr(mctx, "$t属性表达式文本", expr2Text) VarSetValueStr(mctx, "$t检定计算过程", detailWrap) VarSetValueStr(mctx, "$t计算过程", detailWrap) SetTempVars(mctx, mctx.Player.Name) // 信息里没有QQ昵称,用这个顶一下 - VarSetValueStr(mctx, "$t结果文本", DiceFormatTmpl(mctx, "COC:检定_单项结果文本")) return nil } var text string if cmdArgs.SpecialExecuteTimes > 1 { VarSetValueInt64(mctx, "$t次数", int64(cmdArgs.SpecialExecuteTimes)) - if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.MaxExecuteTime) { + if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) { ReplyToSender(mctx, msg, DiceFormatTmpl(mctx, "COC:检定_轮数过多警告")) return CmdExecuteResult{Matched: true, Solved: true} } texts := []string{} - for i := 0; i < cmdArgs.SpecialExecuteTimes; i++ { + for range cmdArgs.SpecialExecuteTimes { ret := rollOne(true) if ret != nil { return *ret @@ -599,7 +598,7 @@ func RegisterBuiltinExtCoc7(self *Dice) { ReplyToSender(ctx, msg, text) case "details": help := "当前有coc7规则如下:\n" - for i := 0; i < 6; i++ { + for i := range 6 { basicStr := strings.ReplaceAll(SetCocRuleText[i], "\n", " ") help += fmt.Sprintf(".setcoc %d // %s\n", i, basicStr) } @@ -907,12 +906,12 @@ func RegisterBuiltinExtCoc7(self *Dice) { increment int64 newVarValue int64 } - RuleNotMatch := fmt.Errorf("rule not match") - FormatMismatch := fmt.Errorf("format mismatch") - SkillNotEntered := fmt.Errorf("skill not entered") - SkillTypeError := fmt.Errorf("skill value type error") - SuccessExprFormatError := fmt.Errorf("success expr format error") - FailExprFormatError := fmt.Errorf("fail expr format error") + RuleNotMatch := errors.New("rule not match") + FormatMismatch := errors.New("format mismatch") + SkillNotEntered := errors.New("skill not entered") + SkillTypeError := errors.New("skill value type error") + SuccessExprFormatError := errors.New("success expr format error") + FailExprFormatError := errors.New("fail expr format error") singleRe := regexp.MustCompile(`([a-zA-Z_\p{Han}]+)\s*(\d+)?\s*(\+(([^/]+)/)?\s*(.+))?`) check := func(skill string) (checkResult enCheckResult) { checkResult.valid = true @@ -1520,13 +1519,12 @@ func RegisterBuiltinExtCoc7(self *Dice) { return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} } } - if val > ctx.Dice.MaxCocCardGen { - val = ctx.Dice.MaxCocCardGen + if val > ctx.Dice.Config.MaxCocCardGen { + val = ctx.Dice.Config.MaxCocCardGen } - var i int64 var ss []string - for i = 0; i < val; i++ { + for range val { result := ctx.EvalFString(`力量:{力量=3d6*5} 敏捷:{敏捷=3d6*5} 意志:{意志=3d6*5}\n体质:{体质=3d6*5} 外貌:{外貌=3d6*5} 教育:{教育=(2d6+6)*5}\n体型:{体型=(2d6+6)*5} 智力:{智力=(2d6+6)*5} 幸运:{幸运=3d6*5}\nHP:{(体质+体型)/10} [{力量+敏捷+意志+体质+外貌+教育+体型+智力}/{力量+敏捷+意志+体质+外貌+教育+体型+智力+幸运}]`, nil) if result.vm.Error != nil { break diff --git a/dice/ext_coc7_template.go b/dice/ext_coc7_template.go index 00ea42d2..1f290a96 100644 --- a/dice/ext_coc7_template.go +++ b/dice/ext_coc7_template.go @@ -2,7 +2,8 @@ package dice import ( "encoding/json" - "fmt" + + log "sealdice-core/utils/kratos" ) var coc7TemplateData = ` @@ -762,7 +763,7 @@ func getCoc7CharTemplate() *GameSystemTemplate { temp := &GameSystemTemplate{} err := json.Unmarshal([]byte(coc7TemplateData), temp) if err != nil { - fmt.Println("解析模板错误:", err.Error()) + log.Errorf("解析模板错误: %v", err) return nil } diff --git a/dice/ext_deck.go b/dice/ext_deck.go index 413491ec..4f71bd55 100644 --- a/dice/ext_deck.go +++ b/dice/ext_deck.go @@ -626,7 +626,7 @@ func RegisterBuiltinExtDeck(d *Dice) { } if strings.EqualFold(deckName, "list") { //nolint:nestif - text := "载入并开启的牌堆:\n" + text := "" for _, i := range ctx.Dice.DeckList { if i.Enable { author := fmt.Sprintf(" 作者:%s", i.Author) @@ -640,7 +640,8 @@ func RegisterBuiltinExtDeck(d *Dice) { text += fmt.Sprintf("- %s 格式: %s%s%s 牌组数量: %d\n", i.Name, i.Format, author, version, count) } } - ReplyToSender(ctx, msg, text) + VarSetValueStr(ctx, "$t牌堆列表", text) + ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "其它:抽牌_牌堆列表")) } else if strings.EqualFold(deckName, "desc") { // 查看详情 text := cmdArgs.GetArgN(2) @@ -754,7 +755,7 @@ func RegisterBuiltinExtDeck(d *Dice) { // if times > 5 { // times = 5 // } - if times > int(ctx.Dice.MaxExecuteTime) { + if times > int(ctx.Dice.Config.MaxExecuteTime) { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告")) return CmdExecuteResult{Matched: true, Solved: true} } @@ -948,7 +949,7 @@ func deckStringFormat(ctx *MsgContext, deckInfo *DeckInfo, s string) (string, er s = ImageRewrite(s, imgSolve) s = strings.ReplaceAll(s, "\n", `\n`) - if ctx.Dice.VMVersionForDeck == "v1" { + if ctx.Dice.getTargetVmEngineVersion(VMVersionDeck) == "v1" { return DiceFormatV1(ctx, s) } else { return DiceFormatV2(ctx, s) @@ -1161,7 +1162,7 @@ func extractExecuteContent(s string) (string, string) { func (d *Dice) DeckCheckUpdate(deckInfo *DeckInfo) (string, string, string, error) { if len(deckInfo.UpdateUrls) == 0 { - return "", "", "", fmt.Errorf("牌堆未提供更新链接") + return "", "", "", errors.New("牌堆未提供更新链接") } statusCode, newData, err := GetCloudContent(deckInfo.UpdateUrls, deckInfo.Etag) @@ -1169,10 +1170,10 @@ func (d *Dice) DeckCheckUpdate(deckInfo *DeckInfo) (string, string, string, erro return "", "", "", err } if statusCode == http.StatusNotModified { - return "", "", "", fmt.Errorf("牌堆没有更新") + return "", "", "", errors.New("牌堆没有更新") } if statusCode != http.StatusOK { - return "", "", "", fmt.Errorf("未获取到牌堆更新") + return "", "", "", errors.New("未获取到牌堆更新") } oldData, err := os.ReadFile(deckInfo.Filename) @@ -1212,13 +1213,13 @@ func (d *Dice) DeckUpdate(deckInfo *DeckInfo, tempFileName string) error { return err } if len(newData) == 0 { - return fmt.Errorf("new data is empty") + return errors.New("new data is empty") } // 更新牌堆 ok := parseDeck(d, tempFileName, newData, deckInfo) if !ok { d.Logger.Errorf("牌堆“%s”更新失败,无法解析获取到的牌堆数据", deckInfo.Name) - return fmt.Errorf("无法解析获取到的牌堆数据") + return errors.New("无法解析获取到的牌堆数据") } err = os.WriteFile(deckInfo.Filename, newData, 0755) diff --git a/dice/ext_deck_test.go b/dice/ext_deck_test.go index fa73c352..ea7300d1 100644 --- a/dice/ext_deck_test.go +++ b/dice/ext_deck_test.go @@ -17,6 +17,7 @@ func TestDeck(t *testing.T) { k := s.Pick().(string) m[k] += 1 } + //nolint:forbidigo // just a test fmt.Println(m) // map[1:1002 2:988 3:954 4:1013 5:965 6:1016 7:4062] } @@ -36,5 +37,6 @@ func TestDeckLast(t *testing.T) { k = fmt.Sprintf("Last=%s", k) m[k] += 1 } + //nolint:forbidigo // just a test fmt.Println(m) // map[Last=1:1413 Last=2:1452 Last=3:1420 Last=4:1401 Last=5:1423 Last=6:1460 Last=7:1431] } diff --git a/dice/ext_dnd5e.go b/dice/ext_dnd5e.go index 4dfcb28d..386c92c1 100644 --- a/dice/ext_dnd5e.go +++ b/dice/ext_dnd5e.go @@ -72,7 +72,7 @@ func setupConfigDND(_ *Dice) AttributeConfigs { } func getPlayerNameTempFunc(mctx *MsgContext) string { - if mctx.Dice.PlayerNameWrapEnable { + if mctx.Dice.Config.PlayerNameWrapEnable { return fmt.Sprintf("<%s>", mctx.Player.Name) } return mctx.Player.Name @@ -398,26 +398,32 @@ func RegisterBuiltinExtDnd5e(self *Dice) { ".rc <属性> // .rc 力量\n" + ".rc <属性>豁免 // .rc 力量豁免\n" + ".rc <表达式> // .rc 力量+3\n" + + ".rc 3# <表达式> // 多重检定\n" + ".rc 优势 <表达式> // .rc 优势 力量+4\n" + ".rc 劣势 <表达式> [<原因>] // .rc 劣势 力量+4 推一下试试\n" + ".rc <表达式> @某人 // 对某人做检定" cmdRc := &CmdItemInfo{ - Name: "rc", - ShortHelp: helpRc, - Help: "DND5E 检定:\n" + helpRc, - AllowDelegate: true, + // Pinenutn: 从这里添加是否检查有多次检定,很隐蔽,通过简单研究cmdArgs是看不出来的,尚未清楚此处逻辑来源 + EnableExecuteTimesParse: true, + Name: "rc", + ShortHelp: helpRc, + Help: "DND5E 检定:\n" + helpRc, + AllowDelegate: true, Solve: func(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs) CmdExecuteResult { + // 获取代骰 mctx := GetCtxProxyFirst(ctx, cmdArgs) mctx.DelegateText = ctx.DelegateText mctx.Player.TempValueAlias = &ac.Alias - + // 参数确认 val := cmdArgs.GetArgN(1) switch val { case "", "help": return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} default: + // 获取参数 restText := cmdArgs.CleanArgs + // 检查是否符合要求 re := regexp.MustCompile(`^(优势|劣势|優勢|劣勢)`) m := re.FindString(restText) if m != "" { @@ -425,43 +431,85 @@ func RegisterBuiltinExtDnd5e(self *Dice) { m = strings.Replace(m, "劣勢", "劣势", 1) restText = strings.TrimSpace(restText[len(m):]) } - expr := fmt.Sprintf("D20%s + %s", m, restText) + // 准备要处理的函数 + expr := fmt.Sprintf("d20%s + %s", m, restText) + // 初始化VM mctx.CreateVmIfNotExists() + // 获取角色模板 tmpl := mctx.Group.GetCharTemplate(mctx.Dice) - mctx.Eval(tmpl.PreloadCode, nil) - mctx.setDndReadForVM(true) - - r := mctx.Eval(expr, nil) - if r.vm.Error != nil { - ReplyToSender(mctx, msg, "无法解析表达式: "+restText) + // 初始化多轮检定结果保存数组 + textList := make([]string, 0) + // 多轮检定判断 + round := 1 + if cmdArgs.SpecialExecuteTimes > 1 { + round = cmdArgs.SpecialExecuteTimes + } + // 从COC复制来的轮数检查,同时特判一次的情况,防止完全骰不出去点 + if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) && cmdArgs.SpecialExecuteTimes != 1 { + VarSetValueStr(ctx, "$t次数", strconv.Itoa(cmdArgs.SpecialExecuteTimes)) + ReplyToSender(mctx, msg, DiceFormatTmpl(mctx, "DND:检定_轮数过多警告")) return CmdExecuteResult{Matched: true, Solved: true} } - reason := r.vm.RestInput - if reason == "" { - reason = restText - } - detail := r.vm.GetDetailText() - - VarSetValueStr(ctx, "$t技能", reason) - VarSetValueStr(ctx, "$t检定过程文本", detail) - VarSetValueStr(ctx, "$t检定结果", r.ToString()) - - text := DiceFormatTmpl(ctx, "DND:检定") - // 指令信息 - commandInfo := map[string]interface{}{ + // commandInfo配置 + var commandInfo = map[string]interface{}{ "cmd": "rc", "rule": "dnd5e", "pcName": mctx.Player.Name, - "items": []interface{}{ - map[string]interface{}{ - "expr": expr, - "reason": reason, - "result": r.Value, - }, - }, + // items的赋值转移到下面 + // "items": []interface{}{}, + } + var commandItems = make([]interface{}, 0) + // 循环N轮 + for range round { + // 执行预订的code + mctx.Eval(tmpl.PreloadCode, nil) + // 为rc设定属性豁免 + mctx.setDndReadForVM(true) + // 执行了一次 + r := mctx.Eval(expr, nil) + // 执行出错就丢出去 + if r.vm.Error != nil { + ReplyToSender(mctx, msg, "无法解析表达式: "+restText) + return CmdExecuteResult{Matched: true, Solved: true} + } + // 拿到执行的结果 + reason := r.vm.RestInput + if reason == "" { + reason = restText + } + detail := r.vm.GetDetailText() + // Pinenutn/bugtower100:猜测这里只是格式化的部分,所以如果做多次检定,这个变量保存最后一次就够了 + VarSetValueStr(ctx, "$t技能", reason) + VarSetValueStr(ctx, "$t检定过程文本", detail) + VarSetValueStr(ctx, "$t检定结果", r.ToString()) + // 添加对应结果文本,若只执行一次,则使用DND检定,否则使用单项文本初始化 + if round == 1 { + textList = append(textList, DiceFormatTmpl(ctx, "DND:检定")) + } else { + textList = append(textList, DiceFormatTmpl(ctx, "DND:检定_单项结果文本")) + } + // 添加对应commandItems + commandItems = append(commandItems, map[string]interface{}{ + "expr": expr, + "reason": reason, + "result": r.Value, + }) + } + // 拼接文本 + // 由于循环内保留了最后一次的部分技能文本,所以这里不需要再初始化一次技能 + var text string + if round > 1 { + VarSetValueStr(ctx, "$t结果文本", strings.Join(textList, "\n")) + VarSetValueStr(ctx, "$t次数", strconv.Itoa(cmdArgs.SpecialExecuteTimes)) + text = DiceFormatTmpl(ctx, "DND:检定_多轮") + } else { + // 是单轮检定,不需要组装成多轮的描述 + text = textList[0] } + // 赋值commandItems + commandInfo["items"] = commandItems + // 设置对应的Command mctx.CommandInfo = commandInfo - if kw := cmdArgs.GetKwarg("ci"); kw != nil { info, err := json.Marshal(mctx.CommandInfo) if err == nil { @@ -470,9 +518,30 @@ func RegisterBuiltinExtDnd5e(self *Dice) { text += "\n" + "指令信息无法序列化" } } + + isHide := cmdArgs.Command == "rah" || cmdArgs.Command == "rch" + + if isHide { + if msg.Platform == "QQ-CH" { + ReplyToSender(ctx, msg, "QQ频道内尚不支持暗骰") + return CmdExecuteResult{Matched: true, Solved: true} + } + if ctx.Group != nil { + if ctx.IsPrivate { + ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_私聊不可用")) + } else { + ctx.CommandHideFlag = ctx.Group.GroupID + prefix := DiceFormatTmpl(ctx, "核心:暗骰_私聊_前缀") + ReplyGroup(ctx, msg, DiceFormatTmpl(ctx, "核心:暗骰_群内")) + ReplyPerson(ctx, msg, prefix+text) + } + } else { + ReplyToSender(ctx, msg, text) + } + return CmdExecuteResult{Matched: true, Solved: true} + } ReplyToSender(mctx, msg, text) } - return CmdExecuteResult{Matched: true, Solved: true} }, } @@ -1011,7 +1080,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) { if m != "" { restText = strings.TrimSpace(restText[len(m):]) } - expr := fmt.Sprintf("D20%s%s", m, restText) + expr := fmt.Sprintf("d20%s%s", m, restText) mctx.CreateVmIfNotExists() mctx.setDndReadForVM(true) r := mctx.Eval(expr, nil) @@ -1172,7 +1241,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) { if strings.HasPrefix(text, "+") { // 加值情况1,D20+ - r := ctx.Eval("D20"+text, nil) + r := ctx.Eval("d20"+text, nil) if r.vm.Error != nil { // 情况1,加值输入错误 return 1, name, val, detail, "" @@ -1183,7 +1252,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) { exprExists = true } else if strings.HasPrefix(text, "-") { // 加值情况1.1,D20- - r := ctx.Eval("D20"+text, nil) + r := ctx.Eval("d20"+text, nil) if r.vm.Error != nil { // 情况1,加值输入错误 return 1, name, val, detail, "" @@ -1205,7 +1274,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) { exprExists = true } else if strings.HasPrefix(text, "优势") || strings.HasPrefix(text, "劣势") { // 优势/劣势 - r := ctx.Eval("D20"+text, nil) + r := ctx.Eval("d20"+text, nil) if r.vm.Error != nil { // 优势劣势输入错误 return 2, name, val, detail, "" @@ -1234,6 +1303,9 @@ func RegisterBuiltinExtDnd5e(self *Dice) { text = strings.TrimPrefix(text, ",") // 情况1,名字是自己 name = mctx.Player.Name + // replace any space or \n with _ + name = strings.ReplaceAll(name, " ", "_") + name = strings.ReplaceAll(name, "\n", "_") // 情况2,名字是自己,没有加值 if !exprExists { val = int64(ds.Roll(nil, 20, 0)) @@ -1349,61 +1421,67 @@ func RegisterBuiltinExtDnd5e(self *Dice) { setInitNextRoundVars(ctx, lst, round) ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "DND:先攻_下一回合")) case "del", "rm": - names := cmdArgs.Args[1:] - riList := (RIList{}).LoadByCurGroup(ctx) - newList := RIList{} + tryDeleteMembersInInitList := func(deleteNames []string, riList RIList) (newList RIList, textOut strings.Builder, ok bool) { + round, _ := VarGetValueInt64(ctx, "$g回合数") + round %= int64(len(riList)) + toDeleted := map[string]bool{} + for _, i := range deleteNames { + toDeleted[i] = true + } - round, _ := VarGetValueInt64(ctx, "$g回合数") - round %= int64(len(riList)) + delCounter := 0 - toDeleted := map[string]bool{} - for _, i := range names { - toDeleted[i] = true - } + preCurrent := 0 // 每有一个在当前单位前面的单位被删除, 当前单位下标需要减 1 + for index, i := range riList { + if !toDeleted[i.name] { + newList = append(newList, i) + } else { + delCounter++ + textOut.WriteString(fmt.Sprintf("%2d. %s\n", delCounter, i.name)) - textOut := strings.Builder{} - textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_移除_前缀")) - delCounter := 0 + if int64(index) < round { + preCurrent++ + } + } + } + current := *riList[round] + currentDeleted := toDeleted[current.name] - preCurrent := 0 // 每有一个在当前单位前面的单位被删除, 当前单位下标需要减 1 - for index, i := range riList { - if !toDeleted[i.name] { - newList = append(newList, i) - } else { - delCounter++ - textOut.WriteString(fmt.Sprintf("%2d. %s\n", delCounter, i.name)) + round -= int64(preCurrent) + if round >= int64(len(newList)) { + round = 0 + } + VarSetValueInt64(ctx, "$g回合数", round) - if int64(index) < round { - preCurrent++ - } + if delCounter == 0 { + textOut.WriteString("- 没有找到任何单位\n") + return newList, textOut, false } - } - current := *riList[round] - currentDeleted := toDeleted[current.name] - round -= int64(preCurrent) - if round >= int64(len(newList)) { - round = 0 + newList.SaveToGroup(ctx) + if currentDeleted { + if len(newList) == 0 { + textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_清除列表")) + } else { + setInitNextRoundVars(ctx, newList, round) + // Note(Xiangze Li): 这是为了让回合结束的角色显示为被删除的角色,而不是当前角色的上一个 + VarSetValueStr(ctx, "$t当前回合角色名", current.name) + VarSetValueStr(ctx, "$t当前回合at", AtBuild(current.uid)) + textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_下一回合")) + } + } + return newList, textOut, true } - VarSetValueInt64(ctx, "$g回合数", round) - if delCounter == 0 { - textOut.WriteString("- 没有找到任何单位\n") + nameWithSpace, _ := cmdArgs.EatPrefixWith("del", "rm") + riList := (RIList{}).LoadByCurGroup(ctx) + _, textOut, ok := tryDeleteMembersInInitList([]string{nameWithSpace}, riList) + if !ok { + _, textOut, _ = tryDeleteMembersInInitList(cmdArgs.Args[1:], riList) } + textToSend := DiceFormatTmpl(ctx, "DND:先攻_移除_前缀") + textOut.String() - newList.SaveToGroup(ctx) - if currentDeleted { - if len(newList) == 0 { - textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_清除列表")) - } else { - setInitNextRoundVars(ctx, newList, round) - // Note(Xiangze Li): 这是为了让回合结束的角色显示为被删除的角色,而不是当前角色的上一个 - VarSetValueStr(ctx, "$t当前回合角色名", current.name) - VarSetValueStr(ctx, "$t当前回合at", AtBuild(current.uid)) - textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_下一回合")) - } - } - ReplyToSender(ctx, msg, textOut.String()) + ReplyToSender(ctx, msg, textToSend) case "set": name := cmdArgs.GetArgN(2) exists := name != "" @@ -1473,6 +1551,8 @@ func RegisterBuiltinExtDnd5e(self *Dice) { "dst": cmdSt, "rc": cmdRc, "ra": cmdRc, + "rah": cmdRc, + "rch": cmdRc, "drc": cmdRc, "buff": cmdBuff, "dbuff": cmdBuff, diff --git a/dice/ext_exp.go b/dice/ext_exp.go index e407d3e1..1a295b8e 100644 --- a/dice/ext_exp.go +++ b/dice/ext_exp.go @@ -590,7 +590,7 @@ func getCmdStBase(soi CmdStOverrideInfo) *CmdItemInfo { // 进行简化卡的尝试解析 input := cmdArgs.CleanArgs - re := regexp.MustCompile(`^(([^\s\-#]{1,25})([-#]))([^=\s\d]+\d+)`) + re := regexp.MustCompile(`^(([^\s\-#]{1,25})([-#]))([^=\s\d(\[{\-+]+\d+)`) matches := re.FindStringSubmatch(input) if len(matches) > 0 { flag := matches[3] @@ -644,7 +644,7 @@ func getCmdStBase(soi CmdStOverrideInfo) *CmdItemInfo { return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} } - rRestIput := ctx.vm.RestInput + rRestIput := mctx.vm.RestInput // 处理直接设置属性 var text string diff --git a/dice/ext_fun.go b/dice/ext_fun.go index bfdfa8aa..1a1eacaa 100644 --- a/dice/ext_fun.go +++ b/dice/ext_fun.go @@ -436,7 +436,7 @@ func RegisterBuiltinExtFun(self *Dice) { } else if val == "help" || val == "" { return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true} } else { - if self.MailEnable { + if self.Config.MailEnable { _ = ctx.Dice.SendMail(cmdArgs.CleanArgs, MailTypeSendNote) ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:留言_已记录")) return CmdExecuteResult{Matched: true, Solved: true} @@ -569,7 +569,7 @@ func RegisterBuiltinExtFun(self *Dice) { successDegrees := int64(0) failedCount := int64(0) var results []string - for i := int64(0); i < num; i++ { + for range num { v := DiceRoll64(6) if v >= 5 { successDegrees++ @@ -738,7 +738,7 @@ func RegisterBuiltinExtFun(self *Dice) { successDegrees := int64(0) var results []string - for i := int64(0); i < diceNum; i++ { + for range diceNum { v := DiceRoll64(10) if v <= checkVal { successDegrees++ @@ -1006,15 +1006,26 @@ func RegisterBuiltinExtFun(self *Dice) { return CmdExecuteResult{Matched: true, Solved: true} } - addNum := int64(10) + wodAdd := int64(10) if adding, exists := groupAttrs.LoadX("wodAdd"); exists { addNumX, _ := adding.ReadInt() - addNum = int64(addNumX) + wodAdd = int64(addNumX) + } + wodFace := int64(10) + if face, exists := groupAttrs.LoadX("wodPoints"); exists { + faceNumX, _ := face.ReadInt() + wodFace = int64(faceNumX) + } + wodSucc := int64(8) + if succ, exists := groupAttrs.LoadX("wodThreshold"); exists { + succNumX, _ := succ.ReadInt() + wodSucc = int64(succNumX) } - txt := readNumber(cmdArgs.CleanArgs, fmt.Sprintf("a%d", addNum)) + exprWoDicenum := fmt.Sprintf("a%dk%dm%d", wodAdd, wodSucc, wodFace) + txt := readNumber(cmdArgs.CleanArgs, exprWoDicenum) if txt == "" { - txt = fmt.Sprintf("10a%d", addNum) + txt = "10" + exprWoDicenum cmdArgs.Args = []string{txt} } cmdArgs.CleanArgs = txt @@ -1031,7 +1042,7 @@ func RegisterBuiltinExtFun(self *Dice) { ShortHelp: textHelp, Help: "文本模板指令:\n" + textHelp, Solve: func(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs) CmdExecuteResult { - if ctx.Dice.TextCmdTrustOnly { + if ctx.Dice.Config.TextCmdTrustOnly { // 检查master和信任权限 // 拒绝无权限访问 if ctx.PrivilegeLevel < 70 { @@ -1118,7 +1129,7 @@ func RegisterBuiltinExtFun(self *Dice) { if m == 0 { m = int(getDefaultDicePoints(ctx)) } - if t > int(ctx.Dice.MaxExecuteTime) { + if t > int(ctx.Dice.Config.MaxExecuteTime) { ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告")) return CmdExecuteResult{Matched: true, Solved: true} } @@ -1208,7 +1219,7 @@ func RegisterBuiltinExtFun(self *Dice) { } // NOTE(Xiangze Li): 允许创建更多轮数。使用洗牌算法后并不会很重复计算 - // if roulette.Time > int(ctx.Dice.MaxExecuteTime) { + // if roulette.Time > int(ctx.Dice.Config.MaxExecuteTime) { // ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告")) // return CmdExecuteResult{Matched: true, Solved: true} // } @@ -1226,7 +1237,7 @@ func RegisterBuiltinExtFun(self *Dice) { for i := range allNum { allNum[i] = i + 1 } - for idx := 0; idx < roulette.Time; idx++ { + for idx := range roulette.Time { i := int(roulette.Face) - 1 - idx j := rand.Intn(i + 1) allNum[i], allNum[j] = allNum[j], allNum[i] diff --git a/dice/ext_log.go b/dice/ext_log.go index e39c7b80..31068c11 100644 --- a/dice/ext_log.go +++ b/dice/ext_log.go @@ -26,7 +26,11 @@ var ErrGroupCardOverlong = errors.New("群名片长度超过限制") func SetPlayerGroupCardByTemplate(ctx *MsgContext, tmpl string) (string, error) { ctx.Player.TempValueAlias = nil // 防止dnd的hp被转为“生命值” - v := ctx.EvalFString(tmpl, nil) + config := ctx.GenDefaultRollVmConfig() + config.HookFuncValueStore = func(ctx *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) { + return nil, true + } + v := ctx.EvalFString(tmpl, config) if v.vm.Error != nil { ctx.Dice.Logger.Infof("SN指令模板错误: %v", v.vm.Error.Error()) return "", v.vm.Error @@ -294,7 +298,8 @@ func RegisterBuiltinExtLog(self *Dice) { group.UpdatedAtTime = time.Now().Unix() time.Sleep(time.Duration(0.3 * float64(time.Second))) - getAndUpload(group.GroupID, group.LogCurName) + // Note: 2024-10-15 经过简单测试,似乎能缓解#1034的问题,但无法根本解决。 + go getAndUpload(group.GroupID, group.LogCurName) group.LogCurName = "" group.UpdatedAtTime = time.Now().Unix() return CmdExecuteResult{Matched: true, Solved: true} @@ -880,11 +885,11 @@ func LogAppend(ctx *MsgContext, groupID string, logName string, logItem *model.L if ok { if size, okCount := model.LogLinesCountGet(ctx.Dice.DBLogs, groupID, logName); okCount { // 默认每记录500条发出提示 - if ctx.Dice.LogSizeNoticeEnable { - if ctx.Dice.LogSizeNoticeCount == 0 { - ctx.Dice.LogSizeNoticeCount = 500 + if ctx.Dice.Config.LogSizeNoticeEnable { + if ctx.Dice.Config.LogSizeNoticeCount == 0 { + ctx.Dice.Config.LogSizeNoticeCount = DefaultConfig.LogSizeNoticeCount } - if size > 0 && int(size)%ctx.Dice.LogSizeNoticeCount == 0 { + if size > 0 && int(size)%ctx.Dice.Config.LogSizeNoticeCount == 0 { VarSetValueInt64(ctx, "$t条数", size) text := DiceFormatTmpl(ctx, "日志:记录_条数提醒") // text := fmt.Sprintf("提示: 当前故事的文本已经记录了 %d 条", size) diff --git a/dice/ext_reply.go b/dice/ext_reply.go index 816057fb..0fb2ed0e 100644 --- a/dice/ext_reply.go +++ b/dice/ext_reply.go @@ -143,7 +143,7 @@ func RegisterBuiltinExtReply(dice *Dice) { OnNotCommandReceived: func(ctx *MsgContext, msg *Message) { // 当前,只有非指令才会匹配 rcs := ctx.Dice.CustomReplyConfig - if !ctx.Dice.CustomReplyConfigEnable { + if !ctx.Dice.Config.CustomReplyConfigEnable { return } executed := false @@ -153,7 +153,7 @@ func RegisterBuiltinExtReply(dice *Dice) { cleanText = strings.TrimSpace(cleanText) VarSetValueInt64(ctx, "$t文本长度", int64(len(cleanText))) - if dice.ReplyDebugMode { + if dice.Config.ReplyDebugMode { log.Infof("[回复调试]当前文本:“%s” hex: %x 字节形式: %v", cleanText, cleanText, []byte(cleanText)) } diff --git a/dice/ext_reply_logic.go b/dice/ext_reply_logic.go index 3ba4903a..e1e039b3 100644 --- a/dice/ext_reply_logic.go +++ b/dice/ext_reply_logic.go @@ -10,6 +10,8 @@ import ( "strings" "time" + log "sealdice-core/utils/kratos" + "github.com/antlabs/strsim" "gopkg.in/yaml.v3" ) @@ -148,7 +150,7 @@ func (m *ReplyConditionExprTrue) Check(ctx *MsgContext, _ *Message, _ *CmdArgs, // r := ctx.Eval(m.Value, ds.RollConfig{}) flags := RollExtraFlags{ V2Only: true, - V1Only: ctx.Dice.VMVersionForReply == "v1", + V1Only: ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1", } r, _, err := DiceExprEvalBase(ctx, m.Value, flags) @@ -182,7 +184,7 @@ func formatExprForReply(ctx *MsgContext, expr string) string { var text string var err error - if ctx.Dice.VMVersionForReply == "v1" { + if ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1" { text, err = DiceFormatV1(ctx, expr) if err != nil { // text = fmt.Sprintf("执行出错V1: %s", err.Error()) @@ -260,7 +262,7 @@ func (m *ReplyResultRunText) Execute(ctx *MsgContext, _ *Message, _ *CmdArgs) { time.Sleep(time.Duration(m.Delay * float64(time.Second))) flags := RollExtraFlags{ V2Only: true, - V1Only: ctx.Dice.VMVersionForReply == "v1", + V1Only: ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1", } _, _, _ = DiceExprTextBase(ctx, m.Message, flags) } @@ -303,7 +305,7 @@ func (c *ReplyConfig) Save(dice *Dice) { attrConfigFn := dice.GetExtConfigFilePath("reply", c.Filename) buf, err := yaml.Marshal(c) if err != nil { - fmt.Println(err) + log.Error("ReplyConfig.Save", err) } else { _ = os.WriteFile(attrConfigFn, buf, 0644) } diff --git a/dice/ext_story.go b/dice/ext_story.go index 3cd4cafc..2f2b0974 100644 --- a/dice/ext_story.go +++ b/dice/ext_story.go @@ -79,7 +79,7 @@ func cmdRandomName(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs, cmdsList [][ } // 开始抽取 - for i := int64(0); i < num; i++ { + for range num { rule := rules[rand.Int()%len(rules)] names = append(names, ctx.Dice.Parent.NamesGenerator.NameGenerate(rule)) } diff --git a/dice/im_helpers.go b/dice/im_helpers.go index 0c5bf9c2..8fa6e85f 100644 --- a/dice/im_helpers.go +++ b/dice/im_helpers.go @@ -64,9 +64,9 @@ func SetBotOnAtGroup(ctx *MsgContext, groupID string) *GroupInfo { group.Active = true } else { // 设定扩展情况 - sort.Sort(ExtDefaultSettingItemSlice(session.Parent.ExtDefaultSettings)) + sort.Sort(ExtDefaultSettingItemSlice(session.Parent.Config.ExtDefaultSettings)) var extLst []*ExtInfo - for _, i := range session.Parent.ExtDefaultSettings { + for _, i := range session.Parent.Config.ExtDefaultSettings { if i.ExtItem != nil { if i.AutoActive { extLst = append(extLst, i.ExtItem) @@ -81,7 +81,7 @@ func SetBotOnAtGroup(ctx *MsgContext, groupID string) *GroupInfo { GroupID: groupID, DiceIDActiveMap: new(SyncMap[string, bool]), DiceIDExistsMap: new(SyncMap[string, bool]), - CocRuleIndex: int(session.Parent.DefaultCocRuleIndex), + CocRuleIndex: int(session.Parent.Config.DefaultCocRuleIndex), UpdatedAtTime: time.Now().Unix(), }) // TODO: Pinenutn:总觉得这里不太对,但是又觉得合理,GPT也没说怎么改更好一些,求教 @@ -181,7 +181,7 @@ func ReplyGroupRaw(ctx *MsgContext, msg *Message, text string, flag string) { ctx.DelegateText = "" } - if ctx.Dice.RateLimitEnabled && msg.Platform == "QQ" { + if ctx.Dice.Config.RateLimitEnabled && msg.Platform == "QQ" { if !spamCheckPerson(ctx, msg) { spamCheckGroup(ctx, msg) } @@ -191,7 +191,7 @@ func ReplyGroupRaw(ctx *MsgContext, msg *Message, text string, flag string) { if d != nil { d.Logger.Infof("发给(群%s): %s", msg.GroupID, text) // 敏感词拦截:回复(群) - if d.EnableCensor && d.CensorMode == OnlyOutputReply { + if d.Config.EnableCensor && d.Config.CensorMode == OnlyOutputReply { // 先拿掉海豹码和CQ码再检查敏感词 checkText := sealCodeRe.ReplaceAllString(text, "") checkText = cqCodeRe.ReplaceAllString(checkText, "") @@ -256,7 +256,7 @@ func ReplyPersonRaw(ctx *MsgContext, msg *Message, text string, flag string) { ctx.DelegateText = "" } - if ctx.Dice.RateLimitEnabled && msg.Platform == "QQ" { + if ctx.Dice.Config.RateLimitEnabled && msg.Platform == "QQ" { spamCheckPerson(ctx, msg) } @@ -264,7 +264,7 @@ func ReplyPersonRaw(ctx *MsgContext, msg *Message, text string, flag string) { if d != nil { d.Logger.Infof("发给(帐号%s): %s", msg.Sender.UserID, text) // 敏感词拦截:回复(个人) - if d.EnableCensor && d.CensorMode == OnlyOutputReply { + if d.Config.EnableCensor && d.Config.CensorMode == OnlyOutputReply { // 先拿掉海豹码和CQ码再检查敏感词 checkText := sealCodeRe.ReplaceAllString(text, "") checkText = cqCodeRe.ReplaceAllString(checkText, "") @@ -449,16 +449,16 @@ func spamCheckPerson(ctx *MsgContext, msg *Message) bool { if ctx.Player.RateLimiter == nil { ctx.Player.RateLimitWarned = false - if ctx.Dice.PersonalReplenishRateStr == "" { - ctx.Dice.PersonalReplenishRateStr = "@every 3s" - ctx.Dice.PersonalReplenishRate = rate.Every(time.Second * 3) + if ctx.Dice.Config.PersonalReplenishRateStr == "" { + ctx.Dice.Config.PersonalReplenishRateStr = DefaultConfig.PersonalReplenishRateStr + ctx.Dice.Config.PersonalReplenishRate = DefaultConfig.PersonalReplenishRate } - if ctx.Dice.PersonalBurst == 0 { - ctx.Dice.PersonalBurst = 3 + if ctx.Dice.Config.PersonalBurst == 0 { + ctx.Dice.Config.PersonalBurst = DefaultConfig.PersonalBurst } ctx.Player.RateLimiter = rate.NewLimiter( - ctx.Dice.PersonalReplenishRate, - int(ctx.Dice.PersonalBurst), + ctx.Dice.Config.PersonalReplenishRate, + int(ctx.Dice.Config.PersonalBurst), ) } @@ -468,7 +468,7 @@ func spamCheckPerson(ctx *MsgContext, msg *Message) bool { } if ctx.Player.RateLimitWarned { - ctx.Dice.BanList.AddScoreByCommandSpam(ctx.Player.UserID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByCommandSpam(ctx.Player.UserID, msg.GroupID, ctx) } else { ctx.Player.RateLimitWarned = true replyToSenderRawNoCheck( @@ -500,16 +500,16 @@ func spamCheckGroup(ctx *MsgContext, msg *Message) bool { if ctx.Group.RateLimiter == nil { ctx.Group.RateLimitWarned = false - if ctx.Dice.GroupReplenishRateStr == "" { - ctx.Dice.GroupReplenishRateStr = "@every 3s" - ctx.Dice.GroupReplenishRate = rate.Every(time.Second * 3) + if ctx.Dice.Config.GroupReplenishRateStr == "" { + ctx.Dice.Config.GroupReplenishRateStr = DefaultConfig.GroupReplenishRateStr + ctx.Dice.Config.GroupReplenishRate = DefaultConfig.GroupReplenishRate } - if ctx.Dice.GroupBurst == 0 { - ctx.Dice.GroupBurst = 3 + if ctx.Dice.Config.GroupBurst == 0 { + ctx.Dice.Config.GroupBurst = DefaultConfig.GroupBurst } ctx.Group.RateLimiter = rate.NewLimiter( - ctx.Dice.GroupReplenishRate, - int(ctx.Dice.GroupBurst), + ctx.Dice.Config.GroupReplenishRate, + int(ctx.Dice.Config.GroupBurst), ) } @@ -520,7 +520,7 @@ func spamCheckGroup(ctx *MsgContext, msg *Message) bool { // If not allow if ctx.Group.RateLimitWarned { - ctx.Dice.BanList.AddScoreByCommandSpam(ctx.Group.GroupID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByCommandSpam(ctx.Group.GroupID, msg.GroupID, ctx) } else { ctx.Group.RateLimitWarned = true replyToSenderRawNoCheck( diff --git a/dice/im_session.go b/dice/im_session.go index 39dfafe2..a24eefd4 100644 --- a/dice/im_session.go +++ b/dice/im_session.go @@ -4,22 +4,25 @@ import ( "encoding/base64" "encoding/binary" "fmt" + "math/rand" "regexp" "runtime/debug" "sort" "strings" - "sync" "time" + "github.com/samber/lo" + "gorm.io/gorm" + "sealdice-core/dice/model" "sealdice-core/message" + log "sealdice-core/utils/kratos" "github.com/golang-module/carbon" ds "github.com/sealdice/dicescript" rand2 "golang.org/x/exp/rand" "github.com/dop251/goja" - "github.com/jmoiron/sqlx" "golang.org/x/time/rate" "gopkg.in/yaml.v3" ) @@ -78,6 +81,7 @@ type GroupPlayerInfo model.GroupPlayerInfoBase type GroupInfo struct { Active bool `json:"active" yaml:"active" jsbind:"active"` // 是否在群内开启 - 过渡为象征意义 ActivatedExtList []*ExtInfo `yaml:"activatedExtList,flow" json:"activatedExtList"` // 当前群开启的扩展列表 + ExtListSnapshot []string `yaml:"-" json:"-"` // 存放当前激活的扩展表,无论其是否存在,用于处理插件重载后优先级混乱的问题 Players *SyncMap[string, *GroupPlayerInfo] `yaml:"-" json:"-"` // 群员角色数据 GroupID string `yaml:"groupId" json:"groupId" jsbind:"groupId"` @@ -126,6 +130,36 @@ func (group *GroupInfo) ExtActive(ei *ExtInfo) { group.ExtClear() } +// ExtActiveBySnapshotOrder 按照快照顺序开启扩展 +func (group *GroupInfo) ExtActiveBySnapshotOrder(ei *ExtInfo, isFirstTimeLoad bool) { + // 这个机制用于解决js插件指令会覆盖原生扩展的指令的问题 + // 与之相关的问题是插件的自动激活,最好能够检测插件是否为首次加载 + orderLst := group.ExtListSnapshot + m := map[string]*ExtInfo{} + for _, i := range group.ActivatedExtList { + m[i.Name] = i + } + m[ei.Name] = ei + + var newLst []*ExtInfo + for _, i := range orderLst { + if m[i] != nil { + newLst = append(newLst, m[i]) + } + } + + // 当首次加载,如果快照列表中没有,将其新增 + if isFirstTimeLoad { + if !lo.Contains(orderLst, ei.Name) { + newLst = append(newLst, ei) + group.ExtListSnapshot = append(group.ExtListSnapshot, ei.Name) + } + } + + group.ActivatedExtList = newLst + group.ExtClear() +} + // ExtClear 清除多余的扩展项 func (group *GroupInfo) ExtClear() { m := map[string]bool{} @@ -191,7 +225,7 @@ func (group *GroupInfo) IsActive(ctx *MsgContext) bool { return false } -func (group *GroupInfo) PlayerGet(db *sqlx.DB, id string) *GroupPlayerInfo { +func (group *GroupInfo) PlayerGet(db *gorm.DB, id string) *GroupPlayerInfo { if group.Players == nil { group.Players = new(SyncMap[string, *GroupPlayerInfo]) } @@ -257,7 +291,7 @@ type EndPointInfoBase struct { Enable bool `yaml:"enable" json:"enable" jsbind:"enable"` // 是否启用 ProtocolType string `yaml:"protocolType" json:"protocolType"` // 协议类型,如onebot、koishi等 - IsPublic bool `yaml:"isPublic"` + IsPublic bool `yaml:"isPublic" json:"isPublic"` Session *IMSession `yaml:"-" json:"-"` } @@ -521,7 +555,7 @@ func (ctx *MsgContext) fillPrivilege(msg *Message) int { } // 加入黑名单相关权限 - if val, exists := ctx.Dice.BanList.GetByID(ctx.Player.UserID); exists { + if val, exists := ctx.Dice.Config.BanList.GetByID(ctx.Player.UserID); exists { switch val.Rank { case BanRankBanned: ctx.PrivilegeLevel = -30 @@ -562,7 +596,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) { // 注意: 此处必须开启,不然下面mctx.player取不到 autoOn := true if msg.Platform == "QQ-CH" { - autoOn = d.QQChannelAutoOn + autoOn = d.Config.QQChannelAutoOn } groupInfo = SetBotOnAtGroup(mctx, msg.GroupID) groupInfo.Active = autoOn @@ -710,13 +744,13 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) { log.Infof("收到群(%s)内<%s>(%s)的指令: %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message) } else { doLog := true - if d.OnlyLogCommandInGroup { + if d.Config.OnlyLogCommandInGroup { // 检查上级选项 doLog = false } if doLog { // 检查QQ频道的独立选项 - if msg.Platform == "QQ-CH" && (!d.QQChannelLogMessage) { + if msg.Platform == "QQ-CH" && (!d.Config.QQChannelLogMessage) { doLog = false } } @@ -728,7 +762,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) { } // 敏感词拦截:全部输入 - if mctx.IsCurGroupBotOn && d.EnableCensor && d.CensorMode == AllInput { + if mctx.IsCurGroupBotOn && d.Config.EnableCensor && d.Config.CensorMode == AllInput { hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "") if needToTerminate { return @@ -760,7 +794,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) { if msg.MessageType == "private" { if mctx.CommandID != 0 { log.Infof("收到<%s>(%s)的私聊指令: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message) - } else if !d.OnlyLogCommandInPrivate { + } else if !d.Config.OnlyLogCommandInPrivate { log.Infof("收到<%s>(%s)的私聊消息: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message) } } @@ -778,7 +812,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) { }() // 敏感词拦截:命令输入 - if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.EnableCensor && d.CensorMode == OnlyInputCommand { + if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.Config.EnableCensor && d.Config.CensorMode == OnlyInputCommand { hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "") if needToTerminate { return @@ -925,7 +959,7 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) { // 注意: 此处必须开启,不然下面mctx.player取不到 autoOn := true if msg.Platform == "QQ-CH" { - autoOn = d.QQChannelAutoOn + autoOn = d.Config.QQChannelAutoOn } groupInfo = SetBotOnAtGroup(mctx, msg.GroupID) groupInfo.Active = autoOn @@ -1047,13 +1081,13 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) { log.Infof("收到群(%s)内<%s>(%s)的指令: %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message) } else { doLog := true - if d.OnlyLogCommandInGroup { + if d.Config.OnlyLogCommandInGroup { // 检查上级选项 doLog = false } if doLog { // 检查QQ频道的独立选项 - if msg.Platform == "QQ-CH" && (!d.QQChannelLogMessage) { + if msg.Platform == "QQ-CH" && (!d.Config.QQChannelLogMessage) { doLog = false } } @@ -1069,13 +1103,13 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) { // TODO(Szzrain): 需要优化的写法,不应根据 CommandID 来判断是否是指令,而应该根据 cmdArgs 是否 match 到指令来判断,同上 if mctx.CommandID != 0 { log.Infof("收到<%s>(%s)的私聊指令: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message) - } else if !d.OnlyLogCommandInPrivate { + } else if !d.Config.OnlyLogCommandInPrivate { log.Infof("收到<%s>(%s)的私聊消息: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message) } } // 敏感词拦截:全部输入 - if mctx.IsCurGroupBotOn && d.EnableCensor && d.CensorMode == AllInput { + if mctx.IsCurGroupBotOn && d.Config.EnableCensor && d.Config.CensorMode == AllInput { hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "") if needToTerminate { return @@ -1167,7 +1201,7 @@ func (s *IMSession) PreTriggerCommand(mctx *MsgContext, msg *Message, cmdArgs *C }() // 敏感词拦截:命令输入 - if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.EnableCensor && d.CensorMode == OnlyInputCommand { + if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.Config.EnableCensor && d.Config.CensorMode == OnlyInputCommand { hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "") if needToTerminate { return @@ -1350,10 +1384,7 @@ func (s *IMSession) OnGroupMemberJoined(ctx *MsgContext, msg *Message) { stdID := msg.Sender.UserID VarSetValueStr(ctx, "$t帐号ID", stdID) VarSetValueStr(ctx, "$t账号ID", stdID) - text, err := DiceFormatV2(ctx, groupInfo.GroupWelcomeMessage) - if err != nil { - text = fmt.Sprintf("执行出错V2: %s", err.Error()) - } + text := DiceFormat(ctx, groupInfo.GroupWelcomeMessage) for _, i := range ctx.SplitText(text) { doSleepQQ(ctx) ReplyGroup(ctx, msg, strings.TrimSpace(i)) @@ -1363,110 +1394,104 @@ func (s *IMSession) OnGroupMemberJoined(ctx *MsgContext, msg *Message) { } } -// 借助类似操作系统信号量的思路来做一个互斥锁 -var muxAutoQuit sync.Mutex -var groupLeaveNum int var platformRE = regexp.MustCompile(`^(.*)-Group:`) -// LongTimeQuitInactiveGroup 另一种退群方案,其中minute代表间隔多久执行一次,num代表一次退几个群(每次退群之间有10秒的等待时间) -func (s *IMSession) LongTimeQuitInactiveGroup(threshold, hint time.Time, roundIntervalMinute int, groupsPerRound int) { - // 该方案目前是issue方案的简化版,是我制作的鲨群机的略高级解决方式。 - // 该方案下,将会创建一个线程,从该时间开始计算将要退出的群聊并以minute为间隔的时间退出num个群。 - if !muxAutoQuit.TryLock() { - // 如果没能获得“临界资源”信号量 - // 直接输出有任务在运行中 - hint := fmt.Sprintf("有任务在运行中,已经退群 %d 个", groupLeaveNum) - s.Parent.Logger.Info(hint) - return - } - - s.Parent.Logger.Infof("开始清理不活跃群聊. 判定线 %s", threshold.Format(time.RFC3339)) - go func() { - type GroupEndpointPair struct { - Group *GroupInfo - Endpoint *EndPointInfo +// LongTimeQuitInactiveGroupReborn +// 完全抛弃当初不懂Go的时候的方案,改成如下方案: +// 每次尝试找到n个符合要求的群,然后启一个线程,将群统一干掉 +// 这样子牺牲了可显示的总群数,但大大增强了稳定性,而且总群数的参考并无意义,因为已经在的群很可能突然活了而不符合判定 +// 当前版本的问题:如果用户设置了很短的时间,那可能之前的群还没退完,就又退那部分的群,造成一些奇怪的问题,但应该概率不大 + 豹错会被捕获 +func (s *IMSession) LongTimeQuitInactiveGroupReborn(threshold time.Time, groupsPerRound int) { + s.Parent.Logger.Infof("开始清理不活跃群聊. 判定线 %s, 本次退群数: %d", threshold.Format(time.RFC3339), groupsPerRound) + type GroupEndpointPair struct { + Group *GroupInfo + Endpoint *EndPointInfo + Last time.Time + } + var selectedGroupEndpoints = make([]*GroupEndpointPair, 0) + var groupCount int + s.ServiceAtNew.Range(func(key string, grp *GroupInfo) bool { + // 如果是PG开头的,忽略掉 + if strings.HasPrefix(grp.GroupID, "PG-") { + return true } - - defer muxAutoQuit.Unlock() - - groupLeaveNum = 0 - selectedGroupEndpoints := []*GroupEndpointPair{} // 创建一个存放 grp 和 ep 组合的切片 - - // Pinenutn: Range模板 ServiceAtNew重构代码 - s.ServiceAtNew.Range(func(key string, grp *GroupInfo) bool { - // Pinenutn: ServiceAtNew重构 - if strings.HasPrefix(grp.GroupID, "PG-") { - return true + // 如果在BanList(这应该是白名单?)内,忽略掉 + if s.Parent.Config.BanList != nil { + info, ok := s.Parent.Config.BanList.GetByID(grp.GroupID) + if ok && info.Rank > BanRankNormal { + return true // 信任等级高于普通的不清理 } - if s.Parent.BanList != nil { - info, ok := s.Parent.BanList.GetByID(grp.GroupID) - if ok && info.Rank > BanRankNormal { - return true // 信任等级高于普通的不清理 + } + // 看看是不是QQ群,如果是QQ群,才进一步判断 + match := platformRE.FindStringSubmatch(grp.GroupID) + if len(match) != 2 { + return true + } + platform := match[1] + if platform != "QQ" { + return true + } + // 获取上次骰子活动时间 + last := time.Unix(grp.RecentDiceSendTime, 0) + // 如果enter是进入时间,它比活动时间更晚(说明骰子刚进去,但是骰子还没有说话),那么上次骰子活动时间=进入时间 + if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) { + last = enter + } + // 如果在上述所有操作后,发现时间仍然是0,那么必须忽略该值,因为可能是还没初始化的群,不能人家刚进来就走 + // 注意不能用last.Equal(time.Time{}),因为这里是时间戳的1970-01-01,而Go初始时间是0000-01-01. + // 预防性代码:如果last是0000-01-01,那也不应该被退群。 + if last.Unix() <= 0 { + return true + } + // 如果时间比要退群的时间早 + if last.Before(threshold) { + for _, ep := range s.EndPoints { + // 找到对应的endpoints,并准备退掉它的群 + if ep.Platform != platform || !grp.DiceIDExistsMap.Exists(ep.UserID) { + continue } - } - - last := time.Unix(grp.RecentDiceSendTime, 0) - if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) { - last = enter - } - match := platformRE.FindStringSubmatch(grp.GroupID) - if len(match) != 2 { - return true - } - platform := match[1] - if platform != "QQ" { - return true - } - if last.Before(threshold) { - for _, ep := range s.EndPoints { - if ep.Platform != platform || !grp.DiceIDExistsMap.Exists(ep.UserID) { - continue - } - selectedGroupEndpoints = append(selectedGroupEndpoints, &GroupEndpointPair{Group: grp, Endpoint: ep}) + selectedGroupEndpoints = append(selectedGroupEndpoints, &GroupEndpointPair{Group: grp, Endpoint: ep, Last: last}) + // 如果群数量超过本次要退的群数量,就不再继续了,退出出去 + groupCount++ + // 如果已经超过了一次退群的数量,则退出循环 + if groupCount > groupsPerRound { + return false } - } else if last.Before(hint) { - s.Parent.Logger.Warnf("检测到群 %s 上次活动时间为 %s,将在未来自动退出", grp.GroupID, last.Format(time.RFC3339)) } - return true - }) - // 采用类似分页的手法进行退群 - groupCount := len(selectedGroupEndpoints) - rounds := (groupCount + groupsPerRound - 1) / groupsPerRound - for round := 0; round < rounds; round++ { - startIndex := round * groupsPerRound - endIndex := (round + 1) * groupsPerRound - if endIndex > groupCount { - endIndex = groupCount - } - for _, pair := range selectedGroupEndpoints[startIndex:endIndex] { - grp := pair.Group - ep := pair.Endpoint - last := time.Unix(grp.RecentDiceSendTime, 0) - if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) { - last = enter - } - hint := fmt.Sprintf("检测到群 %s 上次活动时间为 %s,尝试退出", grp.GroupID, last.Format(time.RFC3339)) - s.Parent.Logger.Info(hint) - msgCtx := CreateTempCtx(ep, &Message{ - MessageType: "group", - Sender: SenderBase{UserID: ep.UserID}, - GroupID: grp.GroupID, - }) - msgText := DiceFormatTmpl(msgCtx, "核心:骰子自动退群告别语") - ep.Adapter.SendToGroup(msgCtx, grp.GroupID, msgText, "") - // 和我自制的鲨群机时间同步 - time.Sleep(10 * time.Second) - grp.DiceIDExistsMap.Delete(ep.UserID) - grp.UpdatedAtTime = time.Now().Unix() - ep.Adapter.QuitGroup(&MsgContext{Dice: s.Parent}, grp.GroupID) - // 保证在多次点击时可以收到日志 - groupLeaveNum++ - (&MsgContext{Dice: s.Parent, EndPoint: ep, Session: s}).Notice(hint) - } - // 等三十分钟 - hint := fmt.Sprintf("第 %d 轮退群已经完成,共计 %d 轮,休息 %d 分钟中", round, rounds, roundIntervalMinute) + } + return true + }) + // 循环完毕,要不然是因为够了要退的数量,要不就是遍历完毕了,但是不够,总之要进行退群活动了 + go func() { + if r := recover(); r != nil { + log.Errorf("自动退群异常: %v 堆栈: %v", r, string(debug.Stack())) + } + for i, pair := range selectedGroupEndpoints { + grp := pair.Group + ep := pair.Endpoint + last := pair.Last + hint := fmt.Sprintf("检测到群 %s 上次活动时间为 %s,尝试退出,当前为本轮第 %d 个", grp.GroupID, last.Format(time.RFC3339), i+1) s.Parent.Logger.Info(hint) - time.Sleep(time.Duration(roundIntervalMinute) * time.Minute) + // 创建对应退群信息 + msgCtx := CreateTempCtx(ep, &Message{ + MessageType: "group", + Sender: SenderBase{UserID: ep.UserID}, + GroupID: grp.GroupID, + }) + // 发送退群消息 + msgText := DiceFormatTmpl(msgCtx, "核心:骰子自动退群告别语") + ep.Adapter.SendToGroup(msgCtx, grp.GroupID, msgText, "") + // 删除群聊绑定信息,更新群处理时间 + grp.DiceIDExistsMap.Delete(ep.UserID) + grp.UpdatedAtTime = time.Now().Unix() + // 执行真正的退群活动,理论上这个msgCtx就能直接用 + ep.Adapter.QuitGroup(msgCtx, grp.GroupID) + // 发出提示 + msgCtx.Notice(hint) + // 生成一个随机值(8~11秒随机) + randomSleep := time.Duration(rand.Intn(3000)+8000) * time.Millisecond + log.Infof("退群等待,等待 %f 秒后继续", randomSleep.Seconds()) + time.Sleep(randomSleep) } }() } @@ -1475,6 +1500,11 @@ func (s *IMSession) LongTimeQuitInactiveGroup(threshold, hint time.Time, roundIn func FormatBlacklistReasons(v *BanListInfoItem) string { var sb strings.Builder sb.WriteString("黑名单原因:") + if v == nil { + sb.WriteString("\n") + sb.WriteString("原因未知,请联系开发者获取进一步信息") + return sb.String() + } for i, reason := range v.Reasons { sb.WriteString("\n") sb.WriteString(carbon.CreateFromTimestamp(v.Times[i]).ToDateTimeString()) @@ -1494,7 +1524,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { var isBanGroup, isWhiteGroup bool // log.Info("check ban ", msg.MessageType, " ", msg.GroupID, " ", ctx.PrivilegeLevel) if msg.MessageType == "group" { - value, exists := d.BanList.GetByID(msg.GroupID) + value, exists := d.Config.BanList.GetByID(msg.GroupID) if exists { if value.Rank == BanRankBanned { isBanGroup = true @@ -1506,7 +1536,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { } banQuitGroup := func() { - banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID) + banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.Sender.UserID) reasontext := FormatBlacklistReasons(banListInfoItem) groupID := msg.GroupID noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),自动退群\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext) @@ -1523,9 +1553,9 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { if ctx.PrivilegeLevel == -30 { groupLevel := ctx.GroupRoleLevel - if d.BanList.BanBehaviorQuitIfAdmin && msg.MessageType == "group" { + if (d.Config.BanList.BanBehaviorQuitIfAdmin || d.Config.BanList.BanBehaviorQuitIfAdminSilentIfNotAdmin) && msg.MessageType == "group" { // 黑名单用户 - 立即退出所在群 - banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID) + banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.Sender.UserID) reasontext := FormatBlacklistReasons(banListInfoItem) groupID := msg.GroupID notReply = true @@ -1546,16 +1576,21 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { log.Infof("收到群(%s)内普通群员黑名单用户<%s>(%s)的消息,但在信任群所以不做其他操作", groupID, msg.Sender.Nickname, msg.Sender.UserID) } else { notReply = true - noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,进行群内通告\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext) - log.Info(noticeMsg) + if d.Config.BanList.BanBehaviorQuitIfAdmin { + noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,进行群内通告\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext) + log.Info(noticeMsg) - text := fmt.Sprintf("警告: <%s>(%s)是黑名单用户,将对骰主进行通知。", msg.Sender.Nickname, msg.Sender.UserID) - ReplyGroupRaw(ctx, &Message{GroupID: groupID}, text, "") + text := fmt.Sprintf("警告: <%s>(%s)是黑名单用户,将对骰主进行通知。", msg.Sender.Nickname, msg.Sender.UserID) + ReplyGroupRaw(ctx, &Message{GroupID: groupID}, text, "") - ctx.Notice(noticeMsg) + ctx.Notice(noticeMsg) + } else { + noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,忽略黑名单用户信息,不做其他操作\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext) + log.Info(noticeMsg) + } } } - } else if d.BanList.BanBehaviorQuitPlaceImmediately && msg.MessageType == "group" { + } else if d.Config.BanList.BanBehaviorQuitPlaceImmediately && msg.MessageType == "group" { notReply = true // 黑名单用户 - 立即退出所在群 groupID := msg.GroupID @@ -1564,16 +1599,17 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { } else { banQuitGroup() } - } else if d.BanList.BanBehaviorRefuseReply { + } else if d.Config.BanList.BanBehaviorRefuseReply { notReply = true // 黑名单用户 - 拒绝回复 log.Infof("忽略黑名单用户信息: 来自群(%s)内<%s>(%s): %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message) } } else if isBanGroup { - if d.BanList.BanBehaviorQuitPlaceImmediately && !isWhiteGroup { + if d.Config.BanList.BanBehaviorQuitPlaceImmediately && !isWhiteGroup { notReply = true // 黑名单群 - 立即退出 - banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID) + // 退群使用GroupID进行判断 + banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.GroupID) reasontext := FormatBlacklistReasons(banListInfoItem) groupID := msg.GroupID if isWhiteGroup { @@ -1589,7 +1625,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) { time.Sleep(1 * time.Second) ctx.EndPoint.Adapter.QuitGroup(ctx, groupID) } - } else if d.BanList.BanBehaviorRefuseReply { + } else if d.Config.BanList.BanBehaviorRefuseReply { notReply = true // 黑名单群 - 拒绝回复 log.Infof("忽略黑名单群消息: 来自群(%s)内<%s>(%s): %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message) @@ -1668,7 +1704,7 @@ func (s *IMSession) commandSolve(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs } if cur != -1 { - if ctx.Dice.PlayerNameWrapEnable { + if ctx.Dice.Config.PlayerNameWrapEnable { ctx.DelegateText = fmt.Sprintf("由<%s>代骰:\n", ctx.Player.Name) } else { ctx.DelegateText = fmt.Sprintf("由%s代骰:\n", ctx.Player.Name) @@ -1958,13 +1994,13 @@ func (d *Dice) NoticeForEveryEndpoint(txt string, allowCrossPlatform bool) { } }() - if d.MailEnable { + if d.Config.MailEnable { _ = d.SendMail(txt, MailTypeNotice) return } for _, ep := range d.ImSession.EndPoints { - for _, i := range d.NoticeIDs { + for _, i := range d.Config.NoticeIDs { n := strings.Split(i, ":") // 如果文本中没有-,则会取到整个字符串 // 但好像不严谨,比如QQ-CH-Group @@ -2001,14 +2037,14 @@ func (ctx *MsgContext) NoticeCrossPlatform(txt string) { } }() - if ctx.Dice.MailEnable { + if ctx.Dice.Config.MailEnable { _ = ctx.Dice.SendMail(txt, MailTypeNotice) return } sent := false - for _, i := range ctx.Dice.NoticeIDs { + for _, i := range ctx.Dice.Config.NoticeIDs { n := strings.Split(i, ":") if len(n) < 2 { continue @@ -2059,14 +2095,14 @@ func (ctx *MsgContext) Notice(txt string) { } }() - if ctx.Dice.MailEnable { + if ctx.Dice.Config.MailEnable { _ = ctx.Dice.SendMail(txt, MailTypeNotice) return } sent := false if ctx.EndPoint.Enable { - for _, i := range ctx.Dice.NoticeIDs { + for _, i := range ctx.Dice.Config.NoticeIDs { n := strings.Split(i, ":") if len(n) >= 2 { if strings.HasSuffix(n[0], "-Group") { diff --git a/dice/im_vars.go b/dice/im_vars.go index 23cc6087..6403b9a9 100644 --- a/dice/im_vars.go +++ b/dice/im_vars.go @@ -222,7 +222,7 @@ func SetTempVars(ctx *MsgContext, qqNickname string) { pcName = strings.ReplaceAll(pcName, `\f`, "") VarSetValueStr(ctx, "$t玩家", fmt.Sprintf("<%s>", pcName)) - if ctx.Dice != nil && !ctx.Dice.PlayerNameWrapEnable { + if ctx.Dice != nil && !ctx.Dice.Config.PlayerNameWrapEnable { VarSetValueStr(ctx, "$t玩家", pcName) } VarSetValueStr(ctx, "$t玩家_RAW", pcName) diff --git a/dice/js_api.go b/dice/js_api.go index a9cf539a..d7f00a3a 100644 --- a/dice/js_api.go +++ b/dice/js_api.go @@ -3,15 +3,15 @@ package dice import ( "crypto/md5" "encoding/base64" + "encoding/hex" "errors" - "fmt" "os" "path/filepath" - "go.uber.org/zap" + log "sealdice-core/utils/kratos" ) -func Base64ToImageFunc(logger *zap.SugaredLogger) func(string) (string, error) { +func Base64ToImageFunc(logger *log.Helper) func(string) (string, error) { return func(b64 string) (string, error) { // 解码 Base64 值 data, e := base64.StdEncoding.DecodeString(b64) @@ -21,7 +21,7 @@ func Base64ToImageFunc(logger *zap.SugaredLogger) func(string) (string, error) { } // 计算 MD5 哈希值作为文件名 hash := md5.Sum(data) //nolint:gosec - filename := fmt.Sprintf("%x", hash) + filename := hex.EncodeToString(hash[:]) tempDir := os.TempDir() // 构建文件路径 imageurlPath := filepath.Join(tempDir, filename) diff --git a/dice/logger/logger.go b/dice/logger/logger.go index 7a0359cf..a6b0ae93 100644 --- a/dice/logger/logger.go +++ b/dice/logger/logger.go @@ -1,113 +1,20 @@ package logger import ( - "encoding/json" - "os" - - "github.com/natefinch/lumberjack" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" + log "sealdice-core/utils/kratos" ) -var enabledLevel = zap.InfoLevel - -func SetEnableLevel(level zapcore.Level) { - switch level { - case zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel, - zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel: - { - enabledLevel = level - } - default: // no-op - } -} - -type LogItem struct { - Level string `json:"level"` - TS float64 `json:"ts"` - Caller string `json:"caller"` - Msg string `json:"msg"` -} - -type WriterX struct { - LogLimit int64 - Items []*LogItem -} - type LogInfo struct { - LoggerRaw *zap.Logger - Logger *zap.SugaredLogger - WX *WriterX + Logger *log.Helper + WX *log.WriterX } -var logLimitDefault int64 = 100 - -func (w *WriterX) Write(p []byte) (n int, err error) { - var a LogItem - err2 := json.Unmarshal(p, &a) - if err2 == nil { - w.Items = append(w.Items, &a) - limit := w.LogLimit - if limit == 0 { - w.LogLimit = logLimitDefault - } - if len(w.Items) > int(limit) { - w.Items = w.Items[1:] - } - } - return len(p), nil -} - -func Init(path string, name string, enableConsoleLog bool) *LogInfo { - lumlog := &lumberjack.Logger{ - Filename: path, - MaxSize: 10, // megabytes - MaxBackups: 3, // number of log files - MaxAge: 7, // days - } - - encoder := getEncoder() - - pe := zap.NewProductionEncoderConfig() - wx := &WriterX{} - - cores := []zapcore.Core{ - zapcore.NewCore(encoder, zapcore.AddSync(lumlog), enabledLevel), - - // This outputs to WebUI, DO NOT apply enabledLevel - zapcore.NewCore(zapcore.NewJSONEncoder(pe), zapcore.AddSync(wx), zapcore.InfoLevel), - } - - if enableConsoleLog { - pe2 := zap.NewProductionEncoderConfig() - pe2.EncodeTime = zapcore.ISO8601TimeEncoder - - consoleEncoder := zapcore.NewConsoleEncoder(pe2) - consoleEncoder.AddString("dice", name) - cores = append(cores, zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), enabledLevel)) - } - - core := zapcore.NewTee(cores...) - - loggerRaw := zap.New(core, zap.AddCaller()) - defer func(loggerRaw *zap.Logger) { - _ = loggerRaw.Sync() - }(loggerRaw) // flushes buffer, if any - - logger := loggerRaw.Sugar() - logger.Infow("Dice日志开始记录") - +func Init() *LogInfo { + // KV输出 + loghelper := log.NewCustomHelper(log.LOG_DICE, false, nil) + loghelper.Info("Dice日志开始记录") return &LogInfo{ - LoggerRaw: loggerRaw, - Logger: logger, - WX: wx, + Logger: loghelper, + WX: log.GetWriterX(), } } - -func getEncoder() zapcore.Encoder { - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder - - return zapcore.NewConsoleEncoder(encoderConfig) -} diff --git a/dice/model/attr.go b/dice/model/attr.go deleted file mode 100644 index ddcc9d77..00000000 --- a/dice/model/attr.go +++ /dev/null @@ -1,39 +0,0 @@ -package model - -import ( - "fmt" - - "github.com/jmoiron/sqlx" -) - -func attrGetAllBase(db *sqlx.DB, bucket string, key string) []byte { - var buf []byte - - query := `SELECT updated_at, data FROM ` + bucket + ` WHERE id=:id` - rows, err := db.NamedQuery(query, map[string]interface{}{"id": key}) - if err != nil { - fmt.Println("Failed to execute query:", err) - return buf - } - - defer rows.Close() - - for rows.Next() { - var updatedAt int64 - var data []byte - - err := rows.Scan(&updatedAt, &data) - if err != nil { - fmt.Println("Failed to scan row:", err) - break - } - - buf = data - } - - return buf -} - -func AttrUserGetAll(db *sqlx.DB, userID string) []byte { - return attrGetAllBase(db, "attrs_user", userID) -} diff --git a/dice/model/attrs_new.go b/dice/model/attrs_new.go index 6f3d0242..eae1ce87 100644 --- a/dice/model/attrs_new.go +++ b/dice/model/attrs_new.go @@ -1,13 +1,13 @@ package model import ( - "database/sql" "errors" + "fmt" "time" - "sealdice-core/utils" + "gorm.io/gorm" - "github.com/jmoiron/sqlx" + "sealdice-core/utils" ds "github.com/sealdice/dicescript" ) @@ -22,188 +22,279 @@ const ( // 注: 角色表有用sheet也有用sheets的,这里数据结构中使用sheet // AttributesItemModel 新版人物卡。说明一下,这里带s的原因是attrs指的是一个map +// 补全GORM缺少部分 type AttributesItemModel struct { - Id string `json:"id" db:"id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid - Data []byte `json:"data" db:"data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些? - AttrsType string `json:"attrsType" db:"attrs_type"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user) + Id string `json:"id" gorm:"column:id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid + Data []byte `json:"data" gorm:"column:data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些? + AttrsType string `json:"attrsType" gorm:"column:attrs_type;index:idx_attrs_attrs_type_id;default:NULL"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user) // 这些是群组内置卡专用的,其实就是替代了绑卡关系表,作为群组内置卡时,这个字段用于存放绑卡关系 - BindingSheetId string `json:"bindingSheetId" db:"binding_sheet_id"` // 绑定的卡片ID + BindingSheetId string `json:"bindingSheetId" gorm:"column:binding_sheet_id;default:'';index:idx_attrs_binding_sheet_id"` // 绑定的卡片ID // 这些是角色卡专用的 - Name string `json:"name" db:"name"` // 卡片名称 - OwnerId string `json:"ownerId" db:"owner_id"` // 若有明确归属,就是对应的UniformID - SheetType string `json:"sheetType" db:"sheet_type"` // 卡片类型,如dnd5e coc7 - IsHidden bool `json:"isHidden" db:"is_hidden"` // 隐藏的卡片不出现在 pc list 中 + Name string `json:"name" gorm:"column:name"` // 卡片名称 + OwnerId string `json:"ownerId" gorm:"column:owner_id;index:idx_attrs_owner_id_id"` // 若有明确归属,就是对应的UniformID + SheetType string `json:"sheetType" gorm:"column:sheet_type"` // 卡片类型,如dnd5e coc7 + // 手动定义bool类的豹存方式 + IsHidden bool `json:"isHidden" gorm:"column:is_hidden;type:bool"` // 隐藏的卡片不出现在 pc list 中 // 通用属性 - CreatedAt int64 `json:"createdAt" db:"created_at"` - UpdatedAt int64 `json:"updatedAt" db:"updated_at"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"` // 下面的属性并非数据库字段,而是用于内存中的缓存 - BindingGroupsNum int64 `json:"bindingGroupNum"` // 当前绑定中群数 + BindingGroupsNum int64 `json:"bindingGroupNum" gorm:"-"` // 当前绑定中群数 +} + +// 兼容旧版本数据库 +func (*AttributesItemModel) TableName() string { + return "attrs" } func (m *AttributesItemModel) IsDataExists() bool { - return m.Data != nil && len(m.Data) > 0 + return len(m.Data) > 0 } // TOOD: 下面这个表记得添加 unique 索引 // PlatformMappingModel 虚拟ID - 平台用户ID 映射表 type PlatformMappingModel struct { - Id string `json:"id" db:"id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal - IMUserID string `json:"IMUserID" db:"im_user_id"` // IM平台的用户ID + Id string `json:"id" gorm:"column:id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal + IMUserID string `json:"IMUserID" gorm:"column:im_user_id"` // IM平台的用户ID } -func AttrsGetById(db *sqlx.DB, id string) (*AttributesItemModel, error) { +func AttrsGetById(db *gorm.DB, id string) (*AttributesItemModel, error) { + // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错 var item AttributesItemModel - err := db.Get(&item, `select id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id, - sheet_type, is_hidden, created_at, updated_at from attrs where id = $1`, id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at"). + Where("id = ?", id). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象本身就是预期正常的行为 + Find(&item).Error + if err != nil { return nil, err } return &item, nil } // AttrsGetBindingSheetIdByGroupId 获取当前正在绑定的ID -func AttrsGetBindingSheetIdByGroupId(db *sqlx.DB, id string) (string, error) { +func AttrsGetBindingSheetIdByGroupId(db *gorm.DB, id string) (string, error) { + // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错 var item AttributesItemModel - err := db.Get(&item, "select binding_sheet_id from attrs where id = $1", id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("binding_sheet_id"). + Where("id = ?", id). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回id=""就是预期正常的行为 + Find(&item).Error + if err != nil { return "", err } return item.BindingSheetId, nil } -func AttrsGetIdByUidAndName(db *sqlx.DB, userId string, name string) (string, error) { +func AttrsGetIdByUidAndName(db *gorm.DB, userId string, name string) (string, error) { + // 这里必须使用AttributesItemModel结构体 + // 如果你定义一个只有ID属性的结构体去接收,居然有概率能接收到值,这样就会和之前的行为不一致了 var item AttributesItemModel - err := db.Get(&item, "select id from attrs where owner_id = $1 and name = $2", userId, name) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("id"). + Where("owner_id = ? AND name = ?", userId, name). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象的id=""就是预期正常的行为 + Find(&item).Error + if err != nil { return "", err } return item.Id, nil } -func AttrsPutById(db *sqlx.DB, tx *sql.Tx, id string, data []byte, name, sheetType string) error { - // TODO: 好像还不够,需要nickname 需要sheetType,还有别的吗 - var err error - now := time.Now().Unix() - query := `insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at, name, sheet_type) - values ($1, $2, true, '', $3, $3, $4, $5) - on conflict (id) do update set data = $2, updated_at = $3, name = $4, sheet_type = $5` - args := []any{id, data, now, name, sheetType} - - if tx != nil { - _, err = tx.Exec(query, args...) - } else { - _, err = db.Exec(query, args...) +func AttrsPutById(db *gorm.DB, id string, data []byte, name, sheetType string) error { + now := time.Now().Unix() // 获取当前时间 + // 这里的原本逻辑是:第一次全量创建,第二次修改部分属性 + // 所以使用了Attrs和Assign配合使用 + if err := db.Where("id = ?", id). + Attrs(map[string]any{ + // 第一次全量建表 + "id": id, + // 使用BYTE规避无法插入的问题 + "data": BYTE(data), + "is_hidden": true, + "binding_sheet_id": "", + "name": name, + "sheet_type": sheetType, + "created_at": now, + "updated_at": now, + }). + // 如果是更新的情况,更新下面这部分,则需要被更新的为: + Assign(map[string]any{ + "data": BYTE(data), + "updated_at": now, + "name": name, + "sheet_type": sheetType, + }).FirstOrCreate(&AttributesItemModel{}).Error; err != nil { + return err // 返回错误 } - return err + return nil // 操作成功,返回 nil } -func AttrsDeleteById(db *sqlx.DB, id string) error { - var err error - query := `delete from attrs where id = ?` - args := []any{id} - - _, err = db.Exec(query, args...) - return err +func AttrsDeleteById(db *gorm.DB, id string) error { + // 使用 GORM 的 Delete 方法删除指定 id 的记录 + if err := db.Where("id = ?", id).Delete(&AttributesItemModel{}).Error; err != nil { + return err // 返回错误 + } + return nil // 操作成功,返回 nil } -func AttrsCharGetBindingList(db *sqlx.DB, id string) ([]string, error) { - rows, err := db.Query(`select id from attrs where binding_sheet_id = $1`, id) - if err != nil { - return nil, err - } +func AttrsCharGetBindingList(db *gorm.DB, id string) ([]string, error) { + // 定义一个切片用于存储结果 + var lst []string - lst := []string{} - for rows.Next() { - item := "" - err = rows.Scan(&item) - if err != nil { - return nil, err - } - lst = append(lst, item) + // 使用 GORM 查询绑定的 id 列表 + if err := db.Model(&AttributesItemModel{}). + Select("id"). + Where("binding_sheet_id = ?", id). + Find(&lst).Error; err != nil { + return nil, err // 返回错误 } - return lst, err + return lst, nil // 返回结果切片 } -func AttrsCharUnbindAll(db *sqlx.DB, id string) (int64, error) { - rows, err := db.Exec(`update attrs set binding_sheet_id = '' where binding_sheet_id = $1`, id) - if err != nil { - return 0, err - } - affected, err := rows.RowsAffected() - if err != nil { - return 0, err +func AttrsCharUnbindAll(db *gorm.DB, id string) (int64, error) { + // 使用 GORM 更新绑定的记录,将 binding_sheet_id 设为空字符串 + result := db.Model(&AttributesItemModel{}). + Where("binding_sheet_id = ?", id). + Update("binding_sheet_id", "") + + if result.Error != nil { + return 0, result.Error // 返回错误 } - return affected, err + return result.RowsAffected, nil // 返回受影响的行数 } // AttrsNewItem 新建一个角色卡/属性容器 -func AttrsNewItem(db *sqlx.DB, item *AttributesItemModel) (*AttributesItemModel, error) { - id := utils.NewID() - now := time.Now().Unix() - item.CreatedAt, item.UpdatedAt = now, now +func AttrsNewItem(db *gorm.DB, item *AttributesItemModel) (*AttributesItemModel, error) { + id := utils.NewID() // 生成新的 ID + now := time.Now().Unix() // 获取当前时间 + item.CreatedAt, item.UpdatedAt = now, now // 设置创建和更新时间 + if item.Id == "" { - item.Id = id + item.Id = id // 如果 ID 为空,则赋值新生成的 ID } - var err error - _, err = db.Exec(` - insert into attrs (id, data, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at, attrs_type) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, - item.Id, item.Data, item.BindingSheetId, item.Name, item.OwnerId, item.SheetType, item.IsHidden, - item.CreatedAt, item.UpdatedAt, item.AttrsType) - return item, err + // 使用 GORM 的 Create 方法插入新记录 + // 这个木落没有忽略错误,所以说这个可以安心使用Create而不用担心出现问题…… + // 这里使用Create可以正确插入byte数组,注意map[string]any里面不可以用byte数组,否则无法入库 + if err := db.Create(item).Error; err != nil { + return nil, err // 返回错误 + } + return item, nil // 返回新创建的项 } -func AttrsBindCharacter(db *sqlx.DB, charId string, id string) error { +func AttrsBindCharacter(db *gorm.DB, charId string, id string) error { + // 开始事务 + tx := db.Begin() + if tx.Error != nil { + return tx.Error // 返回错误 + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() // 发生恐慌时回滚 + } + }() + + // 将新字典值转换为 JSON + now := time.Now().Unix() json, err := ds.NewDictVal(nil).V().ToJSON() if err != nil { + tx.Rollback() // 返回错误时回滚 return err } - _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at) - values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json) - ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id) - if err == nil { - var affected int64 - affected, err = ret.RowsAffected() - if err != nil { - return err - } - if affected == 0 { - return errors.New("群信息不存在: " + id) - } + // 原本代码为: + // _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at) + // values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json) + // + // ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id) + + result := tx.Where("id = ?", id). + // 按照木落的原版代码,应该是这么个逻辑:查不到的时候能正确执行,查到了就不执行了,所以用Attrs而不是Assign + Attrs(map[string]any{ + "id": id, + // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。 + // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用) + "data": BYTE(json), + "is_hidden": true, + // 如果插入成功,原版代码接下来更新这个值,那么现在就是等价的 + "binding_sheet_id": charId, + "created_at": now, + "updated_at": now, + }). + // 按照原版代码,无论是不是能插入成功,都要更新这个值,所以这么写就是等价的了 + Assign(map[string]any{ + "binding_sheet_id": charId, + }). + FirstOrCreate(&AttributesItemModel{}) + if result.Error != nil { + tx.Rollback() // 返回错误时回滚 + return result.Error + } + // 四种情况:没有数据->初始化成功->返回1条 + // 没有数据->更新失败->返回0条 + // 有数据->更新成功->返回1条 + // 有数据->更新失败->返回0条,但理论上所有返回0条的情况应该都会被丢出去 + // 对于FirstOrCreate来说应该不会遇到下面的情况,但是保底一下 + if result.RowsAffected == 0 { + tx.Rollback() + return errors.New("群信息不存在或发生更新异常: " + id) } - return err + + // 提交事务 + return tx.Commit().Error } -func AttrsGetCharacterListByUserId(db *sqlx.DB, userId string) (lst []*AttributesItemModel, err error) { - rows, err := db.Queryx(` - select id, name, sheet_type, - (select count(id) from attrs where binding_sheet_id = t1.id) - from attrs as t1 where owner_id = $1 and is_hidden is false - `, userId) +func AttrsGetCharacterListByUserId(db *gorm.DB, userId string) ([]*AttributesItemModel, error) { + // Pinenutn: 在Gorm中,如果gorm:"-",优先级似乎很高,经过我自己测试: + // 结构体内若使用gorm="-" ,Scan将无法映射到结果中(GPT胡说八道说可以映射上,我试了半天,被骗。) + // 如果不带任何标签: GORM对结构体名称进行转换,如BindingGroupNum对应映射:binding_group_num,结果里有binding_group_num自动映射 + // 如果带上标签"column:xxxxx",则会使用指定的名称映射,如column:xxxxx对应映射xxxxx + // GPT 说带上JSON标签,可以映射到结果中,但实际上是错误的,无法映射。 + // 所以最终”BindingGroupNum“需要创建这个结构体用来临时存放结果,然后将结果映射到AttributesItemModel结构体上。 + // 在gorm="-"这里的配置还有更多可以使用无写入权限,有读权限的标签,但要求必须BindingGroupNum的结构体名称和数据库查询结果一致 + // 且不能指定columns,否则会建表,没找到更好方案。 + type AttrResult struct { + ID string `gorm:"column:id"` + Name string `gorm:"column:name"` + SheetType string `gorm:"column:sheet_type"` + BindingGroupNum int64 `gorm:"column:binding_group_num"` // 映射 COUNT(a.id) + } + var tempResultList []AttrResult + // 由于是复杂查询,无法直接使用Models,又为了防止以后attrs表名称修改,故不使用Table而是用TableName替换 + model := AttributesItemModel{} + tableName := model.TableName() + // 此处使用了JOIN来避免子查询,数据库一般对JOIN有使用索引的优化,所以有性能提升,但是我没有实际测试过性能差距。 + err := db.Table(fmt.Sprintf("%s AS t1", tableName)). + Select("t1.id, t1.name, t1.sheet_type, COUNT(a.id) AS binding_group_num"). + Joins(fmt.Sprintf("LEFT JOIN %s AS a ON a.binding_sheet_id = t1.id", tableName)). + Where("t1.owner_id = ? AND t1.is_hidden IS FALSE", userId). + Group("t1.id, t1.name, t1.sheet_type"). + // Pinenutn:此处我根据创建时间对创建的卡进行排序,不知道是否有意义? + Order("t1.created_at ASC"). + Scan(&tempResultList).Error if err != nil { return nil, err } - var items []*AttributesItemModel - for rows.Next() { - item := &AttributesItemModel{} - err := rows.Scan( - &item.Id, - &item.Name, - &item.SheetType, - &item.BindingGroupsNum, - ) - if err != nil { - return nil, err + items := make([]*AttributesItemModel, len(tempResultList)) + for i, tempResult := range tempResultList { + items[i] = &AttributesItemModel{ + Id: tempResult.ID, + Name: tempResult.Name, + SheetType: tempResult.SheetType, + BindingGroupsNum: tempResult.BindingGroupNum, } - items = append(items, item) } - return items, nil + return items, nil // 返回角色列表 } diff --git a/dice/model/backup.go b/dice/model/backup.go index a564a077..131da51a 100644 --- a/dice/model/backup.go +++ b/dice/model/backup.go @@ -1,19 +1,35 @@ package model import ( - "github.com/jmoiron/sqlx" + "strings" + + "gorm.io/gorm" ) -func Vacuum(db *sqlx.DB, path string) error { - _, err := db.Exec("vacuum into $1", path) - return err +// Vacuum 执行数据库的 vacuum 操作 +func Vacuum(db *gorm.DB, path string) error { + // 检查数据库驱动是否为 SQLite + if !strings.Contains(db.Dialector.Name(), "sqlite") { + return nil + } + + // 使用 GORM 执行 vacuum 操作,并将数据库保存到指定路径 + err := db.Exec("VACUUM INTO ?", path).Error + return err // 返回错误 } -func FlushWAL(db *sqlx.DB) error { - _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);") - if err != nil { - return err +// FlushWAL 执行 WAL 日志的检查点和内存收缩 +func FlushWAL(db *gorm.DB) error { + // 检查数据库驱动是否为 SQLite + if !strings.Contains(db.Dialector.Name(), "sqlite") { + return nil + } + + // 执行 WAL 检查点操作 + if err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil { + return err // 返回错误 } - _, err = db.Exec("PRAGMA shrink_memory") - return err + // 执行内存收缩操作 + err := db.Exec("PRAGMA shrink_memory;").Error + return err // 返回错误 } diff --git a/dice/model/ban.go b/dice/model/ban.go index 6f4d3328..755ff942 100644 --- a/dice/model/ban.go +++ b/dice/model/ban.go @@ -1,36 +1,60 @@ package model import ( - "github.com/jmoiron/sqlx" + "gorm.io/gorm" ) -func BanItemDel(db *sqlx.DB, id string) error { - _, err := db.Exec("delete from ban_info where id=$1", id) - return err +// BanInfo 模型 +// GORM STRUCT +type BanInfo struct { + ID string `gorm:"primaryKey;column:id"` // 主键列 + BanUpdatedAt int `gorm:"index:idx_ban_info_ban_updated_at;column:ban_updated_at"` // BanUpdatedAt 列 + UpdatedAt int `gorm:"index:idx_ban_info_updated_at;column:updated_at"` // UpdatedAt 列 + Data []byte `gorm:"column:data"` // BLOB 类型 } -func BanItemSave(db *sqlx.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error { - _, err := db.NamedExec("replace into ban_info (id, updated_at, ban_updated_at, data) values (:id, :updated_at, :ban_updated_at, :data)", - map[string]interface{}{ - "id": id, - "updated_at": updatedAt, - "ban_updated_at": banUpdatedAt, - "data": data, - }) - return err +func (*BanInfo) TableName() string { + return "ban_info" } -func BanItemList(db *sqlx.DB, callback func(id string, banUpdatedAt int64, data []byte)) error { - var items []struct { - ID string `db:"id"` - BanUpdatedAt int64 `db:"ban_updated_at"` - Data []byte `db:"data"` +// BanItemDel 删除指定 ID 的禁用项 +func BanItemDel(db *gorm.DB, id string) error { + // 使用 GORM 的 Delete 方法删除指定 ID 的记录 + result := db.Where("id = ?", id).Delete(&BanInfo{}) + return result.Error // 返回错误 +} + +// BanItemSave 保存或替换禁用项 这里的[]byte也是json反序列化产物 +func BanItemSave(db *gorm.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error { + // 使用 FirstOrCreate ,这里显然,第一次初始化的时候替换ID,而剩余的时候只换ID以外的数据 + if err := db.Where("id = ?", id).Attrs(map[string]any{ + "id": id, + "updated_at": int(updatedAt), + "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 + "data": BYTE(data), // 禁用项数据 + }). + Assign(map[string]any{ + "updated_at": int(updatedAt), + "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 + "data": BYTE(data), // 禁用项数据 + }).FirstOrCreate(&BanInfo{}).Error; err != nil { + return err // 返回错误 } - if err := db.Select(&items, "SELECT id, ban_updated_at, data FROM ban_info ORDER BY ban_updated_at DESC"); err != nil { - return err + return nil // 操作成功,返回 nil +} + +// BanItemList 列出所有禁用项并调用回调函数处理 +func BanItemList(db *gorm.DB, callback func(id string, banUpdatedAt int64, data []byte)) error { + var items []BanInfo + + // 使用 GORM 查询所有禁用项 + if err := db.Order("ban_updated_at DESC").Find(&items).Error; err != nil { + return err // 返回错误 } + + // 遍历每个禁用项并调用回调函数 for _, item := range items { - callback(item.ID, item.BanUpdatedAt, item.Data) + callback(item.ID, int64(item.BanUpdatedAt), item.Data) // 确保类型一致 } - return nil + return nil // 操作成功,返回 nil } diff --git a/dice/model/censor_log.go b/dice/model/censor_log.go index 745d2b3c..fb497e40 100644 --- a/dice/model/censor_log.go +++ b/dice/model/censor_log.go @@ -4,113 +4,129 @@ import ( "encoding/json" "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" "sealdice-core/dice/censor" + log "sealdice-core/utils/kratos" ) type CensorLog struct { - ID uint64 `json:"id"` - MsgType string `json:"msgType"` - UserID string `json:"userId"` - GroupID string `json:"groupId"` - Content string `json:"content"` - HighestLevel int `json:"highestLevel"` - CreatedAt int `json:"createdAt"` + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + MsgType string `json:"msgType" gorm:"column:msg_type"` + UserID string `json:"userId" gorm:"index:idx_censor_log_user_id;column:user_id"` + GroupID string `json:"groupId" gorm:"column:group_id"` + Content string `json:"content" gorm:"column:content"` + HighestLevel int `json:"highestLevel" gorm:"index:idx_censor_log_level;column:highest_level"` + CreatedAt int `json:"createdAt" gorm:"column:created_at"` + // 补充gorm有的部分: + SensitiveWords string `json:"-" gorm:"column:sensitive_words"` + ClearMark bool `json:"-" gorm:"column:clear_mark;type:bool"` } -func CensorAppend(db *sqlx.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool { - now := time.Now() - nowTimestamp := now.Unix() +func (CensorLog) TableName() string { + return "censor_log" +} + +// 添加一个敏感词记录 +func CensorAppend(db *gorm.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool { + // 获取当前时间的 Unix 时间戳 + nowTimestamp := time.Now().Unix() + // 将敏感词转换为 JSON 字符串 words, err := json.Marshal(sensitiveWords) if err != nil { return false } - _, err = db.Exec(` -INSERT INTO censor_log( - msg_type, - user_id, - group_id, - content, - sensitive_words, - highest_level, - created_at, - clear_mark -) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - msgType, userID, groupID, content, words, highestLevel, nowTimestamp, false) - - if err != nil { + // 创建 CensorLog 实例,手动设置 CreatedAt + censorLog := CensorLog{ + MsgType: msgType, + UserID: userID, + GroupID: groupID, + Content: content, + SensitiveWords: string(words), + HighestLevel: highestLevel, + CreatedAt: int(nowTimestamp), // Unix 时间戳 + ClearMark: false, + } + // 使用 GORM 的 Create 方法插入记录 + if err := db.Create(&censorLog).Error; err != nil { return false } - return err == nil + return true } -func CensorCount(db *sqlx.DB, userID string) map[censor.Level]int { +func CensorCount(db *gorm.DB, userID string) map[censor.Level]int { + // 定义要查询的不同敏感级别 levels := [5]censor.Level{censor.Ignore, censor.Notice, censor.Caution, censor.Warning, censor.Danger} - var temp int + var temp int64 res := make(map[censor.Level]int) + + // 遍历每个敏感级别并执行查询 for _, level := range levels { - _ = db.Get(&temp, `SELECT COUNT(*) FROM censor_log WHERE user_id = ? AND highest_level = ? AND clear_mark = ?`, userID, level, false) - res[level] = temp + // 使用 GORM 的链式查询 + err := db.Model(&CensorLog{}).Where("user_id = ? AND highest_level = ? AND clear_mark = ?", userID, level, false). + Count(&temp).Error + + // 如果查询出现错误,忽略并赋值为 0 + if err != nil { + res[level] = 0 + } else { + res[level] = int(temp) + } } + return res } -func CensorClearLevelCount(db *sqlx.DB, userID string, level censor.Level) { - _, _ = db.Exec(`UPDATE censor_log SET clear_mark = ? WHERE user_id = ? AND highest_level = ?`, true, userID, level) +func CensorClearLevelCount(db *gorm.DB, userID string, level censor.Level) { + // 使用 GORM 的链式查询执行批量更新 + err := db.Model(&CensorLog{}). + Where("user_id = ? AND highest_level = ?", userID, level). + Update("clear_mark", true).Error + if err != nil { + log.Error(err) + } } +// QueryCensorLog 是分页查询的参数 type QueryCensorLog struct { - PageNum int `query:"pageNum"` - PageSize int `query:"pageSize"` - UserID string `query:"userId"` - Level int `query:"level"` + PageNum int `query:"pageNum"` // 当前页码 + PageSize int `query:"pageSize"` // 每页条数 + UserID string `query:"userId"` // 用户ID + Level int `query:"level"` // 敏感级别 } -func CensorGetLogPage(db *sqlx.DB, params QueryCensorLog) (int, []CensorLog, error) { - var total int - res := make([]CensorLog, 0, params.PageSize) +// CensorGetLogPage 使用 GORM 进行分页查询 +func CensorGetLogPage(db *gorm.DB, params QueryCensorLog) (int64, []CensorLog, error) { + var total int64 + var logs []CensorLog - err := db.QueryRow("SELECT COUNT(*) FROM censor_log").Scan(&total) - if err != nil { - return 0, nil, err + // 首先统计总记录数 + query := db.Model(&CensorLog{}) + + // 如果传入了 UserID 和 Level,则添加查询条件 + if params.UserID != "" { + query = query.Where("user_id = ?", params.UserID) } - rows, err := db.Queryx(` -SELECT id, - msg_type, - user_id, - group_id, - content, - highest_level, - created_at -FROM censor_log -ORDER BY created_at DESC -LIMIT ? OFFSET ?`, params.PageSize, (params.PageNum-1)*params.PageSize) - if err != nil { + if params.Level != 0 { + query = query.Where("highest_level = ?", params.Level) + } + + // 统计符合条件的总记录数 + if err := query.Count(&total).Error; err != nil { return 0, nil, err } - defer func(rows *sqlx.Rows) { - _ = rows.Close() - }(rows) - - for rows.Next() { - log := CensorLog{} - err := rows.Scan( - &log.ID, - &log.MsgType, - &log.UserID, - &log.GroupID, - &log.Content, - &log.HighestLevel, - &log.CreatedAt, - ) - if err != nil { - return 0, nil, err - } - res = append(res, log) + + // 查询分页数据 + if err := query. + Order("created_at DESC"). // 按照创建时间倒序排列 + Limit(params.PageSize). // 限制返回条数 + Offset((params.PageNum - 1) * params.PageSize). // 偏移 + Find(&logs). // 查询数据 + Error; err != nil { + return 0, nil, err } - return total, res, nil + return total, logs, nil } diff --git a/dice/model/const.go b/dice/model/const.go new file mode 100644 index 00000000..cd2cb7ff --- /dev/null +++ b/dice/model/const.go @@ -0,0 +1,7 @@ +package model + +const ( + SQLITE = "sqlite" + MYSQL = "mysql" + POSTGRESQL = "postgres" +) diff --git a/dice/model/database/cache/gormcache.go b/dice/model/database/cache/gormcache.go new file mode 100644 index 00000000..4e964717 --- /dev/null +++ b/dice/model/database/cache/gormcache.go @@ -0,0 +1,132 @@ +package cache + +import ( + "context" + "errors" + "strconv" + "time" + + "github.com/go-gorm/caches/v4" + "github.com/spaolacci/murmur3" + "github.com/tidwall/buntdb" + "gorm.io/gorm" +) + +type buntDBCacher struct { + db *buntdb.DB +} + +func generateHashKey(key string) string { + hash := murmur3.Sum64([]byte(key)) + return strconv.FormatUint(hash, 16) // 返回十六进制字符串 +} + +// Get 从缓存中获取与给定键关联的数据。 +// 该方法接受一个上下文、一个键和一个查询对象作为参数。 +// 它首先将键转换为哈希键,然后从数据库中获取相应的值。 +// 如果键不存在于数据库中,则返回nil, nil。 +// 如果存在错误,将返回错误信息。 +// 如果成功获取数据,将返回填充了数据的查询对象。 +func (c *buntDBCacher) Get(_ context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) { + // 生成哈希键以确定缓存的位置。 + hashedKey := generateHashKey(key) + + // 尝试查找对应的关联值 + var res string + err := c.db.View(func(tx *buntdb.Tx) error { + var err error + // 从事务中获取与哈希键关联的值。 + res, err = tx.Get(hashedKey) + return err + }) + + // 如果键在数据库中不存在,记录信息并返回nil, nil。 + if errors.Is(err, buntdb.ErrNotFound) { + // 此处不得不忽略,因为这个cache的实现机理就是如此,除非修改gorm cache的源码。 + return nil, nil //nolint:nilnil + } + + // 如果发生其他错误,返回错误信息。 + if err != nil { + return nil, err + } + // 将获取到的值解码为查询对象。 + if err = q.Unmarshal([]byte(res)); err != nil { + return nil, err + } + + return q, nil +} + +// Store 方法用于将查询结果存储到缓存中。 +// 该方法接收一个上下文、一个键和一个查询对象作为参数。 +// 它首先对键进行哈希处理,然后将查询对象序列化为字节切片。 +// 序列化成功后,它将数据存储到缓存数据库中,并设置数据过期时间为5秒。 +// 参数: +// +// _ context.Context: 上下文,本例中未使用。 +// key string: 需要存储的数据的键。 +// val *caches.Query[any]: 需要存储的查询对象。 +// +// 返回值: +// +// error: 在序列化或存储过程中遇到的错误,如果没有错误则返回nil。 +func (c *buntDBCacher) Store(_ context.Context, key string, val *caches.Query[any]) error { + // 生成哈希键以确保键的均匀分布和避免潜在的键冲突。 + hashedKey := generateHashKey(key) + // 将查询对象序列化为字节切片,以便存储到缓存中。 + res, err := val.Marshal() + if err != nil { + return err + } + // 使用数据库的Update方法来原子地设置数据。 + err = c.db.Update(func(tx *buntdb.Tx) error { + // 设置键值对,并指定数据过期时间为5秒。 + _, _, err = tx.Set(hashedKey, string(res), &buntdb.SetOptions{Expires: true, TTL: time.Second * 5}) + return err + }) + + return err +} + +// Invalidate 使缓存器中的所有缓存项失效。 +// 该方法通过删除数据库中所有以 caches.IdentifierPrefix 开头的键来实现。 +// 参数: +// +// _context.Context: 未使用。 +// +// 返回值: +// +// error: 如果在使缓存项失效的过程中发生错误,则返回该错误。 +func (c *buntDBCacher) Invalidate(_ context.Context) error { + // 清理所有缓存 + err := c.db.Update(func(tx *buntdb.Tx) error { + err := tx.DeleteAll() + if err != nil { + return err + } + return nil + }) + return err +} + +func GetBuntCacheDB(db *gorm.DB) (*gorm.DB, error) { + open, err := buntdb.Open(":memory:") + if err != nil { + return nil, err + } + // Easer参数:使用ServantGo任务执行与合并库 + // ServantGo提供了一种简单且惯用的方法来合并同时运行的相同类型的任务。 + // 可以先尝试一下easer=true是否可以加速 + cachesPlugin := &caches.Caches{Conf: &caches.Config{ + Easer: true, + Cacher: &buntDBCacher{ + db: open, + }, + }} + err = db.Use(cachesPlugin) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/dice/model/database/mysql.go b/dice/model/database/mysql.go new file mode 100644 index 00000000..f4035690 --- /dev/null +++ b/dice/model/database/mysql.go @@ -0,0 +1,27 @@ +package database + +import ( + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func MySQLDBInit(dsn string) (*gorm.DB, error) { + // 构建 MySQL DSN (Data Source Name) + // 使用 GORM 连接 MySQL + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info)}) + if err != nil { + return nil, err + } + // 存疑,MYSQL是否需要使用缓存 + cacheDB, err := cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + // 返回数据库连接 + return cacheDB, nil +} diff --git a/dice/model/database/pgsql.go b/dice/model/database/pgsql.go new file mode 100644 index 00000000..aa24e54a --- /dev/null +++ b/dice/model/database/pgsql.go @@ -0,0 +1,31 @@ +package database + +import ( + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func PostgresDBInit(dsn string) (*gorm.DB, error) { + // 构建 PostgreSQL DSN (Data Source Name) + + // 使用 GORM 连接 PostgreSQL + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, err + } + + // GetBuntCacheDB 逻辑保持不变 + cacheDB, err := cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + + // 返回数据库连接 + return cacheDB, nil +} diff --git a/dice/model/database/sqlite.go b/dice/model/database/sqlite.go new file mode 100644 index 00000000..3e87ac9f --- /dev/null +++ b/dice/model/database/sqlite.go @@ -0,0 +1,38 @@ +//go:build !cgo +// +build !cgo + +package database + +import ( + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + // https://github.com/glebarez/sqlite/issues/52 尚未遇见问题,可以先考虑不使用 + // sqlDB, _ := db.DB() + // sqlDB.SetMaxOpenConns(1) + if err != nil { + return nil, err + } + // Enable Cache Mode + db, err = cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + // enable WAL mode + if useWAL { + err = db.Exec("PRAGMA journal_mode=WAL").Error + if err != nil { + return nil, err + } + } + return db, err +} diff --git a/dice/model/database/sqlite_cgo.go b/dice/model/database/sqlite_cgo.go new file mode 100644 index 00000000..40ec5679 --- /dev/null +++ b/dice/model/database/sqlite_cgo.go @@ -0,0 +1,37 @@ +//go:build cgo +// +build cgo + +package database + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { + open, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, err + } + // Enable Cache Mode + // DELETE + open, err = cache.GetBuntCacheDB(open) + if err != nil { + return nil, err + } + // enable WAL mode + if useWAL { + err = open.Exec("PRAGMA journal_mode=WAL").Error + if err != nil { + panic(err) + } + } + + return open, err +} diff --git a/dice/model/db.go b/dice/model/db.go index f1b0bd5a..902abeca 100644 --- a/dice/model/db.go +++ b/dice/model/db.go @@ -1,258 +1,119 @@ package model import ( - "fmt" - "path/filepath" + "os" + "sync" - "github.com/jmoiron/sqlx" -) + "gorm.io/gorm" -func DBCheck(dataDir string) { - checkDB := func(db *sqlx.DB) bool { - rows, err := db.Query("PRAGMA integrity_check") //nolint:execinquery - if err != nil { - return false - } - var ok bool - for rows.Next() { - var s string - if errR := rows.Scan(&s); errR != nil { - ok = false - break - } - fmt.Println(s) - if s == "ok" { - ok = true - } - } - - if errR := rows.Err(); errR != nil { - ok = false - } - return ok - } - - var ok1, ok2, ok3 bool - var dataDB *sqlx.DB - var logsDB *sqlx.DB - var censorDB *sqlx.DB - var err error + log "sealdice-core/utils/kratos" +) - dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) - dataDB, err = _SQLiteDBInit(dbDataPath, false) - if err != nil { - fmt.Println("数据库 data.db 无法打开") - } else { - ok1 = checkDB(dataDB) - dataDB.Close() - } +var ( + engine DatabaseOperator + once sync.Once + errEngineInstance error +) - dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) - logsDB, err = _SQLiteDBInit(dbDataLogsPath, false) - if err != nil { - fmt.Println("数据库 data-logs.db 无法打开") - } else { - ok2 = checkDB(logsDB) - logsDB.Close() +// initEngine 初始化数据库引擎,仅执行一次 +func initEngine() { + dbType := os.Getenv("DB_TYPE") + switch dbType { + case SQLITE: + log.Info("当前选择使用: SQLITE数据库") + engine = &SQLiteEngine{} + case MYSQL: + log.Info("当前选择使用: MYSQL数据库") + engine = &MYSQLEngine{} + case POSTGRESQL: + log.Info("当前选择使用: POSTGRESQL数据库") + engine = &PGSQLEngine{} + default: + log.Warn("未配置数据库类型,默认使用: SQLITE数据库") + engine = &SQLiteEngine{} } - dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) - censorDB, err = _SQLiteDBInit(dbDataCensorPath, false) - if err != nil { - fmt.Println("数据库 data-censor.db 无法打开") - } else { - ok3 = checkDB(censorDB) - censorDB.Close() + errEngineInstance = engine.Init() + if errEngineInstance != nil { + log.Error("数据库引擎初始化失败:", errEngineInstance) } +} - fmt.Println("数据库检查结果:") - fmt.Println("data.db:", ok1) - fmt.Println("data-logs.db:", ok2) - fmt.Println("data-censor.db:", ok3) +// getEngine 获取数据库引擎,确保只初始化一次 +func getEngine() (DatabaseOperator, error) { + once.Do(initEngine) + return engine, errEngineInstance } -func SQLiteDBInit(dataDir string) (dataDB *sqlx.DB, logsDB *sqlx.DB, err error) { - dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) - dataDB, err = _SQLiteDBInit(dbDataPath, true) +// DatabaseInit 初始化数据和日志数据库 +func DatabaseInit() (dataDB *gorm.DB, logsDB *gorm.DB, err error) { + engine, err = getEngine() if err != nil { - return + return nil, nil, err } - dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) - logsDB, err = _SQLiteDBInit(dbDataLogsPath, true) + dataDB, err = engine.DataDBInit() if err != nil { - return + return nil, nil, err } - // data建表 - texts := []string{ - ` -create table if not exists group_player_info -( - id INTEGER - primary key autoincrement, - group_id TEXT, - user_id TEXT, - name TEXT, - created_at INTEGER, - updated_at INTEGER, - last_command_time INTEGER, - auto_set_name_template TEXT, - dice_side_num TEXT -);`, - `create index if not exists idx_group_player_info_group_id on group_player_info (group_id);`, - `create index if not exists idx_group_player_info_user_id on group_player_info (user_id);`, - `create unique index if not exists idx_group_player_info_group_user on group_player_info (group_id, user_id);`, - ` -create table if not exists group_info -( - id TEXT primary key, - created_at INTEGER, - updated_at INTEGER, - data BLOB -);`, - - ` -create table if not exists ban_info -( - id TEXT primary key, - ban_updated_at INTEGER, - updated_at INTEGER, - data BLOB -);`, - `create index if not exists idx_ban_info_updated_at on ban_info (updated_at);`, - `create index if not exists idx_ban_info_ban_updated_at on ban_info (ban_updated_at);`, - - `CREATE TABLE IF NOT EXISTS endpoint_info ( -user_id TEXT PRIMARY KEY, -cmd_num INTEGER, -cmd_last_time INTEGER, -online_time INTEGER, -updated_at INTEGER -);`, - - ` -CREATE TABLE IF NOT EXISTS attrs ( - id TEXT PRIMARY KEY, - data BYTEA, - attrs_type TEXT, - - -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 - binding_sheet_id TEXT default '', - - name TEXT default '', - owner_id TEXT default '', - sheet_type TEXT default '', - is_hidden BOOLEAN default FALSE, - - created_at INTEGER default 0, - updated_at INTEGER default 0 -); -`, - `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`, - `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`, - `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`, - } - for _, i := range texts { - _, _ = dataDB.Exec(i) + logsDB, err = engine.LogDBInit() + if err != nil { + return nil, nil, err } - - // logs建表 - texts = []string{ - ` -create table if not exists logs -( - id INTEGER primary key autoincrement, - name TEXT, - group_id TEXT, - extra TEXT, - created_at INTEGER, - updated_at INTEGER, - upload_url TEXT, - upload_time INTEGER -);`, - ` -create index if not exists idx_logs_group - on logs (group_id);`, - ` -create index if not exists idx_logs_update_at - on logs (updated_at);`, - ` -create unique index if not exists idx_log_group_id_name - on logs (group_id, name);`, - // 如果log_items有更改,需同步检查migrate/convert_logs.go - ` -create table if not exists log_items -( - id INTEGER primary key autoincrement, - log_id INTEGER, - group_id TEXT, - nickname TEXT, - im_userid TEXT, - time INTEGER, - message TEXT, - is_dice INTEGER, - command_id INTEGER, - command_info TEXT, - raw_msg_id TEXT, - user_uniform_id TEXT, - removed INTEGER, - parent_id INTEGER -);`, - ` -create index if not exists idx_log_items_group_id - on log_items (log_id);`, - ` -create index if not exists idx_log_items_log_id - on log_items (log_id);`, - - `alter table logs add upload_url text;`, // 测试版特供 - `alter table logs add upload_time integer;`, + // TODO: 将这段逻辑挪移到Migrator上 + var ids []uint64 + var logItemSums []struct { + LogID uint64 + Count int64 } + logsDB.Model(&LogInfo{}).Where("size IS NULL").Pluck("id", &ids) + if len(ids) > 0 { + // 根据 LogInfo 表中的 IDs 查找对应的 LogOneItem 记录 + err = logsDB.Model(&LogOneItem{}). + Where("log_id IN ?", ids). + Group("log_id"). + Select("log_id, COUNT(*) AS count"). // 如果需要求和其他字段,可以使用 Sum + Scan(&logItemSums).Error + if err != nil { + // 错误处理 + log.Infof("Error querying LogOneItem: %v", err) + return nil, nil, err + } - for _, i := range texts { - _, _ = logsDB.Exec(i) + // 2. 更新 LogInfo 表的 Size 字段 + for _, sum := range logItemSums { + // 将求和结果更新到对应的 LogInfo 的 Size 字段 + err = logsDB.Model(&LogInfo{}). + Where("id = ?", sum.LogID). + UpdateColumn("size", sum.Count).Error // 或者是 sum.Time 等,如果要是其他字段的求和 + if err != nil { + // 错误处理 + log.Errorf("Error updating LogInfo: %v", err) + return nil, nil, err + } + } } - - return + return dataDB, logsDB, nil } -func SQLiteCensorDBInit(dataDir string) (censorDB *sqlx.DB, err error) { - path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) - if err != nil { - return - } - censorDB, err = _SQLiteDBInit(path, true) +// DBCheck 检查数据库状态 +func DBCheck() { + dbEngine, err := getEngine() if err != nil { + log.Error("数据库引擎获取失败:", err) return } - texts := []string{` -CREATE TABLE IF NOT EXISTS censor_log -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - msg_type TEXT, - user_id TEXT, - group_id TEXT, - content TEXT, - sensitive_words TEXT, - highest_level INTEGER, - created_at INTEGER, - clear_mark BOOLEAN -); -`, - ` -CREATE INDEX IF NOT EXISTS idx_censor_log_user_id - ON censor_log (user_id); -`, - ` -CREATE INDEX IF NOT EXISTS idx_censor_log_level - ON censor_log (highest_level); -`, - } + dbEngine.DBCheck() +} - for _, i := range texts { - _, _ = censorDB.Exec(i) +// CensorDBInit 初始化敏感词数据库 +func CensorDBInit() (censorDB *gorm.DB, err error) { + censorEngine, err := getEngine() + if err != nil { + return nil, err } - return + + return censorEngine.CensorDBInit() } diff --git a/dice/model/db_init.go b/dice/model/db_init.go deleted file mode 100644 index 99c78ccf..00000000 --- a/dice/model/db_init.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !cgo -// +build !cgo - -package model - -import ( - _ "github.com/glebarez/go-sqlite" - "github.com/jmoiron/sqlx" -) - -func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite", path) - if err != nil { - panic(err) - } - - // _, err = db.Exec("vacuum") - // if err != nil { - // panic(err) - // } - - // enable WAL mode - if useWAL { - _, err = db.Exec("PRAGMA journal_mode=WAL") - if err != nil { - panic(err) - } - } - - return db, err -} diff --git a/dice/model/db_init_cgo.go b/dice/model/db_init_cgo.go deleted file mode 100644 index dcf25921..00000000 --- a/dice/model/db_init_cgo.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build cgo -// +build cgo - -package model - -import ( - "github.com/jmoiron/sqlx" - _ "github.com/mattn/go-sqlite3" // sqlite3 driver -) - -func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite3", path) - if err != nil { - panic(err) - } - - // enable WAL mode - if useWAL { - _, err = db.Exec("PRAGMA journal_mode=WAL") - if err != nil { - panic(err) - } - } - - return db, err -} diff --git a/dice/model/db_utils.go b/dice/model/db_utils.go index f9ae79fd..e24eea7d 100644 --- a/dice/model/db_utils.go +++ b/dice/model/db_utils.go @@ -1,58 +1,52 @@ package model import ( + "database/sql/driver" + "errors" "fmt" - "os" - "path/filepath" - "runtime" + "strings" "sync" + "sealdice-core/dice/model/database" + log "sealdice-core/utils/kratos" "sealdice-core/utils/spinner" ) -func DBCacheDelete() bool { - // d.BaseConfig.DataDir - dataDir := "./data/default" +// BYTES类 +// 如果我们使用FirstOrCreate,不可避免的会遇到这样的问题: +// 传入的是BYTE数组,由于使用了any会被转换为[]int8,而gorm又不会处理这种数据,进而导致转换失败 +// 通过强制设置一个封装,可以确认any的类型,进而避免转换失败 - tryDelete := func(fn string) bool { - fnPath, _ := filepath.Abs(filepath.Join(dataDir, fn)) - if _, err := os.Stat(fnPath); err != nil { - // 文件不在了,就当作删除成功 - return true - } - return os.Remove(fnPath) == nil - } +// 定义一个新的类型 JSON,封装 []byte +type BYTE []byte - // 非 windows 不删缓存 - if runtime.GOOS != "windows" { - return true +// Scan 实现 sql.Scanner 接口,用于扫描数据库中的 JSON 数据 +func (j *BYTE) Scan(value interface{}) error { + // 将数据库中的值转换为 []byte + bytes, ok := value.([]byte) + if !ok { + return errors.New(fmt.Sprint("Failed to unmarshal JSON value:", value)) } - ok := true - if ok { - ok = tryDelete("data.db-shm") - } - if ok { - tryDelete("data.db-wal") - } - if ok { - tryDelete("data-logs.db-shm") - } - if ok { - tryDelete("data-logs.db-wal") - } - if ok { - tryDelete("data-censor.db-shm") - } - if ok { - tryDelete("data-censor.db-wal") + // 将 []byte 赋值给 JSON 类型的指针 + *j = bytes + return nil +} + +// Value 实现 driver.Valuer 接口,用于将 JSON 类型存储到数据库中 +func (j BYTE) Value() (driver.Value, error) { + // 如果 BYTE 数据为空,则返回 nil + if len(j) == 0 { + return nil, nil //nolint:nilnil } - return ok + // 返回原始的 []byte + return []byte(j), nil } +// DBVacuum 整理数据库 func DBVacuum() { done := make(chan interface{}, 1) - fmt.Println("开始进行数据库整理") + log.Info("开始进行数据库整理") go spinner.WithLines(done, 3, 10) defer func() { @@ -64,15 +58,29 @@ func DBVacuum() { vacuum := func(path string, wg *sync.WaitGroup) { defer wg.Done() - db, err := _SQLiteDBInit(path, true) - defer func() { _ = db.Close() }() + // 使用 GORM 初始化数据库 + vacuumDB, err := database.SQLiteDBInit(path, true) + // 数据库类型不是 SQLite 直接返回 + if !strings.Contains(vacuumDB.Dialector.Name(), "sqlite") { + return + } + defer func() { + rawdb, err2 := vacuumDB.DB() + if err2 != nil { + return + } + err = rawdb.Close() + if err != nil { + return + } + }() if err != nil { - fmt.Printf("清理 %q 时出现错误:%v", path, err) + log.Errorf("清理 %q 时出现错误:%v", path, err) return } - _, err = db.Exec("VACUUM;") + err = vacuumDB.Exec("VACUUM;").Error if err != nil { - fmt.Printf("清理 %q 时出现错误:%v", path, err) + log.Errorf("清理 %q 时出现错误:%v", path, err) } } @@ -82,5 +90,5 @@ func DBVacuum() { wg.Wait() - fmt.Println("\n数据库整理完成") + log.Info("数据库整理完成") } diff --git a/dice/model/endpoint_info.go b/dice/model/endpoint_info.go index 947f72df..df582488 100644 --- a/dice/model/endpoint_info.go +++ b/dice/model/endpoint_info.go @@ -1,54 +1,59 @@ package model import ( - "database/sql" "errors" - "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" + "gorm.io/gorm/clause" ) +var ErrEndpointInfoUIDEmpty = errors.New("user id is empty") + +// 仅修改为gorm格式 type EndpointInfo struct { - UserID string `db:"user_id"` - CmdNum int64 `db:"cmd_num"` - CmdLastTime int64 `db:"cmd_last_time"` - OnlineTime int64 `db:"online_time"` - UpdatedAt int64 `db:"updated_at"` + UserID string `gorm:"column:user_id;primaryKey"` + CmdNum int64 `gorm:"column:cmd_num;"` + CmdLastTime int64 `gorm:"column:cmd_last_time;"` + OnlineTime int64 `gorm:"column:online_time;"` + UpdatedAt int64 `gorm:"column:updated_at;"` } -var ErrEndpointInfoUIDEmpty = errors.New("user id is empty") +func (EndpointInfo) TableName() string { + return "endpoint_info" +} -func (e *EndpointInfo) Query(db *sqlx.DB) error { +func (e *EndpointInfo) Query(db *gorm.DB) error { if len(e.UserID) == 0 { return ErrEndpointInfoUIDEmpty } if db == nil { return errors.New("db is nil") } - row := db.QueryRowx( - `SELECT cmd_num, cmd_last_time, online_time, updated_at FROM endpoint_info WHERE user_id = $1`, - e.UserID, - ) - err := row.Scan(&e.CmdNum, &e.CmdLastTime, &e.OnlineTime, &e.UpdatedAt) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + + err := db.Model(&EndpointInfo{}). + Where("user_id = ?", e.UserID). + Select("cmd_num", "cmd_last_time", "online_time", "updated_at"). + Scan(&e).Error + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } + return nil } -func (e *EndpointInfo) Save(db *sqlx.DB) error { +func (e *EndpointInfo) Save(db *gorm.DB) error { + // 检查 user_id 是否为空 if len(e.UserID) == 0 { return ErrEndpointInfoUIDEmpty } - if db == nil { - return errors.New("db is nil") - } - now := time.Now().Unix() - e.UpdatedAt = now - - _, err := db.Exec( - `REPLACE INTO endpoint_info (user_id, cmd_num, cmd_last_time, online_time, updated_at) VALUES (?, ?, ?, ?, ?)`, - e.UserID, e.CmdNum, e.CmdLastTime, e.OnlineTime, e.UpdatedAt, - ) - return err + // 检查user_id冲突时更新,否则进行创建 + result := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_id"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "cmd_num", "cmd_last_time", "online_time", "updated_at", + }), + }).Create(e) + + return result.Error } diff --git a/dice/model/engine_interface.go b/dice/model/engine_interface.go new file mode 100644 index 00000000..28281f9b --- /dev/null +++ b/dice/model/engine_interface.go @@ -0,0 +1,13 @@ +package model + +import "gorm.io/gorm" + +// DatabaseOperator 本来是单独放了个文件夹的,但是由于现在所有的model都和处理逻辑在一起,如果放在单独文件夹必然会循环依赖 +// 只能放在外面 +type DatabaseOperator interface { + Init() error + DBCheck() + DataDBInit() (*gorm.DB, error) + LogDBInit() (*gorm.DB, error) + CensorDBInit() (*gorm.DB, error) +} diff --git a/dice/model/engine_mysql.go b/dice/model/engine_mysql.go new file mode 100644 index 00000000..06fbf46d --- /dev/null +++ b/dice/model/engine_mysql.go @@ -0,0 +1,163 @@ +package model + +import ( + "errors" + "fmt" + "os" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" + log "sealdice-core/utils/kratos" +) + +type MYSQLEngine struct { + DSN string + DB *gorm.DB +} + +type LogInfoHookMySQL struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + Name string `json:"name" gorm:"column:name"` + GroupID string `json:"groupId" gorm:"column:group_id"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"` + Size *int `json:"size" gorm:"<-:false"` + Extra *string `json:"-" gorm:"column:extra"` + UploadURL string `json:"-" gorm:"column:upload_url"` + UploadTime int `json:"-" gorm:"column:upload_time"` +} + +func (*LogInfoHookMySQL) TableName() string { + return "logs" +} + +type LogOneItemHookMySQL struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + LogID uint64 `json:"-" gorm:"column:log_id"` + GroupID string `gorm:"column:group_id"` + Nickname string `json:"nickname" gorm:"column:nickname"` + IMUserID string `json:"IMUserId" gorm:"column:im_userid"` + Time int64 `json:"time" gorm:"column:time"` + Message string `json:"message" gorm:"column:message"` + IsDice bool `json:"isDice" gorm:"column:is_dice"` + CommandID int64 `json:"commandId" gorm:"column:command_id"` + CommandInfo interface{} `json:"commandInfo" gorm:"-"` + CommandInfoStr string `json:"-" gorm:"column:command_info"` + RawMsgID interface{} `json:"rawMsgId" gorm:"-"` + RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id"` + UniformID string `json:"uniformId" gorm:"column:user_uniform_id"` + Channel string `json:"channel" gorm:"-"` + Removed *int `gorm:"column:removed" json:"-"` + ParentID *int `gorm:"column:parent_id" json:"-"` +} + +func (*LogOneItemHookMySQL) TableName() string { + return "log_items" +} + +// 利用前缀索引,规避索引BUG +// 创建不出来也没关系,反正MYSQL数据库 +func createIndexForLogInfo(db *gorm.DB) (err error) { + // 创建前缀索引 + // 检查并创建索引 + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_log_name") { + err = db.Exec("CREATE INDEX idx_log_name ON logs (name(20));").Error + if err != nil { + log.Errorf("创建idx_log_name索引失败,原因为 %v", err) + } + } + + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_group") { + err = db.Exec("CREATE INDEX idx_logs_group ON logs (group_id(20));").Error + if err != nil { + log.Errorf("创建idx_logs_group索引失败,原因为 %v", err) + } + } + + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_updated_at") { + err = db.Exec("CREATE INDEX idx_logs_updated_at ON logs (updated_at);").Error + if err != nil { + log.Errorf("创建idx_logs_updated_at索引失败,原因为 %v", err) + } + } + return nil +} + +func createIndexForLogOneItem(db *gorm.DB) (err error) { + // 创建前缀索引 + // 检查并创建索引 + if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_log_items_group_id") { + err = db.Exec("CREATE INDEX idx_log_items_group_id ON log_items(group_id(20))").Error + if err != nil { + log.Errorf("创建idx_logs_group索引失败,原因为 %v", err) + } + } + if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_raw_msg_id") { + err = db.Exec("CREATE INDEX idx_raw_msg_id ON log_items(raw_msg_id(20))").Error + if err != nil { + log.Errorf("创建idx_log_group_id_name索引失败,原因为 %v", err) + } + } + // MYSQL似乎不能创建前缀联合索引,放弃所有的前缀联合索引 + return nil +} + +func (s *MYSQLEngine) Init() error { + s.DSN = os.Getenv("DB_DSN") + if s.DSN == "" { + return errors.New("DB_DSN is missing") + } + var err error + s.DB, err = database.MySQLDBInit(s.DSN) + if err != nil { + return err + } + return nil +} + +// DBCheck DB检查 +func (s *MYSQLEngine) DBCheck() { + fmt.Fprintln(os.Stdout, "MYSQL 海豹不提供检查,请自行检查数据库!") +} + +// DataDBInit 初始化 +func (s *MYSQLEngine) DataDBInit() (*gorm.DB, error) { + err := s.DB.AutoMigrate( + // TODO: 这个的索引有没有必要进行修改 + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *MYSQLEngine) LogDBInit() (*gorm.DB, error) { + // logs特殊建表 + if err := s.DB.AutoMigrate(&LogInfoHookMySQL{}, &LogOneItemHookMySQL{}); err != nil { + return nil, err + } + // logs建立索引 + err := createIndexForLogInfo(s.DB) + if err != nil { + return nil, err + } + err = createIndexForLogOneItem(s.DB) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *MYSQLEngine) CensorDBInit() (*gorm.DB, error) { + // 创建基本的表结构,并通过标签定义索引 + if err := s.DB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return s.DB, nil +} diff --git a/dice/model/engine_pgsql.go b/dice/model/engine_pgsql.go new file mode 100644 index 00000000..a734840e --- /dev/null +++ b/dice/model/engine_pgsql.go @@ -0,0 +1,66 @@ +package model + +import ( + "errors" + "fmt" + "os" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" +) + +type PGSQLEngine struct { + DSN string + DB *gorm.DB +} + +func (s *PGSQLEngine) Init() error { + s.DSN = os.Getenv("DB_DSN") + if s.DSN == "" { + return errors.New("DB_DSN is missing") + } + var err error + s.DB, err = database.PostgresDBInit(s.DSN) + if err != nil { + return err + } + return nil +} + +// DBCheck DB检查 +func (s *PGSQLEngine) DBCheck() { + fmt.Fprintln(os.Stdout, "PostGRESQL 海豹不提供检查,请自行检查数据库!") +} + +// DataDBInit 初始化 +func (s *PGSQLEngine) DataDBInit() (*gorm.DB, error) { + // data建表 + err := s.DB.AutoMigrate( + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *PGSQLEngine) LogDBInit() (*gorm.DB, error) { + // logs建表 + if err := s.DB.AutoMigrate(&LogInfo{}, &LogOneItem{}); err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *PGSQLEngine) CensorDBInit() (*gorm.DB, error) { + // 创建基本的表结构,并通过标签定义索引 + if err := s.DB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return s.DB, nil +} diff --git a/dice/model/engine_sqlite.go b/dice/model/engine_sqlite.go new file mode 100644 index 00000000..4d42aee4 --- /dev/null +++ b/dice/model/engine_sqlite.go @@ -0,0 +1,291 @@ +package model + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" + log "sealdice-core/utils/kratos" +) + +type SQLiteEngine struct { + DataDir string +} + +const defaultDataDir = "./data/default" + +const createSql = ` +CREATE TABLE attrs__temp ( + id TEXT PRIMARY KEY, + data BYTEA, + attrs_type TEXT, + binding_sheet_id TEXT default '', + name TEXT default '', + owner_id TEXT default '', + sheet_type TEXT default '', + is_hidden BOOLEAN default FALSE, + created_at INTEGER default 0, + updated_at INTEGER default 0 +); +` + +func (s *SQLiteEngine) Init() error { + s.DataDir = os.Getenv("DATADIR") + if s.DataDir == "" { + log.Debug("未能发现SQLITE定义位置,使用默认data地址") + s.DataDir = defaultDataDir + } + return nil +} + +// DB检查 BUG FIXME +func (s *SQLiteEngine) DBCheck() { + dataDir := s.DataDir + checkDB := func(db *gorm.DB) bool { + rows, err := db.Raw("PRAGMA integrity_check").Rows() + if err != nil { + return false + } + defer rows.Close() + var ok bool + for rows.Next() { + var s string + if errR := rows.Scan(&s); errR != nil { + ok = false + break + } + fmt.Fprintln(os.Stdout, s) + if s == "ok" { + ok = true + } + } + + if errR := rows.Err(); errR != nil { + ok = false + } + return ok + } + + var ok1, ok2, ok3 bool + var dataDB *gorm.DB + var logsDB *gorm.DB + var censorDB *gorm.DB + var err error + + dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) + dataDB, err = database.SQLiteDBInit(dbDataPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data.db 无法打开") + } else { + ok1 = checkDB(dataDB) + db, _ := dataDB.DB() + // 关闭 + db.Close() + } + + dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) + logsDB, err = database.SQLiteDBInit(dbDataLogsPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data-logs.db 无法打开") + } else { + ok2 = checkDB(logsDB) + db, _ := logsDB.DB() + // 关闭db + db.Close() + } + + dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) + censorDB, err = database.SQLiteDBInit(dbDataCensorPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data-censor.db 无法打开") + } else { + ok3 = checkDB(censorDB) + db, _ := censorDB.DB() + // 关闭db + db.Close() + } + + fmt.Fprintln(os.Stdout, "数据库检查结果:") + fmt.Fprintln(os.Stdout, "data.db:", ok1) + fmt.Fprintln(os.Stdout, "data-logs.db:", ok2) + fmt.Fprintln(os.Stdout, "data-censor.db:", ok3) +} + +// 初始化 +func (s *SQLiteEngine) DataDBInit() (*gorm.DB, error) { + dbDataPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data.db")) + dataDB, err := database.SQLiteDBInit(dbDataPath, true) + if err != nil { + return nil, err + } + // 特殊情况建表语句处置 + tx := dataDB.Begin() + // 检查是否有这个影响的注释 + var count int64 + err = dataDB.Raw("SELECT count(*) FROM `sqlite_master` WHERE tbl_name = 'attrs' AND `sql` LIKE '%这个方法太严格了%'").Count(&count).Error + if err != nil { + tx.Rollback() + return nil, err + } + if count > 0 { + log.Warn("数据库 attrs 表结构为前置测试版本150,重建中") + // 创建临时表 + err = tx.Exec(createSql).Error + if err != nil { + tx.Rollback() + return nil, err + } + // 迁移数据 + err = tx.Exec("INSERT INTO `attrs__temp` SELECT * FROM `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + // 删除旧的表 + err = tx.Exec("DROP TABLE `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + // 改名 + err = tx.Exec("ALTER TABLE `attrs__temp` RENAME TO `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + } + + // data建表 + err = dataDB.AutoMigrate( + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return dataDB, nil +} + +func (s *SQLiteEngine) LogDBInit() (*gorm.DB, error) { + dbDataLogsPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data-logs.db")) + logsDB, err := database.SQLiteDBInit(dbDataLogsPath, true) + if err != nil { + return nil, err + } + // logs建表 + if err = logsDB.AutoMigrate(&LogInfo{}); err != nil { + return nil, err + } + + itemsAutoMigrate := false + // 用于确认是否需要重建LogOneItem数据库 + if logsDB.Migrator().HasTable(&LogOneItem{}) { + if err = logItemsSQLiteMigrate(logsDB); err != nil { + return nil, err + } + } else { + itemsAutoMigrate = true + } + if itemsAutoMigrate { + if err = logsDB.AutoMigrate(&LogOneItem{}); err != nil { + return nil, err + } + } + return logsDB, nil +} + +func (s *SQLiteEngine) CensorDBInit() (*gorm.DB, error) { + dataDir := os.Getenv("DATA_DIR") + if dataDir == "" { + dataDir = defaultDataDir + } + path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) + if err != nil { + return nil, err + } + censorDB, err := database.SQLiteDBInit(path, true) + if err != nil { + return nil, err + } + // 创建基本的表结构,并通过标签定义索引 + if err = censorDB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return censorDB, nil +} + +func logItemsSQLiteMigrate(db *gorm.DB) error { + type DBColumn struct { + Name string + Type string + } + + // 获取当前列信息 + var currentColumns []DBColumn + err := db.Raw("PRAGMA table_info(log_items)").Scan(¤tColumns).Error + if err != nil { + return err + } + + // 获取模型定义的列信息 + var modelColumns []DBColumn + stmt := &gorm.Statement{DB: db} + err = stmt.Parse(&LogOneItem{}) + if err != nil { + return err + } + for _, field := range stmt.Schema.Fields { + if field.DBName != "" { + x := db.Migrator().FullDataTypeOf(field) + col := strings.SplitN(x.SQL, " ", 2)[0] + modelColumns = append(modelColumns, DBColumn{field.DBName, strings.ToLower(col)}) + } + } + + // 比较列是否有变化 + needMigrate := false + if len(currentColumns) != len(modelColumns) { + needMigrate = true + } else { + columnMap := make(map[string]string) + for _, col := range currentColumns { + columnMap[col.Name] = strings.ToLower(col.Type) + } + + for _, col := range modelColumns { + newType := col.Type + currentType := columnMap[col.Name] + + // 特殊处理 is_dice 列,允许 bool 或 numeric 类型 + if col.Name == "is_dice" { + if currentType != "bool" && currentType != "numeric" { + needMigrate = true + break + } + continue + } + + if currentType != newType { + needMigrate = true + break + } + } + } + + // 如果需要迁移则执行 + if needMigrate { + log.Info("现在进行log_items表的迁移,如果数据库较大,会花费较长时间,请耐心等待") + log.Info("若是迁移后观察到数据库体积显著膨胀,可以关闭骰子使用 sealdice-core --vacuum 进行数据库整理,这同样会花费较长时间") + return db.AutoMigrate() + } + + return nil +} diff --git a/dice/model/group_info.go b/dice/model/group_info.go index b13230ba..3f22619a 100644 --- a/dice/model/group_info.go +++ b/dice/model/group_info.go @@ -1,142 +1,189 @@ package model import ( - "fmt" - - "github.com/jmoiron/sqlx" "golang.org/x/time/rate" + "gorm.io/gorm" + "gorm.io/gorm/clause" ds "github.com/sealdice/dicescript" + + log "sealdice-core/utils/kratos" ) -func GroupInfoListGet(db *sqlx.DB, callback func(id string, updatedAt int64, data []byte)) error { - rows, err := db.Queryx("SELECT id, updated_at, data FROM group_info") +// GroupInfo 模型 +type GroupInfo struct { + ID string `gorm:"column:id;primaryKey"` // 主键,字符串类型 + CreatedAt int `gorm:"column:created_at"` // 创建时间 + UpdatedAt *int64 `gorm:"column:updated_at"` // 更新时间,int64类型 + Data []byte `gorm:"column:data"` // BLOB 类型字段,使用 []byte 表示 +} + +func (*GroupInfo) TableName() string { + return "group_info" +} + +// GroupInfoListGet 使用 GORM 实现,遍历 group_info 表中的数据并调用回调函数 +func GroupInfoListGet(db *gorm.DB, callback func(id string, updatedAt int64, data []byte)) error { + // 创建一个保存查询结果的结构体 + var results []struct { + ID string `gorm:"column:id"` // 字段 id + UpdatedAt *int64 `gorm:"column:updated_at"` // 由于可能存在 NULL,定义为指针类型 + Data []byte `gorm:"column:data"` // 字段 data + } + + // 使用 GORM 查询 group_info 表中的 id, updated_at, data 列 + err := db.Model(&GroupInfo{}).Select("id, updated_at, data").Find(&results).Error if err != nil { + // 如果查询发生错误,返回错误信息 return err } - defer rows.Close() - for rows.Next() { - var id string + // 遍历查询结果 + for _, result := range results { var updatedAt int64 - var data []byte - - var pUpdatedAt *int64 - err = rows.Scan(&id, &pUpdatedAt, &data) - if err != nil { - fmt.Println("!!!", err.Error()) - return err + // 如果 updatedAt 是 NULL,需要跳过该字段 + if result.UpdatedAt != nil { + updatedAt = *result.UpdatedAt } - if pUpdatedAt != nil { - updatedAt = *pUpdatedAt - } - callback(id, updatedAt, data) + // 调用回调函数,传递 id, updatedAt, data + callback(result.ID, updatedAt, result.Data) } - return rows.Err() + // 返回 nil 表示操作成功 + return nil } // GroupInfoSave 保存群组信息 -func GroupInfoSave(db *sqlx.DB, groupID string, updatedAt int64, data []byte) error { - // INSERT OR REPLACE 语句可以根据是否已存在对应记录自动插入或更新记录 - _, err := db.Exec("INSERT OR REPLACE INTO group_info (id, updated_at, data) VALUES (?, ?, ?)", groupID, updatedAt, data) - return err -} - -// GroupPlayerNumGet 查询指定群组中玩家数量 -func GroupPlayerNumGet(db *sqlx.DB, groupID string) (int64, error) { - var count int64 - - // 使用Named方法绑定命名参数 - // sqlitex.ExecuteTransient(conn, `select count(id) from group_player_info where group_id=$group_id`, &sqlitex.ExecOptions{ - query, args, err := sqlx.Named("SELECT COUNT(id) FROM group_player_info WHERE group_id = :group_id", map[string]interface{}{"group_id": groupID}) - if err != nil { - return 0, err +func GroupInfoSave(db *gorm.DB, groupID string, updatedAt int64, data []byte) error { + // 使用 gorm 的 Upsert 功能实现插入或更新 + groupInfo := GroupInfo{ + ID: groupID, + UpdatedAt: &updatedAt, + Data: data, } - - // 执行查询并将结果存储到 count 变量中 - if err := db.QueryRowx(query, args...).Scan(&count); err != nil { - return 0, err - } - - return count, nil + result := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"updated_at", "data"}), + }).Create(&groupInfo) + return result.Error } // GroupPlayerInfoBase 群内玩家信息 type GroupPlayerInfoBase struct { - Name string `yaml:"name" jsbind:"name"` // 玩家昵称 - UserID string `yaml:"userId" jsbind:"userId"` - InGroup bool `yaml:"inGroup"` // 是否在群内,有时一个人走了,信息还暂时残留 - LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime"` // 上次发送指令时间 - RateLimiter *rate.Limiter `yaml:"-"` // 限速器 - RateLimitWarned bool `yaml:"-"` // 是否已经警告过限速 - AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate"` // 名片模板 + // 补充这个字段,从而保证包含主键ID + ID uint `yaml:"-" jsbind:"-" gorm:"column:id;primaryKey;autoIncrement"` // 主键ID字段,自增 + Name string `yaml:"name" jsbind:"name" gorm:"column:name"` // 玩家昵称 + UserID string `yaml:"userId" jsbind:"userId" gorm:"column:user_id;index:idx_group_player_info_user_id; uniqueIndex:idx_group_player_info_group_user"` + // 非数据库信息:是否在群内 + InGroup bool `yaml:"inGroup" gorm:"-"` // 是否在群内,有时一个人走了,信息还暂时残留 + LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime" gorm:"column:last_command_time"` // 上次发送指令时间 + // 非数据库信息 + RateLimiter *rate.Limiter `yaml:"-" gorm:"-"` // 限速器 + // 非数据库信息 + RateLimitWarned bool `yaml:"-" gorm:"-"` // 是否已经警告过限速 + AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate" gorm:"column:auto_set_name_template"` // 名片模板 // level int 权限 - DiceSideNum int `yaml:"diceSideNum"` // 面数,为0时等同于d100 - ValueMapTemp *ds.ValueMap `yaml:"-"` // 玩家的群内临时变量 + DiceSideNum int `yaml:"diceSideNum" gorm:"column:dice_side_num"` // 面数,为0时等同于d100 + // 非数据库信息 + ValueMapTemp *ds.ValueMap `yaml:"-" gorm:"-"` // 玩家的群内临时变量 // ValueMapTemp map[string]*VMValue `yaml:"-"` // 玩家的群内临时变量 - TempValueAlias *map[string][]string `yaml:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里? - - UpdatedAtTime int64 `yaml:"-" json:"-"` - RecentUsedTime int64 `yaml:"-" json:"-"` + // 非数据库信息 + TempValueAlias *map[string][]string `yaml:"-" gorm:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里? + + // 非数据库信息 + UpdatedAtTime int64 `yaml:"-" json:"-" gorm:"-"` + // 非数据库信息 + RecentUsedTime int64 `yaml:"-" json:"-" gorm:"-"` + // 缺少信息 -> 这边原来就是int吗? + CreatedAt int `yaml:"-" json:"-" gorm:"column:created_at"` // 创建时间 + UpdatedAt int `yaml:"-" json:"-" gorm:"column:updated_at"` // 更新时间 + GroupID string `yaml:"-" json:"-" gorm:"column:group_id;index:idx_group_player_info_group_id; uniqueIndex:idx_group_player_info_group_user"` } -func GroupPlayerInfoGet(db *sqlx.DB, groupID string, playerID string) *GroupPlayerInfoBase { - var ret GroupPlayerInfoBase +// 兼容设置 +func (GroupPlayerInfoBase) TableName() string { + return "group_player_info" +} - rows, err := db.NamedQuery("SELECT name, last_command_time, auto_set_name_template, dice_side_num FROM group_player_info WHERE group_id=:group_id AND user_id=:user_id", map[string]interface{}{ - "group_id": groupID, - "user_id": playerID, - }) +// GroupPlayerNumGet 获取指定群组的玩家数量 +func GroupPlayerNumGet(db *gorm.DB, groupID string) (int64, error) { + var count int64 + // 使用 GORM 的 Table 方法指定表名进行查询 + // db.Table("表名").Where("条件").Count(&count) 是通用的 GORM 用法 + // 将 group_id 作为查询条件 + err := db.Model(&GroupPlayerInfoBase{}).Where("group_id = ?", groupID).Count(&count).Error if err != nil { - fmt.Printf("error getting group player info: %s", err.Error()) - return nil + // 如果查询出现错误,返回错误信息 + return 0, err } - defer rows.Close() - - // Name: stmt.ColumnText(0), - // UserId: playerId, - // LastCommandTime: stmt.ColumnInt64(2), - // AutoSetNameTemplate: stmt.ColumnText(3), - // DiceSideNum: int(stmt.ColumnInt64(4)), - - exists := false - for rows.Next() { - exists = true - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &ret.Name, - &ret.LastCommandTime, - &ret.AutoSetNameTemplate, - &ret.DiceSideNum, - ); err != nil { - fmt.Printf("error getting group player info: %s", err.Error()) - return nil - } + // 返回统计的数量 + return count, nil +} + +// GroupPlayerInfoGet 获取指定群组中的玩家信息 +func GroupPlayerInfoGet(db *gorm.DB, groupID string, playerID string) *GroupPlayerInfoBase { + var ret GroupPlayerInfoBase + + // 使用 GORM 查询数据并绑定到结构体中 + // db.Table("表名").Where("条件").First(&ret) 查询一条数据并映射到结构体 + result := db.Model(&GroupPlayerInfoBase{}). + Where("group_id = ? AND user_id = ?", groupID, playerID). + Select("name, last_command_time, auto_set_name_template, dice_side_num"). + Limit(1). + Find(&ret) + err := result.Error + // 如果查询发生错误,打印错误并返回 nil + if err != nil { + log.Errorf("error getting group player info: %s", err.Error()) + return nil } - if !exists { + if result.RowsAffected == 0 { return nil } + + // 将 playerID 赋值给结构体中的 UserID 字段 ret.UserID = playerID + + // 返回查询结果 return &ret } -func GroupPlayerInfoSave(db *sqlx.DB, groupID string, playerID string, info *GroupPlayerInfoBase) error { - _, err := db.NamedExec("REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id) VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id)", map[string]interface{}{ +// GroupPlayerInfoSave 保存玩家信息,不再使用 REPLACE INTO 语句 +func GroupPlayerInfoSave(db *gorm.DB, info *GroupPlayerInfoBase) error { + // 考虑到info是指针,为了防止可能info还会被用到其他地方,这里的给info指针赋值也是有意义的 + // 但强烈建议将这段去除掉,数据库层面理论上不应该混杂业务层逻辑? + // 判断条件:联合主键相同 + // TODO: 那自增的ID是干嘛的…… + conditions := map[string]any{ + "user_id": info.UserID, + "group_id": info.GroupID, + } + data := map[string]any{ "name": info.Name, - "updated_at": info.UpdatedAtTime, + "user_id": info.UserID, "last_command_time": info.LastCommandTime, "auto_set_name_template": info.AutoSetNameTemplate, "dice_side_num": info.DiceSideNum, - "group_id": groupID, - "user_id": playerID, - }) - return err + "group_id": info.GroupID, + "updated_at": info.UpdatedAt, + } + // 原代码逻辑: + // REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id) + // VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id) + // 所以它是全局替换,使用Assign方法,无论如何都给我替换 + if err := db. + Where(conditions). + Assign(data).FirstOrCreate(&GroupPlayerInfoBase{}).Error; err != nil { + return err + } + + // 返回 nil 表示操作成功 + return nil } diff --git a/dice/model/log.go b/dice/model/log.go index 1732fde5..68628fad 100644 --- a/dice/model/log.go +++ b/dice/model/log.go @@ -5,10 +5,11 @@ import ( "encoding/json" "errors" "fmt" - "strings" "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" + + log "sealdice-core/utils/kratos" ) type LogOne struct { @@ -18,70 +19,139 @@ type LogOne struct { } type LogOneItem struct { - ID uint64 `json:"id" db:"id"` - Nickname string `json:"nickname" db:"nickname"` - IMUserID string `json:"IMUserId" db:"im_userid"` - Time int64 `json:"time" db:"time"` - Message string `json:"message" db:"message"` - IsDice bool `json:"isDice" db:"is_dice"` - CommandID int64 `json:"commandId" db:"command_id"` - CommandInfo interface{} `json:"commandInfo" db:"command_info"` - RawMsgID interface{} `json:"rawMsgId" db:"raw_msg_id"` - - UniformID string `json:"uniformId" db:"user_uniform_id"` - Channel string `json:"channel"` + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + LogID uint64 `json:"-" gorm:"column:log_id;index:idx_log_items_log_id"` + GroupID string `gorm:"index:idx_log_items_group_id;column:group_id;index:idx_log_delete_by_id"` + Nickname string `json:"nickname" gorm:"column:nickname"` + IMUserID string `json:"IMUserId" gorm:"column:im_userid"` + Time int64 `json:"time" gorm:"column:time"` + Message string `json:"message" gorm:"column:message"` + IsDice bool `json:"isDice" gorm:"column:is_dice"` + CommandID int64 `json:"commandId" gorm:"column:command_id"` + CommandInfo interface{} `json:"commandInfo" gorm:"-"` + CommandInfoStr string `json:"-" gorm:"column:command_info"` + // 这里的RawMsgID 真的什么都有可能 + RawMsgID interface{} `json:"rawMsgId" gorm:"-"` + RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id;index:idx_raw_msg_id;index:idx_log_delete_by_id"` + UniformID string `json:"uniformId" gorm:"column:user_uniform_id"` + // 数据库里没有的 + Channel string `json:"channel" gorm:"-"` + // 数据库里有,JSON里没有的 + // 允许default=NULL + Removed *int `gorm:"column:removed" json:"-"` + ParentID *int `gorm:"column:parent_id" json:"-"` +} + +// 兼容旧版本的数据库设计 +func (*LogOneItem) TableName() string { + return "log_items" +} + +// BeforeSave 钩子函数: 查询前,interface{}转换为json +func (item *LogOneItem) BeforeSave(_ *gorm.DB) (err error) { + // 将 CommandInfo 转换为 JSON 字符串保存到 CommandInfoStr + if item.CommandInfo != nil { + if data, err := json.Marshal(item.CommandInfo); err == nil { + item.CommandInfoStr = string(data) + } else { + return err + } + } + + // 将 RawMsgID 转换为 string 字符串,保存到 RawMsgIDStr + if item.RawMsgID != nil { + item.RawMsgIDStr = fmt.Sprintf("%v", item.RawMsgID) + } + + return nil +} + +// AfterFind 钩子函数: 查询后,interface{}转换为json +func (item *LogOneItem) AfterFind(_ *gorm.DB) (err error) { + // 将 CommandInfoStr 从 JSON 字符串反序列化为 CommandInfo + if item.CommandInfoStr != "" { + if err := json.Unmarshal([]byte(item.CommandInfoStr), &item.CommandInfo); err != nil { + return err + } + } + + // 将 RawMsgIDStr string 直接赋值给 RawMsgID + if item.RawMsgIDStr != "" { + item.RawMsgID = item.RawMsgIDStr + } + + return nil } type LogInfo struct { - ID uint64 `json:"id" db:"id"` - Name string `json:"name" db:"name"` - GroupID string `json:"groupId" db:"groupId"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - UpdatedAt int64 `json:"updatedAt" db:"updated_at"` - Size int `json:"size" db:"size"` + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + Name string `json:"name" gorm:"index:idx_log_group_id_name,unique"` + GroupID string `json:"groupId" gorm:"index:idx_logs_group;index:idx_log_group_id_name,unique"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at;index:idx_logs_update_at"` + // 允许数据库NULL值 + // 原版代码中,此处标记了db:size,但实际上,该列并不存在。 + // 考虑到该处数据将会为未来log查询提供优化手段,保留该结构体定义,但不使用。 + // 使用GORM:<-:false 无写入权限,这样它就不会建库,但请注意,下面LogGetLogPage处,如果你查询出的名称不是size + // 不能在这里绑定column,因为column会给你建立那一列。 + // TODO: 将这个字段使用上会不会比后台查询就JOIN更合适? + Size *int `json:"size" gorm:"column:size"` + // 数据库里有,json不展示的 + // 允许数据库NULL值(该字段当前不使用) + Extra *string `json:"-" gorm:"column:extra"` + // 原本标记为:测试版特供,由于原代码每次都会执行,故直接启用此处column记录。 + UploadURL string `json:"-" gorm:"column:upload_url"` // 测试版特供 + UploadTime int `json:"-" gorm:"column:upload_time"` // 测试版特供 +} + +func (*LogInfo) TableName() string { + return "logs" } -func LogGetInfo(db *sqlx.DB) ([]int, error) { +// LogGetInfo 查询日志简略信息,使用通用函数替代SQLITE专属函数 +func LogGetInfo(db *gorm.DB) ([]int, error) { lst := []int{0, 0, 0, 0} - err := db.Get(&lst[0], "SELECT seq FROM sqlite_sequence WHERE name == 'logs'") + + var maxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值 + var itemsMaxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值 + // 获取 logs 表的记录数和最大 ID + err := db.Model(&LogInfo{}).Select("COUNT(*)").Scan(&lst[2]).Error if err != nil { return nil, err } - err = db.Get(&lst[1], "SELECT seq FROM sqlite_sequence WHERE name == 'log_items'") + + err = db.Model(&LogInfo{}).Select("MAX(id)").Scan(&maxID).Error if err != nil { return nil, err } - err = db.Get(&lst[2], "SELECT COUNT(*) FROM logs") + lst[0] = int(maxID.Int64) + + // 获取 log_items 表的记录数和最大 ID + err = db.Model(&LogOneItem{}).Select("COUNT(*)").Scan(&lst[3]).Error if err != nil { return nil, err } - err = db.Get(&lst[3], "SELECT COUNT(*) FROM log_items") + + err = db.Model(&LogOneItem{}).Select("MAX(id)").Scan(&itemsMaxID).Error if err != nil { return nil, err } + lst[1] = int(itemsMaxID.Int64) + return lst, nil } // Deprecated: replaced by page -func LogGetLogs(db *sqlx.DB) ([]*LogInfo, error) { +func LogGetLogs(db *gorm.DB) ([]*LogInfo, error) { var lst []*LogInfo - rows, err := db.Queryx("SELECT id,name,group_id,created_at, updated_at FROM logs") - if err != nil { + + // 使用 GORM 查询 logs 表 + if err := db.Model(&LogInfo{}). + Select("id, name, group_id, created_at, updated_at"). + Find(&lst).Error; err != nil { return nil, err } - for rows.Next() { - log := &LogInfo{} - if err := rows.Scan( - &log.ID, - &log.Name, - &log.GroupID, - &log.CreatedAt, - &log.UpdatedAt, - ); err != nil { - return nil, err - } - lst = append(lst, log) - } + return lst, nil } @@ -95,181 +165,138 @@ type QueryLogPage struct { } // LogGetLogPage 获取分页 -func LogGetLogPage(db *sqlx.DB, param *QueryLogPage) (int, []*LogInfo, error) { - countQuery := `SELECT count(*) FROM logs` - query := ` -SELECT logs.id as id, - logs.name as name, - logs.group_id as group_id, - logs.created_at as created_at, - logs.updated_at as updated_at, - count(logs.id) as size -FROM logs - LEFT JOIN log_items items ON logs.id = items.log_id -` - var conditions []string +func LogGetLogPage(db *gorm.DB, param *QueryLogPage) (int, []*LogInfo, error) { + var lst []*LogInfo + + // 构建基础查询 + query := db.Model(&LogInfo{}).Select("logs.id, logs.name, logs.group_id, logs.created_at, logs.updated_at,COALESCE(logs.size, 0) as size").Order("logs.updated_at desc") + // 添加条件 if param.Name != "" { - conditions = append(conditions, "logs.name like '%' || :name || '%'") + query = query.Where("logs.name LIKE ?", "%"+param.Name+"%") } if param.GroupID != "" { - conditions = append(conditions, "logs.group_id like '%' || :group_id || '%'") + query = query.Where("logs.group_id LIKE ?", "%"+param.GroupID+"%") } if param.CreatedTimeBegin != "" { - conditions = append(conditions, "logs.created_at >= :created_time_begin") + query = query.Where("logs.created_at >= ?", param.CreatedTimeBegin) } if param.CreatedTimeEnd != "" { - conditions = append(conditions, "logs.created_at <= :created_time_end") - } - if len(conditions) > 0 { - where := " WHERE " + strings.Join(conditions, " AND ") - query += where - countQuery += where + query = query.Where("logs.created_at <= ?", param.CreatedTimeEnd) } - query += fmt.Sprintf(" GROUP BY logs.id LIMIT %d, %d", (param.PageNum-1)*param.PageSize, param.PageSize) - - var total int - count, err := db.NamedQuery(countQuery, param) - if err != nil { - return 0, nil, err - } - count.Next() - err = count.Scan(&total) - if err != nil { + // 获取总数 + var count int64 + if err := db.Model(&LogInfo{}).Count(&count).Error; err != nil { return 0, nil, err } - lst := make([]*LogInfo, 0, param.PageSize) - rows, err := db.NamedQuery(query, param) - if err != nil { + // 分页查询 + query = query.Group("logs.id").Limit(param.PageSize).Offset((param.PageNum - 1) * param.PageSize) + + // 执行查询 + if err := query.Scan(&lst).Error; err != nil { return 0, nil, err } - for rows.Next() { - log := &LogInfo{} - if err := rows.Scan( - &log.ID, - &log.Name, - &log.GroupID, - &log.CreatedAt, - &log.UpdatedAt, - &log.Size, - ); err != nil { - return 0, nil, err - } - lst = append(lst, log) - } - return total, lst, nil + + return int(count), lst, nil } // LogGetList 获取列表 -func LogGetList(db *sqlx.DB, groupID string) ([]string, error) { +func LogGetList(db *gorm.DB, groupID string) ([]string, error) { var lst []string - err := db.Select(&lst, "SELECT name FROM logs WHERE group_id = $1 ORDER BY updated_at DESC", groupID) - if err != nil { + + // 执行查询 + if err := db.Model(&LogInfo{}). + Select("name"). + Where("group_id = ?", groupID). + Order("updated_at DESC"). + Pluck("name", &lst).Error; err != nil { return nil, err } + return lst, nil } // LogGetIDByGroupIDAndName 获取ID -func LogGetIDByGroupIDAndName(db *sqlx.DB, groupID string, logName string) (logID int64, err error) { - err = db.Get(&logID, "SELECT id FROM logs WHERE group_id = $1 AND name = $2", groupID, logName) +func LogGetIDByGroupIDAndName(db *gorm.DB, groupID string, logName string) (logID uint64, err error) { + err = db.Model(&LogInfo{}). + Select("id"). + Where("group_id = ? AND name = ?", groupID, logName). + Scan(&logID).Error + if err != nil { // 如果出现错误,判断是否没有找到对应的记录 - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, gorm.ErrRecordNotFound) { return 0, nil } return 0, err } + return logID, nil } -func LogGetUploadInfo(db *sqlx.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) { - res, err := db.Queryx( - `SELECT updated_at, upload_url, upload_time FROM logs WHERE group_id = $1 AND name = $2`, - groupID, logName, - ) - if err != nil { - return "", 0, 0, err +// LogGetUploadInfo 获取上传信息 +func LogGetUploadInfo(db *gorm.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) { + var logInfo struct { + UpdatedAt int64 `gorm:"column:updated_at"` + UploadURL string `gorm:"column:upload_url"` + UploadTime int64 `gorm:"column:upload_time"` } - defer func() { _ = res.Close() }() - for res.Next() { - err = res.Scan(&updateTime, &url, &uploadTime) - if err != nil { - return "", 0, 0, err - } + err = db.Model(&LogInfo{}). + Select("updated_at, upload_url, upload_time"). + Where("group_id = ? AND name = ?", groupID, logName). + Scan(&logInfo).Error + + if err != nil { + return "", 0, 0, err } + // 提取结果 + updateTime = logInfo.UpdatedAt + url = logInfo.UploadURL + uploadTime = logInfo.UploadTime return } -func LogSetUploadInfo(db *sqlx.DB, groupID string, logName string, url string) error { +// LogSetUploadInfo 设置上传信息 +func LogSetUploadInfo(db *gorm.DB, groupID string, logName string, url string) error { if len(url) == 0 { return nil } now := time.Now().Unix() - _, err := db.Exec( - `UPDATE logs SET upload_url = $1, upload_time = $2 WHERE group_id = $3 AND name = $4`, - url, now, groupID, logName, - ) + // 使用 GORM 更新上传信息 + err := db.Model(&LogInfo{}).Where("group_id = ? AND name = ?", groupID, logName). + Update("upload_url", url). + Update("upload_time", now). + Error + return err } // LogGetAllLines 获取log的所有行数据 -func LogGetAllLines(db *sqlx.DB, groupID string, logName string) ([]*LogOneItem, error) { +func LogGetAllLines(db *gorm.DB, groupID string, logName string) ([]*LogOneItem, error) { // 获取log的ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return nil, err } - // 查询行数据 - rows, err := db.Queryx(`SELECT id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id - FROM log_items WHERE log_id=$1 ORDER BY time ASC`, logID) - if err != nil { - return nil, err - } - defer func(rows *sqlx.Rows) { - _ = rows.Close() - }(rows) - - var ret []*LogOneItem - for rows.Next() { - item := &LogOneItem{} - var commandInfoStr []byte - - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &item.ID, - &item.Nickname, - &item.IMUserID, - &item.Time, - &item.Message, - &item.IsDice, - &item.CommandID, - &commandInfoStr, - &item.RawMsgID, - &item.UniformID, - ); err != nil { - return nil, err - } - - // 反序列化commandInfo - if commandInfoStr != nil { - _ = json.Unmarshal(commandInfoStr, &item.CommandInfo) - } + var items []*LogOneItem - ret = append(ret, item) - } + // 查询行数据 + err = db.Model(&LogOneItem{}). + Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id"). + Where("log_id = ?", logID). + Order("time ASC"). + Find(&items).Error - if err := rows.Err(); err != nil { + if err != nil { return nil, err } - - return ret, nil + return items, nil } type QueryLogLinePage struct { @@ -280,75 +307,33 @@ type QueryLogLinePage struct { } // LogGetLinePage 获取log的行分页 -func LogGetLinePage(db *sqlx.DB, param *QueryLogLinePage) ([]*LogOneItem, error) { +func LogGetLinePage(db *gorm.DB, param *QueryLogLinePage) ([]*LogOneItem, error) { // 获取log的ID logID, err := LogGetIDByGroupIDAndName(db, param.GroupID, param.LogName) if err != nil { return nil, err } + var items []*LogOneItem + // 查询行数据 - rows, err := db.Queryx(` -SELECT id, - nickname, - im_userid, - time, - message, - is_dice, - command_id, - command_info, - raw_msg_id, - user_uniform_id -FROM log_items -WHERE log_id =$1 -ORDER BY time ASC -LIMIT $2, $3;`, logID, (param.PageNum-1)*param.PageSize, param.PageSize) + err = db.Model(&LogOneItem{}). + Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id"). + Where("log_id = ?", logID). + Order("time ASC"). + Limit(param.PageSize). + Offset((param.PageNum - 1) * param.PageSize). + Scan(&items).Error if err != nil { return nil, err } - defer func(rows *sqlx.Rows) { - _ = rows.Close() - }(rows) - - var ret []*LogOneItem - for rows.Next() { - item := &LogOneItem{} - var commandInfoStr []byte - - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &item.ID, - &item.Nickname, - &item.IMUserID, - &item.Time, - &item.Message, - &item.IsDice, - &item.CommandID, - &commandInfoStr, - &item.RawMsgID, - &item.UniformID, - ); err != nil { - return nil, err - } - - // 反序列化commandInfo - if commandInfoStr != nil { - _ = json.Unmarshal(commandInfoStr, &item.CommandInfo) - } - - ret = append(ret, item) - } - - if err := rows.Err(); err != nil { - return nil, err - } - return ret, nil + return items, nil } // LogLinesCountGet 获取日志行数 -func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) { +func LogLinesCountGet(db *gorm.DB, groupID string, logName string) (int64, bool) { // 获取日志 ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil || logID == 0 { @@ -357,9 +342,10 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) // 获取日志行数 var count int64 - err = db.Get(&count, ` - SELECT COUNT(id) FROM log_items WHERE log_id=$1 AND removed IS NULL - `, logID) + err = db.Model(&LogOneItem{}). + Where("log_id = ? and removed IS NULL", logID). + Count(&count).Error + if err != nil { return 0, false } @@ -368,17 +354,16 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) } // LogDelete 删除log -func LogDelete(db *sqlx.DB, groupID string, logName string) bool { - // 获取 log id +func LogDelete(db *gorm.DB, groupID string, logName string) bool { + // 获取 log ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil || logID == 0 { return false } - // 获取文本 - // 通过BeginTxx方法开启事务 - tx, err := db.Beginx() - if err != nil { + // 开启事务 + tx := db.Begin() + if err = tx.Error; err != nil { return false } defer func() { @@ -387,41 +372,38 @@ func LogDelete(db *sqlx.DB, groupID string, logName string) bool { } }() - // 删除log_id相关的log_items记录 - _, err = tx.Exec("DELETE FROM log_items WHERE log_id = $1", logID) - if err != nil { + // 删除 log_id 相关的 log_items 记录 + if err = tx.Where("log_id = ?", logID).Delete(&LogOneItem{}).Error; err != nil { return false } - // 删除log_id相关的logs记录 - _, err = tx.Exec("DELETE FROM logs WHERE id = $1", logID) - if err != nil { + // 删除 log_id 相关的 logs 记录 + if err = tx.Where("id = ?", logID).Delete(&LogInfo{}).Error; err != nil { return false } // 提交事务 - err = tx.Commit() + err = tx.Commit().Error return err == nil } // LogAppend 向指定的log中添加一条信息 -func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem) bool { - // 获取 log id +func LogAppend(db *gorm.DB, groupID string, logName string, logItem *LogOneItem) bool { + // 获取 log ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return false } - // 如果不存在,创建 + // 获取当前时间戳 now := time.Now() nowTimestamp := now.Unix() // 开始事务 - tx, err := db.Beginx() - if err != nil { + tx := db.Begin() + if err = tx.Error; err != nil { return false } - // 执行事务时发生错误时回滚 defer func() { if err != nil { _ = tx.Rollback() @@ -429,65 +411,86 @@ func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem) }() if logID == 0 { - // 创建一个新的log - query := "INSERT INTO logs (name, group_id, created_at, updated_at) VALUES (?, ?, ?, ?)" - rst, errNew := tx.Exec(query, logName, groupID, nowTimestamp, nowTimestamp) - if errNew != nil { - return false - } - // 获取新创建log的ID - logID, errNew = rst.LastInsertId() - if errNew != nil { + // 创建一个新的 log + newLog := LogInfo{Name: logName, GroupID: groupID, CreatedAt: nowTimestamp, UpdatedAt: nowTimestamp} + if err = tx.Create(&newLog).Error; err != nil { return false } + logID = newLog.ID } - // 向log_items表中添加一条信息 - data, err := json.Marshal(logItem.CommandInfo) - query := "INSERT INTO log_items (log_id, group_id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + // 向 log_items 表中添加一条信息 + // Pinenutn: 由此可以推知,CommandInfo必然是一个 map[string]interface{} - rid := "" - if logItem.RawMsgID != nil { - rid = fmt.Sprintf("%v", logItem.RawMsgID) + if err != nil { + return false } - // fmt.Println("log append", logId, rid, "|", groupId, logName) - _, err = tx.Exec(query, logID, groupID, logItem.Nickname, logItem.IMUserID, nowTimestamp, logItem.Message, logItem.IsDice, logItem.CommandID, data, rid, logItem.UniformID) - _, err = tx.Exec("UPDATE logs SET updated_at = ? WHERE id = ?", nowTimestamp, logID) - if err != nil { + newLogItem := LogOneItem{ + LogID: logID, + GroupID: groupID, + Nickname: logItem.Nickname, + IMUserID: logItem.IMUserID, + Time: nowTimestamp, + Message: logItem.Message, + IsDice: logItem.IsDice, + CommandID: logItem.CommandID, + CommandInfo: logItem.CommandInfo, + RawMsgID: logItem.RawMsgID, + UniformID: logItem.UniformID, + } + + if err = tx.Create(&newLogItem).Error; err != nil { + return false + } + + // 更新 logs 表中的 updated_at 字段 和 size 字段 + if err = tx.Model(&LogInfo{}). + Where("id = ?", logID). + Updates(map[string]interface{}{ + "updated_at": nowTimestamp, + "size": gorm.Expr("COALESCE(size, 0) + ?", 1), + }).Error; err != nil { return false } // 提交事务 - err = tx.Commit() + err = tx.Commit().Error return err == nil } // LogMarkDeleteByMsgID 撤回删除 -func LogMarkDeleteByMsgID(db *sqlx.DB, groupID string, logName string, rawID interface{}) error { +func LogMarkDeleteByMsgID(db *gorm.DB, groupID string, logName string, rawID interface{}) error { // 获取 log id logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return err } - - // 删除记录 - rid := "" - if rawID != nil { - rid = fmt.Sprintf("%v", rawID) + rid := fmt.Sprintf("%v", rawID) + tx := db.Begin() + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + if err = tx.Where("log_id = ? AND raw_msg_id = ?", logID, rid).Delete(&LogOneItem{}).Error; err != nil { + log.Errorf("log delete error %s", err.Error()) + return err } - - // fmt.Printf("log delete %v %d\n", rawId, logId) - _, err = db.Exec("DELETE FROM log_items WHERE log_id=? AND raw_msg_id=?", logID, rid) - if err != nil { - fmt.Println("log delete error", err.Error()) + // 更新 logs 表中的 updated_at 字段 和 size 字段 + // 真的有默认为NULL还能触发删除的情况吗?! + if err = tx.Model(&LogInfo{}).Where("id = ?", logID).Updates(map[string]interface{}{ + "updated_at": time.Now().Unix(), + "size": gorm.Expr("COALESCE(size, 0) - ?", 1), + }).Error; err != nil { return err } - - return nil + err = tx.Commit().Error + return err } -func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID interface{}) error { +// LogEditByMsgID 编辑日志 +func LogEditByMsgID(db *gorm.DB, groupID, logName, newContent string, rawID interface{}) error { logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return err @@ -498,11 +501,11 @@ func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID inte rid = fmt.Sprintf("%v", rawID) } - _, err = db.Exec(`UPDATE log_items -SET message = ? -WHERE log_id = ? AND raw_msg_id = ?`, newContent, logID, rid) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { + // 更新 log_items 表中的内容 + if err := db.Model(&LogOneItem{}). + Where("log_id = ? AND raw_msg_id = ?", logID, rid). + Update("message", newContent).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return fmt.Errorf("log edit: %w", err) diff --git a/dice/platform_adapter_gocq.go b/dice/platform_adapter_gocq.go index 223afc58..fad8f55b 100644 --- a/dice/platform_adapter_gocq.go +++ b/dice/platform_adapter_gocq.go @@ -2,6 +2,7 @@ package dice import ( "encoding/json" + "errors" "fmt" "math/rand" "os" @@ -14,15 +15,18 @@ import ( "syscall" "time" - "github.com/gorilla/websocket" "github.com/labstack/echo/v4" - "github.com/sacOO7/gowebsocket" - "github.com/samber/lo" - "go.uber.org/zap" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "gopkg.in/yaml.v3" "sealdice-core/message" + log "sealdice-core/utils/kratos" "sealdice-core/utils/procs" + + "github.com/gorilla/websocket" + "github.com/sacOO7/gowebsocket" + "github.com/samber/lo" ) // 0 默认 1登录中 2登录中-二维码 3登录中-滑条 4登录中-手机验证码 10登录成功 11登录失败 @@ -96,6 +100,8 @@ type PlatformAdapterGocq struct { riskAlertShieldCount int // 风控警告屏蔽次数,一个临时变量 useArrayMessage bool `yaml:"-"` // 使用分段消息 lagrangeRebootTimes int + SignServerVer string `yaml:"signServerVer" json:"signServerVer"` // 用于前端显示 + SignServerName string `yaml:"signServerName" json:"signServerName"` // 用于前端显示 } type Sender struct { @@ -273,38 +279,61 @@ func FormatDiceIDQQChGroup(guildID, channelID string) string { return fmt.Sprintf("QQ-CH-Group:%s-%s", guildID, channelID) } -func tryParseOneBot11ArrayMessage(log *zap.SugaredLogger, message string, writeTo *MessageQQ) error { - msgQQType2 := new(MessageQQArray) - err := json.Unmarshal([]byte(message), msgQQType2) +func hasURLScheme(text string) bool { + // 正则表达式匹配三种情况:file URI、http(s) URL 和 base64 URI + regex := `^[a-z]+://` + match, _ := regexp.MatchString(regex, text) + return match +} - if err != nil { +func tryParseOneBot11ArrayMessage(log *log.Helper, message string, writeTo *MessageQQ) error { + // 不合法的信息体 + if !gjson.Valid(message) { log.Warn("无法解析 onebot11 字段:", message) - return err + return errors.New("解析失败") } - + // 原版本转换为gjson对象 + parseContent := gjson.Parse(message) + arrayContent := parseContent.Get("message").Array() cqMessage := strings.Builder{} - for _, i := range msgQQType2.Message { - switch i.Type { + for _, i := range arrayContent { + // 使用String()方法,如果为空,会自动产生空字符串 + typeStr := i.Get("type").String() + dataObj := i.Get("data") + switch typeStr { case "text": - cqMessage.WriteString(i.Data["text"].(string)) + cqMessage.WriteString(dataObj.Get("text").String()) case "image": - cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", i.Data["file"])) + // 兼容NC情况, 此时file字段只有文件名, 完整URL在url字段 + if !hasURLScheme(dataObj.Get("file").String()) && hasURLScheme(dataObj.Get("url").String()) { + cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", dataObj.Get("url").String())) + } else { + cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", dataObj.Get("file").String())) + } case "face": // 兼容四叶草,移除 .(string)。自动获取的信息表示此类型为 float64,这是go解析的问题 - cqMessage.WriteString(fmt.Sprintf("[CQ:face,id=%v]", i.Data["id"])) + cqMessage.WriteString(fmt.Sprintf("[CQ:face,id=%v]", dataObj.Get("id").String())) case "record": - cqMessage.WriteString(fmt.Sprintf("[CQ:record,file=%v]", i.Data["file"])) + cqMessage.WriteString(fmt.Sprintf("[CQ:record,file=%v]", dataObj.Get("file").String())) case "at": - cqMessage.WriteString(fmt.Sprintf("[CQ:at,qq=%v]", i.Data["qq"])) + cqMessage.WriteString(fmt.Sprintf("[CQ:at,qq=%v]", dataObj.Get("qq").String())) case "poke": cqMessage.WriteString("[CQ:poke]") case "reply": - cqMessage.WriteString(fmt.Sprintf("[CQ:reply,id=%v]", i.Data["id"])) + cqMessage.WriteString(fmt.Sprintf("[CQ:reply,id=%v]", dataObj.Get("id").String())) } } - writeTo.MessageQQBase = msgQQType2.MessageQQBase - writeTo.Message = cqMessage.String() + // 赋值对应的Message + tempStr, err := sjson.Set(parseContent.String(), "message", cqMessage.String()) + if err != nil { + return err + } + // 返回被转换成结构体的结果 + err = json.Unmarshal([]byte(tempStr), &writeTo) + if err != nil { + return err + } return nil } @@ -329,8 +358,14 @@ func OneBot11CqMessageToArrayMessage(longText string) []interface{} { // 将 CQ 拼入数组 switch cq.Type { case "image": - i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["file"]}} - arr = append(arr, i) + // 兼容NC情况, 此时file字段只有文件名, 完整URL在url字段 + if !hasURLScheme(cq.Args["file"]) && hasURLScheme(cq.Args["url"]) { + i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["url"]}} + arr = append(arr, i) + } else { + i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["file"]}} + arr = append(arr, i) + } case "record": i := OneBotV11ArrMsgItem[OneBotV11MsgItemRecordType]{Type: "record", Data: OneBotV11MsgItemRecordType{File: cq.Args["file"]}} arr = append(arr, i) @@ -366,7 +401,7 @@ func (pa *PlatformAdapterGocq) SendSegmentToPerson(ctx *MsgContext, userID strin } func (pa *PlatformAdapterGocq) Serve() int { - if pa.BuiltinMode == "lagrange" { + if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" { pa.Implementation = "lagrange" } else { pa.Implementation = "gocq" @@ -429,9 +464,9 @@ func (pa *PlatformAdapterGocq) Serve() int { // log.Info("...", message) // } if strings.Contains(message, `"guild_id"`) { - // log.Info("!!!", message, s.Parent.WorkInQQChannel) + // log.Info("!!!", message, s.Parent.Config.WorkInQQChannel) // 暂时忽略频道消息 - if s.Parent.WorkInQQChannel { + if s.Parent.Config.WorkInQQChannel { pa.QQChannelTrySolve(message) } return @@ -529,9 +564,9 @@ func (pa *PlatformAdapterGocq) Serve() int { // 处理被强制拉群的情况 uid := groupInfo.InviteUserID - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { // 如果是被ban之后拉群,判定为强制拉群 if groupInfo.EnteredTime > 0 && groupInfo.EnteredTime > banInfo.BanTime { text := fmt.Sprintf("本次入群为遭遇强制邀请,即将主动退群,因为邀请人%s正处于黑名单上。打扰各位还请见谅。感谢使用海豹核心。", groupInfo.InviteUserID) @@ -544,7 +579,7 @@ func (pa *PlatformAdapterGocq) Serve() int { } // 强制拉群情况2 - 群在黑名单 - banInfo, ok = ctx.Dice.BanList.GetByID(groupID) + banInfo, ok = ctx.Dice.Config.BanList.GetByID(groupID) if ok { if banInfo.Rank == BanRankBanned { // 如果是被ban之后拉群,判定为强制拉群 @@ -560,7 +595,7 @@ func (pa *PlatformAdapterGocq) Serve() int { } else { // TODO: 这玩意的创建是个专业活,等下来弄 // session.ServiceAtNew[groupId] = GroupInfo{} - fmt.Println("TODO create group") + log.Debug("TODO create group") } // 这句话太吵了 // log.Debug("群信息刷新: ", msgQQ.Data.GroupName) @@ -593,9 +628,9 @@ func (pa *PlatformAdapterGocq) Serve() int { tempInviteMap2[msg.GroupID] = uid // 邀请人在黑名单上 - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "黑名单") return } @@ -603,13 +638,13 @@ func (pa *PlatformAdapterGocq) Serve() int { // 信任模式,如果不是信任,又不是master则拒绝拉群邀请 isMaster := ctx.Dice.IsMaster(uid) - if ctx.Dice.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { + if ctx.Dice.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "只允许骰主设置信任的人拉群") return } // 群在黑名单上 - banInfo, ok = ctx.Dice.BanList.GetByID(msg.GroupID) + banInfo, ok = ctx.Dice.Config.BanList.GetByID(msg.GroupID) if ok { if banInfo.Rank == BanRankBanned { pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "群黑名单") @@ -617,7 +652,7 @@ func (pa *PlatformAdapterGocq) Serve() int { } } - if ctx.Dice.RefuseGroupInvite { + if ctx.Dice.Config.RefuseGroupInvite { pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "设置拒绝加群") return } @@ -645,7 +680,7 @@ func (pa *PlatformAdapterGocq) Serve() int { comment = strings.ReplaceAll(comment, "\u00a0", "") } - toMatch := strings.TrimSpace(session.Parent.FriendAddComment) + toMatch := strings.TrimSpace(session.Parent.Config.FriendAddComment) willAccept := comment == DiceFormat(ctx, toMatch) if toMatch == "" { willAccept = true @@ -666,7 +701,7 @@ func (pa *PlatformAdapterGocq) Serve() int { if len(m2) == len(items) { ok := true - for i := 0; i < len(m2); i++ { + for i := range m2 { if m2[i] != items[i] { ok = false break @@ -685,9 +720,9 @@ func (pa *PlatformAdapterGocq) Serve() int { // 检查黑名单 extra := "" uid := FormatDiceIDQQ(string(msgQQ.UserID)) - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { if willAccept { extra = "。回答正确,但为被禁止用户,准备自动拒绝" } else { @@ -924,7 +959,7 @@ func (pa *PlatformAdapterGocq) Serve() int { skip := false skipReason := "" - banInfo, ok := ctx.Dice.BanList.GetByID(opUID) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(opUID) if ok { if banInfo.Rank == 30 { skip = true @@ -940,7 +975,7 @@ func (pa *PlatformAdapterGocq) Serve() int { if skip { extra = fmt.Sprintf("\n取消处罚,原因为%s", skipReason) } else { - ctx.Dice.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx) } txt := fmt.Sprintf("被踢出群: 在QQ群组<%s>(%s)中被踢出,操作者:<%s>(%s)%s", groupName, msgQQ.GroupID, userName, msgQQ.OperatorID, extra) @@ -958,6 +993,18 @@ func (pa *PlatformAdapterGocq) Serve() int { // {"group_id":564808710,"notice_type":"group_decrease","operator_id":2589922907,"post_type":"notice","self_id":2589922907,"sub_type":"leave","time":1651584460,"user_id":2589922907} groupName := dm.TryGetGroupName(msg.GroupID) txt := fmt.Sprintf("离开群组或群解散: <%s>(%s)", groupName, msgQQ.GroupID) + // 这个就是要删除的部分,离开这个群组=群组退出=删除对应的群聊绑定信息(也就是用户的骰子和这个群聊无关了) + // 同时考虑到:QQ群团队发布公告称,由于业务调整,“恢复QQ群”功能将于2023年10月13日起正式下线,届时涉及QQ群相关的恢复功能都将无法使用,可以安心删除群聊对应绑定信息。 + group, exists := session.ServiceAtNew.Load(msg.GroupID) + if !exists { + txtErr := fmt.Sprintf("离开群组或群解散,删除对应群聊信息失败: <%s>(%s)", groupName, msgQQ.GroupID) + log.Error(txtErr) + ctx.Notice(txtErr) + return + } + // TODO:存疑,根据DISMISS的代码复制而来 + group.DiceIDExistsMap.Delete(ep.UserID) + group.UpdatedAtTime = time.Now().Unix() log.Info(txt) ctx.Notice(txt) return @@ -971,7 +1018,7 @@ func (pa *PlatformAdapterGocq) Serve() int { groupName := dm.TryGetGroupName(msg.GroupID) userName := dm.TryGetUserName(opUID) - ctx.Dice.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx) txt := fmt.Sprintf("被禁言: 在群组<%s>(%s)中被禁言,时长%d秒,操作者:<%s>(%s)", groupName, msgQQ.GroupID, msgQQ.Duration, userName, msgQQ.OperatorID) log.Info(txt) ctx.Notice(txt) @@ -992,7 +1039,7 @@ func (pa *PlatformAdapterGocq) Serve() int { if pa.riskAlertShieldCount > 0 { pa.riskAlertShieldCount-- } else { - fmt.Println("群消息发送失败: 账号可能被风控") + log.Warn("群消息发送失败: 账号可能被风控") _ = ctx.Dice.SendMail("群消息发送失败: 账号可能被风控", MailTypeCIAMLock) } } @@ -1002,7 +1049,7 @@ func (pa *PlatformAdapterGocq) Serve() int { // {"post_type":"notice","notice_type":"notify","time":1672489767,"self_id":2589922907,"sub_type":"poke","group_id":131687852,"user_id":303451945,"sender_id":303451945,"target_id":2589922907} // 检查设置中是否开启 - if !ctx.Dice.QQEnablePoke { + if !ctx.Dice.Config.QQEnablePoke { return } @@ -1050,21 +1097,15 @@ func (pa *PlatformAdapterGocq) Serve() int { } session.Execute(ep, msg, false) } else { - fmt.Println("Received message " + message) + log.Debug("Received message " + message) } } - socket.OnBinaryMessage = func(data []byte, socket gowebsocket.Socket) { - log.Debug("Recieved binary data ", data) - } + socket.OnBinaryMessage = func(_ /* data */ []byte, _ /* socket */ gowebsocket.Socket) {} - socket.OnPingReceived = func(data string, socket gowebsocket.Socket) { - log.Debug("Recieved ping " + data) - } + socket.OnPingReceived = func(_ /* data */ string, _ /* socket */ gowebsocket.Socket) {} - socket.OnPongReceived = func(data string, socket gowebsocket.Socket) { - log.Debug("Recieved pong " + data) - } + socket.OnPongReceived = func(_ /* data */ string, _ /* socket */ gowebsocket.Socket) {} var lastDisconnect int64 socket.OnDisconnected = func(err error, socket gowebsocket.Socket) { @@ -1182,7 +1223,7 @@ func (pa *PlatformAdapterGocq) DoRelogin() bool { if pa.InPackGoCqhttpDisconnectedCH != nil { pa.InPackGoCqhttpDisconnectedCH <- -1 } - if pa.BuiltinMode == "lagrange" { + if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" { myDice.Logger.Infof("重新启动 lagrange 进程,对应账号: <%s>(%s)", ep.Nickname, ep.UserID) pa.CurLoginIndex++ pa.GoCqhttpState = StateCodeInit @@ -1229,7 +1270,7 @@ func (pa *PlatformAdapterGocq) SetEnable(enable bool) { c.Enable = true if pa.UseInPackClient { - if pa.BuiltinMode == "lagrange" { + if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" { BuiltinQQServeProcessKill(d, c) time.Sleep(1 * time.Second) LagrangeServe(d, c, LagrangeLoginInfo{ diff --git a/dice/platform_adapter_gocq_actions.go b/dice/platform_adapter_gocq_actions.go index 0963a629..aa1fb291 100644 --- a/dice/platform_adapter_gocq_actions.go +++ b/dice/platform_adapter_gocq_actions.go @@ -139,6 +139,7 @@ func socketSendText(socket *gowebsocket.Socket, s string) { }() if socket != nil { + // 什么也不做,这样就能用来做不发话的测试 socket.SendText(s) } } @@ -159,8 +160,8 @@ func socketSendBinary(socket *gowebsocket.Socket, data []byte) { //nolint func doSleepQQ(ctx *MsgContext) { if ctx.Dice != nil { d := ctx.Dice - offset := d.MessageDelayRangeEnd - d.MessageDelayRangeStart - time.Sleep(time.Duration((d.MessageDelayRangeStart + rand.Float64()*offset) * float64(time.Second))) + offset := d.Config.MessageDelayRangeEnd - d.Config.MessageDelayRangeStart + time.Sleep(time.Duration((d.Config.MessageDelayRangeStart + rand.Float64()*offset) * float64(time.Second))) } else { time.Sleep(time.Duration((0.4 + rand.Float64()/2) * float64(time.Second))) } @@ -197,6 +198,18 @@ func (pa *PlatformAdapterGocq) SendToPerson(ctx *MsgContext, userID string, text text = textAssetsConvert(text) texts := textSplit(text) + + for index, subText := range texts { + re := regexp.MustCompile(`\[CQ:poke,qq=(\d+)\]`) + + if re.MatchString(subText) { + re = regexp.MustCompile(`\d+`) + qq := re.FindStringSubmatch(subText) + pa.FriendPoke(qq[0]) + texts = append(texts[:index], texts[index+1:]...) + } + } + for _, subText := range texts { a, _ := json.Marshal(oneBotCommand{ Action: "send_msg", @@ -211,6 +224,41 @@ func (pa *PlatformAdapterGocq) SendToPerson(ctx *MsgContext, userID string, text } } +type PokeStruct struct { + UserID int64 `json:"user_id"` + GroupID int64 `json:"group_id,omitempty"` +} + +func (pa *PlatformAdapterGocq) FriendPoke(userId string) { + userID, _ := strconv.ParseInt(userId, 10, 64) + + text, _ := json.Marshal(oneBotCommand{ + Action: "friend_poke", + Params: PokeStruct{ + UserID: userID, + }, + }) + s := string(text) + + socketSendText(pa.Socket, s) +} + +func (pa *PlatformAdapterGocq) GroupPoke(ctx *MsgContext, userId string) { + groupId := strings.ReplaceAll(ctx.Group.GroupID, "QQ-Group:", "") + groupID, _ := strconv.ParseInt(groupId, 10, 64) + userID, _ := strconv.ParseInt(userId, 10, 64) + + text, _ := json.Marshal(oneBotCommand{ + Action: "group_poke", + Params: PokeStruct{ + UserID: userID, + GroupID: groupID, + }, + }) + s := string(text) + socketSendText(pa.Socket, s) +} + func (pa *PlatformAdapterGocq) SendToGroup(ctx *MsgContext, groupID string, text string, flag string) { if groupID == "" { return @@ -254,6 +302,17 @@ func (pa *PlatformAdapterGocq) SendToGroup(ctx *MsgContext, groupID string, text text = textAssetsConvert(text) texts := textSplit(text) + for index, subText := range texts { + re := regexp.MustCompile(`\[CQ:poke,qq=(\d+)\]`) + + if re.MatchString(subText) { + re = regexp.MustCompile(`\d+`) + qq := re.FindStringSubmatch(subText) + pa.GroupPoke(ctx, qq[0]) + texts = append(texts[:index], texts[index+1:]...) + } + } + for index, subText := range texts { var a []byte if pa.useArrayMessage { @@ -626,7 +685,6 @@ func textSplit(input string) []string { input = input[0:span[0]] + input[span[1]:] } } - splits := utils.SplitLongText(input, 2000, utils.DefaultSplitPaginationHint) splits = append(splits, poke...) diff --git a/dice/platform_adapter_gocq_channel.go b/dice/platform_adapter_gocq_channel.go index 2adc9f6c..dcf7a41b 100644 --- a/dice/platform_adapter_gocq_channel.go +++ b/dice/platform_adapter_gocq_channel.go @@ -2,7 +2,6 @@ package dice import ( "encoding/json" - "fmt" "strings" "sealdice-core/dice/model" @@ -88,8 +87,6 @@ func (pa *PlatformAdapterGocq) QQChannelTrySolve(message string) { // fmt.Println("Recieved message1 " + message) session.Execute(ep, msg, false) - } else { - fmt.Println("CH Recieved message " + message) } } // pa.SendToChannelGroup(ctx, msg.GroupId, msg.Message+"asdasd", "") diff --git a/dice/platform_adapter_gocq_helper.go b/dice/platform_adapter_gocq_helper.go index 0a7f121e..1d5ba3b3 100644 --- a/dice/platform_adapter_gocq_helper.go +++ b/dice/platform_adapter_gocq_helper.go @@ -12,15 +12,16 @@ import ( "regexp" "runtime" "runtime/debug" + "strconv" "strings" "time" "github.com/ShiraazMoollatjie/goluhn" "github.com/acarl005/stripansi" "github.com/google/uuid" - "go.uber.org/zap" "sealdice-core/utils" + log "sealdice-core/utils/kratos" "sealdice-core/utils/procs" ) @@ -74,7 +75,7 @@ func RandString(n int) string { r := rand.New(rand.NewSource(time.Now().Unix())) bytes := make([]byte, n) - for i := 0; i < n; i++ { + for i := range n { b := r.Intn(26) + 65 bytes[i] = byte(b) } @@ -380,8 +381,8 @@ servers: ` func GenerateConfig(qq int64, port int, info GoCqhttpLoginInfo) string { - ret := strings.ReplaceAll(defaultConfig, "{WS端口}", fmt.Sprintf("%d", port)) - ret = strings.Replace(ret, "{QQ帐号}", fmt.Sprintf("%d", qq), 1) + ret := strings.ReplaceAll(defaultConfig, "{WS端口}", strconv.Itoa(port)) + ret = strings.Replace(ret, "{QQ帐号}", strconv.FormatInt(qq, 10), 1) ret = strings.Replace(ret, "{QQ密码}", info.Password, 1) if info.UseSignServer && info.SignServerConfig != nil { @@ -820,7 +821,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi pa.GoCqhttpLoginCaptcha = "" go func() { // 检查是否有短信验证码 - for i := 0; i < 100; i++ { + for range 100 { if pa.GoCqhttpState != GoCqhttpStateCodeInLoginBar { break } @@ -844,7 +845,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi pa.GoCqhttpLoginVerifyCode = "" go func() { // 检查是否有短信验证码 - for i := 0; i < 100; i++ { + for range 100 { if pa.GoCqhttpState != GoCqhttpStateCodeInLoginVerifyCode { break } @@ -881,7 +882,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi if strings.Contains(line, "请使用手机QQ扫描二维码以继续登录") { // TODO - fmt.Println("请使用手机QQ扫描二维码以继续登录") + log.Info("请使用手机QQ扫描二维码以继续登录") } if (pa.IsLoginSuccessed() && strings.Contains(line, "[ERROR]:") && strings.Contains(line, "Protocol -> sendPacket msg error: 120")) || strings.Contains(line, "账号可能被风控####2测试触发语句") { @@ -930,11 +931,11 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi if !skip { dice.Logger.Infof("onebot | %s", stripansi.Strip(line)) } else if strings.HasSuffix(line, "\n") { - fmt.Printf("onebot | %s", line) + dice.Logger.Infof("onebot | %s", line[:len(line)-1]) } } else { if strings.HasSuffix(line, "\n") { - fmt.Printf("onebot | %s", line) + dice.Logger.Infof("onebot | %s", line[:len(line)-1]) } skip := false @@ -963,7 +964,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi <-chQrCode if _, err := os.Stat(qrcodeFile); err == nil { dice.Logger.Info("onebot: 二维码已经就绪") - fmt.Println("如控制台二维码不好扫描,可以手动打开 ./data/default/extra/go-cqhttp-qqXXXXX 目录下qrcode.png") + fmt.Fprintln(os.Stdout, "如控制台二维码不好扫描,可以手动打开 ./data/default/extra/go-cqhttp-qqXXXXX 目录下qrcode.png") qrdata, err := os.ReadFile(qrcodeFile) if err == nil { pa.GoCqhttpState = StateCodeInLoginQrCode @@ -1036,7 +1037,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi var isGocqDownloading = false -func downloadGoCqhttp(logger *zap.SugaredLogger) { +func downloadGoCqhttp(logger *log.Helper) { fn := "go-cqhttp/go-cqhttp" if runtime.GOOS == "windows" { fn += ".exe" diff --git a/dice/platform_adapter_gocq_helper_others.go b/dice/platform_adapter_gocq_helper_others.go index 9e04044e..be107d06 100644 --- a/dice/platform_adapter_gocq_helper_others.go +++ b/dice/platform_adapter_gocq_helper_others.go @@ -8,7 +8,7 @@ import "os" type ProcessExitGroup uintptr func NewProcessExitGroup() (ProcessExitGroup, error) { - return 0, nil + return 0, nil //nolint:nilnil } func (g ProcessExitGroup) Dispose() error { diff --git a/dice/platform_adapter_kook.go b/dice/platform_adapter_kook.go index 55c669f4..c01d3193 100644 --- a/dice/platform_adapter_kook.go +++ b/dice/platform_adapter_kook.go @@ -412,6 +412,9 @@ func (pa *PlatformAdapterKook) SendSegmentToPerson(ctx *MsgContext, userID strin } func (pa *PlatformAdapterKook) SendToPerson(ctx *MsgContext, userID string, text string, flag string) { + if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 { + return + } channel, err := pa.IntentSession.UserChatCreate(ExtractKookUserID(userID)) if err != nil { pa.Session.Parent.Logger.Errorf("创建Kook用户#%s的私聊频道时出错:%s", userID, err) @@ -430,6 +433,9 @@ func (pa *PlatformAdapterKook) SendToPerson(ctx *MsgContext, userID string, text } func (pa *PlatformAdapterKook) SendToGroup(ctx *MsgContext, groupID string, text string, flag string) { + if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 { + return + } pa.SendToChannelRaw(ExtractKookChannelID(groupID), text, false) pa.Session.OnMessageSend(ctx, &Message{ Platform: "KOOK", @@ -444,6 +450,9 @@ func (pa *PlatformAdapterKook) SendToGroup(ctx *MsgContext, groupID string, text } func (pa *PlatformAdapterKook) SendFileToPerson(_ *MsgContext, userID string, path string, _ string) { + if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 { + return + } channel, err := pa.IntentSession.UserChatCreate(ExtractKookUserID(userID)) if err != nil { pa.Session.Parent.Logger.Errorf("创建Kook用户#%s的私聊频道时出错:%s", userID, err) @@ -453,6 +462,9 @@ func (pa *PlatformAdapterKook) SendFileToPerson(_ *MsgContext, userID string, pa } func (pa *PlatformAdapterKook) SendFileToGroup(_ *MsgContext, groupID string, path string, _ string) { + if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 { + return + } pa.SendFileToChannelRaw(ExtractKookChannelID(groupID), path, false) } @@ -474,6 +486,9 @@ func (pa *PlatformAdapterKook) EditMessage(ctx *MsgContext, msgID, message strin func (pa *PlatformAdapterKook) RecallMessage(ctx *MsgContext, msgID string) { // TODO: not tested + if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 { + return + } _ = pa.IntentSession.MessageDelete(msgID) } diff --git a/dice/platform_adapter_lagrange_helper.go b/dice/platform_adapter_lagrange_helper.go index 1827b9e5..62c98856 100644 --- a/dice/platform_adapter_lagrange_helper.go +++ b/dice/platform_adapter_lagrange_helper.go @@ -4,23 +4,29 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "runtime/debug" + "strconv" "strings" + "sync" "time" "github.com/google/uuid" + "gopkg.in/yaml.v3" + log "sealdice-core/utils/kratos" "sealdice-core/utils/procs" ) type LagrangeLoginInfo struct { UIN int64 - SignServerUrl string + SignServerName string SignServerVersion string IsAsyncRun bool } @@ -30,19 +36,23 @@ func lagrangeGetWorkDir(dice *Dice, conn *EndPointInfo) string { return workDir } -func NewLagrangeConnectInfoItem(account string) *EndPointInfo { +func NewLagrangeConnectInfoItem(account string, isGocq bool) *EndPointInfo { conn := new(EndPointInfo) conn.ID = uuid.New().String() conn.Platform = "QQ" conn.ProtocolType = "onebot" conn.Enable = false conn.RelWorkDir = "extra/lagrange-qq" + account - conn.Adapter = &PlatformAdapterGocq{ EndPoint: conn, UseInPackClient: true, BuiltinMode: "lagrange", } + + if isGocq { + conn.RelWorkDir = "extra/lagrange-gocq-qq" + account + conn.Adapter.(*PlatformAdapterGocq).BuiltinMode = "lagrange-gocq" + } return conn } @@ -53,11 +63,15 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) loginIndex := pa.CurLoginIndex pa.GoCqhttpState = StateCodeInLogin - if pa.UseInPackClient && pa.BuiltinMode == "lagrange" { //nolint:nestif - log := dice.Logger + if pa.UseInPackClient && (pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq") { //nolint:nestif + helper := log.NewCustomHelper(log.LOG_LAGR, false, nil) if dice.ContainerMode { - log.Warn("onebot: 尝试启动内置客户端,但内置客户端在容器模式下被禁用") + if pa.BuiltinMode == "lagrange" { + helper.Warn("onebot: 尝试启动内置客户端,但内置客户端在容器模式下被禁用") + } else { + helper.Warn("onebot: 尝试启动内置gocq,但内置gocq在容器模式下被禁用") + } conn.State = 3 pa.GoCqhttpState = StateCodeLoginFailed dice.Save(false) @@ -68,40 +82,66 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) _ = os.MkdirAll(workDir, 0o755) wd, _ := os.Getwd() exeFilePath, _ := filepath.Abs(filepath.Join(wd, "lagrange/Lagrange.OneBot")) + qrcodeFilePath := filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.UserID[3:])) + configFilePath := filepath.Join(workDir, "appsettings.json") + appinfoFilePath := filepath.Join(workDir, "appinfo.json") + + if pa.BuiltinMode == "lagrange-gocq" { + exeFilePath, _ = filepath.Abs(filepath.Join(wd, "lagrange/go-cqhttp")) + qrcodeFilePath = filepath.Join(workDir, "qrcode.png") + configFilePath = filepath.Join(workDir, "config.yml") + appinfoFilePath = filepath.Join(workDir, "data/versions/7.json") + } + exeFilePath = filepath.ToSlash(exeFilePath) // windows平台需要这个替换 if runtime.GOOS == "windows" { exeFilePath += ".exe" } - qrcodeFilePath := filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.UserID[3:])) - configFilePath := filepath.Join(workDir, "appsettings.json") if _, err := os.Stat(qrcodeFilePath); err == nil { // 如果已经存在二维码文件,将其删除 _ = os.Remove(qrcodeFilePath) - } else { - // 如果找不到二维码文件,有一种可能是用户添加账号时写错了账号,这里做个兼容让错误的账号依旧能获取到二维码 - qrcodeFilePath = filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.RelWorkDir[17:])) - if _, err := os.Stat(qrcodeFilePath); err == nil { - _ = os.Remove(qrcodeFilePath) - } } - log.Info("onebot: 删除已存在的二维码文件") + helper.Info("onebot: 删除已存在的二维码文件") // 创建配置文件 pa.ConnectURL = "" if file, err := os.ReadFile(configFilePath); err == nil { var result map[string]interface{} - if err := json.Unmarshal(file, &result); err == nil { - if val, ok := result["Implementations"].([]interface{})[0].(map[string]interface{})["Port"].(float64); ok { - pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", int(val)) + if pa.BuiltinMode == "lagrange" { + if err := json.Unmarshal(file, &result); err == nil { + if val, ok := result["Implementations"].([]interface{})[0].(map[string]interface{})["Port"].(float64); ok { + pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", int(val)) + } + } + } else { + if err := yaml.Unmarshal(file, &result); err == nil { + if val, ok := result["servers"].([]interface{})[0].(map[string]interface{})["ws"].(map[string]interface{})["address"].(string); ok { + pa.ConnectURL = fmt.Sprintf("ws://%s", val) + } } } } if pa.ConnectURL == "" { p, _ := GetRandomFreePort() pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", p) - c := GenerateLagrangeConfig(p, loginInfo.SignServerUrl, loginInfo.SignServerVersion, conn) - _ = os.WriteFile(configFilePath, []byte(c), 0o644) + // 这里是为了防止用户手动删除配置,但数据库里还存有账号 + if loginInfo.SignServerName == "" { + loginInfo.SignServerName = pa.SignServerName + } + if loginInfo.SignServerVersion == "" { + loginInfo.SignServerVersion = pa.SignServerVer + } + // 生成appinfo和signserverurl写入文件 + a, c := GenerateLagrangeConfig(p, loginInfo.SignServerName, loginInfo.SignServerVersion, dice, conn) + if a != nil { + dir := filepath.Dir(appinfoFilePath) + if _, err := os.Stat(dir); err != nil { + _ = os.MkdirAll(dir, 0o755) + } + _ = os.WriteFile(appinfoFilePath, a, 0o644) + } + _ = os.WriteFile(configFilePath, c, 0o644) } if pa.GoCqhttpProcess != nil { @@ -116,7 +156,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) if runtime.GOOS == "android" { for i, s := range os.Environ() { if strings.HasPrefix(s, "RUNNER_PATH=") { - log.Infof("RUNNER_PATH: %s", os.Environ()[i][12:]) + helper.Infof("RUNNER_PATH: %s", os.Environ()[i][12:]) command = os.Environ()[i][12:] break } @@ -127,7 +167,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) } else { command = fmt.Sprintf(`"%s"`, exeFilePath) } - log.Info("onebot: 正在启动 onebot 客户端…… ", command) + helper.Info("onebot: 正在启动 onebot 客户端…… ", command) conn.State = 2 conn.Enable = true p := procs.NewProcess(command) @@ -143,7 +183,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) if loginIndex != pa.CurLoginIndex { // 当前连接已经无用,进程自杀 if !isSelfKilling { - log.Infof("检测到新的连接序号 %d,当前连接 %d 将自动退出", pa.CurLoginIndex, loginIndex) + helper.Infof("检测到新的连接序号 %d,当前连接 %d 将自动退出", pa.CurLoginIndex, loginIndex) // 注: 这里不要调用kill isSelfKilling = true _ = p.Stop() @@ -153,16 +193,24 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) // 登录中 if pa.IsInLogin() { + qrcodeSignal := "QrCode Fetched" + onlineSignal := "Bot Online: " + qrcodeExpiredSignal := "QrCode Expired, Please Fetch QrCode Again" + if pa.BuiltinMode == "lagrange-gocq" { + qrcodeSignal = "请使用手机QQ扫描二维码" + onlineSignal = "登录成功" + qrcodeExpiredSignal = "二维码过期" + } // 读取二维码 - if strings.Contains(line, "QrCode Fetched") { + if strings.Contains(line, qrcodeSignal) { chQrCode <- 1 } // 登录成功 - if strings.Contains(line, "Success") || strings.Contains(line, "Bot Online: ") { + if strings.Contains(line, "Success") || strings.Contains(line, onlineSignal) || strings.Contains(line, "Bot Uin:") { pa.GoCqhttpState = StateCodeLoginSuccessed pa.GoCqhttpLoginSucceeded = true - log.Infof("onebot: 登录成功,账号:<%s>(%s)", conn.Nickname, conn.UserID) + helper.Infof("onebot: 登录成功,账号:<%s>(%s)", conn.Nickname, conn.UserID) dice.LastUpdatedTime = time.Now().Unix() dice.Save(false) isPrintLog = false @@ -172,23 +220,23 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) go ServeQQ(dice, conn) } - if strings.Contains(line, "QrCode Expired, Please Fetch QrCode Again") { + if strings.Contains(line, qrcodeExpiredSignal) { // 二维码过期,登录失败,杀掉进程 pa.GoCqhttpState = StateCodeLoginFailed - log.Infof("onebot: 二维码过期,登录失败,账号:%s", conn.UserID) + helper.Infof("onebot: 二维码过期,登录失败,账号:%s", conn.UserID) BuiltinQQServeProcessKill(dice, conn) } } if _type == "stderr" { - log.Error("onebot | ", line) + helper.Error("onebot | ", line) } else { isPrint := isPrintLog || pa.ForcePrintLog || strings.HasPrefix(line, "warn:") if isPrint { - log.Warn("onebot | ", line) + helper.Warn("onebot | ", line) } if regFatal.MatchString(line) { - log.Error("onebot | ", line) + helper.Error("onebot | ", line) } } @@ -199,12 +247,12 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) <-chQrCode time.Sleep(3 * time.Second) if _, err := os.Stat(qrcodeFilePath); err == nil { - log.Info("onebot: 二维码已就绪") + helper.Info("onebot: 二维码已就绪") qrdata, err := os.ReadFile(qrcodeFilePath) if err == nil { pa.GoCqhttpState = StateCodeInLoginQrCode pa.GoCqhttpQrcodeData = qrdata - log.Info("onebot: 读取二维码成功") + helper.Info("onebot: 读取二维码成功") dice.LastUpdatedTime = time.Now().Unix() dice.Save(false) } else { @@ -213,7 +261,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) pa.GocqhttpLoginFailedReason = "读取二维码失败" dice.LastUpdatedTime = time.Now().Unix() dice.Save(false) - log.Infof("onebot: 读取二维码失败:%s", err) + helper.Infof("onebot: 读取二维码失败:%s", err) } } }() @@ -221,7 +269,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) run := func() { defer func() { if r := recover(); r != nil { - log.Errorf("onebot: 异常: %v 堆栈: %v", r, string(debug.Stack())) + helper.Errorf("onebot: 异常: %v 堆栈: %v", r, string(debug.Stack())) } }() @@ -249,7 +297,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) } if err != nil { - log.Info("lagrange 进程异常退出: ", err) + helper.Info("lagrange 进程异常退出: ", err) pa.GoCqhttpState = StateCodeLoginFailed var exitErr *exec.ExitError @@ -259,21 +307,21 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) case 137: // Failed to create CoreCLR, HRESULT: 0x8007054F // +++ exited with 137 +++ - log.Info("你的设备尚未被支持,请等待后续更新。") + helper.Info("你的设备尚未被支持,请等待后续更新。") case 134: // Resource temporarily unavailable // System.Net.Dns.GetHostEntryOrAddressesCore(String hostName, Boolean justAddresses, AddressFamily addressFamily, Int64 startingTimestamp) - log.Info("当前网络无法进行域名解析,请更换网络。") + helper.Info("当前网络无法进行域名解析,请更换网络。") default: if time.Now().Unix()-processStartTime < 10 { - log.Info("进程在启动后10秒内即退出,请检查配置是否正确") + helper.Info("进程在启动后10秒内即退出,请检查配置是否正确") } else { if pa.lagrangeRebootTimes > 5 { - log.Info("自动重启次数达到上限,放弃") + helper.Info("自动重启次数达到上限,放弃") } else { pa.lagrangeRebootTimes++ if conn.Enable { - log.Info("5秒后,尝试对其进行重启") + helper.Info("5秒后,尝试对其进行重启") time.Sleep(5 * time.Second) } if conn.Enable { @@ -284,7 +332,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) } } } else { - log.Info("lagrange 进程退出") + helper.Info("lagrange 进程退出") } } @@ -301,74 +349,42 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo) } } -var defaultLagrangeConfig = ` -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "SignServerUrl": "{NTSignServer地址}", - "Account": { - "Uin": {账号UIN}, - "Password": "", - "Protocol": "Linux", - "AutoReconnect": true, - "GetOptimumServer": true - }, - "Message": { - "IgnoreSelf": true, - "StringPost": false - }, - "QrCode": { - "ConsoleCompatibilityMode": false - }, - "Implementations": [ - { - "Type": "ForwardWebSocket", - "Host": "127.0.0.1", - "Port": {WS端口}, - "HeartBeatInterval": 5000, - "AccessToken": "" - } - ] -} -` - // 在构建时注入 -var defaultNTSignServer = `https://lwxmagic.sealdice.com/api/sign` -var lagrangeNTSignServer = "https://sign.lagrangecore.org/api/sign" - -func GenerateLagrangeConfig(port int, signServerUrl string, signServerVersion string, info *EndPointInfo) string { - switch signServerUrl { - case "": - signServerUrl = defaultNTSignServer - if signServerVersion != "" && signServerVersion != "13107" { - signServerUrl += "/" + signServerVersion - } - case "sealdice": - signServerUrl = defaultNTSignServer - if signServerVersion != "" && signServerVersion != "13107" { - signServerUrl += "/" + signServerVersion - } - case "lagrange": - signServerUrl = lagrangeNTSignServer - if signServerVersion != "" && signServerVersion != "13107" { - signServerUrl += "/" + signServerVersion +// var defaultNTSignServer = `https://lwxmagic.sealdice.com/api/sign` +// var lagrangeNTSignServer = "https://sign.lagrangecore.org/api/sign" + +func GenerateLagrangeConfig(port int, signServerName string, signServerVersion string, dice *Dice, info *EndPointInfo) ([]byte, []byte) { + var appinfo []byte + var signServerUrl string + pa := info.Adapter.(*PlatformAdapterGocq) + if signServerVersion == "自定义" { + appinfo, _ = lagrangeGetAppinfoFromSignServer(signServerName) + signServerUrl = signServerName + } else { + if len(signInfoGlobal) == 0 { + _, _ = LagrangeGetSignInfo(dice) } + appinfo, signServerUrl = lagrangeGetSignSeverFromInfo(signServerVersion, signServerName) + } + conf := strings.ReplaceAll(defaultLagrangeConfig, "{WS端口}", strconv.Itoa(port)) + if pa.BuiltinMode == "lagrange-gocq" { + conf = strings.ReplaceAll(defaultLagrangeGocqConfig, "{WS端口}", strconv.Itoa(port)) } - conf := strings.ReplaceAll(defaultLagrangeConfig, "{WS端口}", fmt.Sprintf("%d", port)) conf = strings.ReplaceAll(conf, "{NTSignServer地址}", signServerUrl) conf = strings.ReplaceAll(conf, "{账号UIN}", info.UserID[3:]) - return conf + return appinfo, []byte(conf) } +// 该函数后续考虑优化掉 func LagrangeServeRemoveSession(dice *Dice, conn *EndPointInfo) { workDir := gocqGetWorkDir(dice, conn) - if _, err := os.Stat(filepath.Join(workDir, "keystore.json")); err == nil { - _ = os.Remove(filepath.Join(workDir, "keystore.json")) + file := filepath.Join(workDir, "keystore.json") + pa := conn.Adapter.(*PlatformAdapterGocq) + if pa.BuiltinMode == "lagrange-gocq" { + file = filepath.Join(workDir, "session.token") + } + if _, err := os.Stat(file); err == nil { + _ = os.Remove(file) } } @@ -383,53 +399,332 @@ func LagrangeServeRemoveConfig(dice *Dice, conn *EndPointInfo) { } } -func RWLagrangeSignServerUrl(dice *Dice, conn *EndPointInfo, signServerUrl string, w bool, signServerVersion string) (string, string) { - switch signServerUrl { - case "sealdice": - signServerUrl = defaultNTSignServer - if signServerVersion != "" && signServerVersion != "13107" { - signServerUrl += "/" + signServerVersion - } - case "lagrange": - signServerUrl = "https://sign.lagrangecore.org/api/sign" - if signServerVersion != "" && signServerVersion != "13107" { - signServerUrl += "/" + signServerVersion +// 云端SignInfo.Servers结构 +type SignServerInfo struct { + Name string `json:"name"` + Url string `json:"url"` + Latency int `json:"latency"` + Selected bool `json:"selected"` + Ignored bool `json:"ignored"` + Note string `json:"note"` +} + +// 云端SignInfo结构 +type SignInfo struct { + Version string `json:"version"` + Appinfo map[string]interface{} `json:"appinfo"` + Servers []*SignServerInfo `json:"servers"` + Selected bool `json:"selected"` + Ignored bool `json:"ignored"` + Note string `json:"note"` +} + +// 小概率出现并发读写,需上锁 +var mu sync.Mutex +var signInfoGlobal []SignInfo + +func LagrangeGetSignInfo(dice *Dice) ([]SignInfo, error) { + mu.Lock() + defer mu.Unlock() + cachePath := filepath.Join(dice.BaseConfig.DataDir, "extra/SignInfo.cache") + signInfo, err := lagrangeGetSignInfoFromCloud(cachePath) + if err == nil && len(signInfo) > 0 { + signInfoGlobal = append([]SignInfo(nil), signInfo...) + return signInfo, nil + } + dice.Logger.Infof("无法从云端获取SignInfo,即将读取本地缓存数据, 原因: %s", err.Error()) + + signInfo, err = lagrangeGetSignInfoFromCache(cachePath) + if err == nil && len(signInfo) > 0 { + signInfoGlobal = append([]SignInfo(nil), signInfo...) + return signInfo, nil + } + dice.Logger.Infof("无法从本地缓存获取SignInfo,即将读取内置数据, 原因: %s", err.Error()) + + if err = json.Unmarshal([]byte(signInfoJson), &signInfo); err == nil { + lagrangeGetSignServerLatency(signInfo) + signInfoGlobal = append([]SignInfo(nil), signInfo...) + return signInfo, nil + } + dice.Logger.Infof("无法从内置数据获取SignInfo,请联系开发者上报问题, 原因: %s", err.Error()) + return nil, errors.New("内置SignInfo信息有误") +} + +func lagrangeGetSignInfoFromCloud(cachePath string) ([]SignInfo, error) { + now := time.Now() + unixTimestamp := now.Unix() + url := fmt.Sprintf("https://d1.sealdice.com/sealsign/signinfo.json?v=%v", unixTimestamp) + c := http.Client{ + Timeout: 3 * time.Second, + } + resp, err := c.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var signInfo []SignInfo + err = json.Unmarshal(body, &signInfo) + if err != nil { + return nil, err + } + _ = os.WriteFile(cachePath, body, 0o644) + lagrangeGetSignServerLatency(signInfo) + return signInfo, nil +} + +func lagrangeGetSignInfoFromCache(cachePath string) ([]SignInfo, error) { + var err error + if _, err = os.Stat(cachePath); err == nil { + var file []byte + if file, err = os.ReadFile(cachePath); err == nil { + var signInfo []SignInfo + if err = json.Unmarshal(file, &signInfo); err == nil { + lagrangeGetSignServerLatency(signInfo) + return signInfo, nil + } } } - workDir := lagrangeGetWorkDir(dice, conn) - configFilePath := filepath.Join(workDir, "appsettings.json") - file, err := os.ReadFile(configFilePath) - if err == nil { - var result map[string]interface{} - err = json.Unmarshal(file, &result) - if err == nil { - if val, ok := result["SignServerUrl"].(string); ok { - if w { - result["SignServerUrl"] = signServerUrl - result["SignServerVersion"] = signServerVersion - var c []byte - if c, err = json.MarshalIndent(result, "", " "); err == nil { - _ = os.WriteFile(configFilePath, c, 0o644) - } else { - dice.Logger.Infof("SignServerUrl字段无法正常覆写,账号:%s, 原因: %s", conn.UserID, err.Error()) - } - } + return nil, err +} - var version string - if strings.HasPrefix(val, defaultNTSignServer) { - version, _ = strings.CutPrefix(val, defaultNTSignServer) - version, _ = strings.CutPrefix(version, "/") - val = "sealdice" - } else if strings.HasPrefix(val, lagrangeNTSignServer) { - version, _ = strings.CutPrefix(val, lagrangeNTSignServer) - version, _ = strings.CutPrefix(version, "/") - val = "lagrange" +func lagrangeGetSignSeverFromInfo(serverVer string, serverName string) ([]byte, string) { + mu.Lock() + defer mu.Unlock() + for _, info := range signInfoGlobal { + if info.Version == serverVer { + for _, server := range info.Servers { + if server.Name == serverName { + if appinfo, err := json.Marshal(info.Appinfo); err == nil { + return appinfo, server.Url + } } - return val, version } - err = errors.New("SignServerUrl字段无法正常读取") } } - dice.Logger.Infof("读取内置客户端配置失败,账号:%s, 原因: %s", conn.UserID, err.Error()) - return "", "" + return nil, "" +} + +func lagrangeGetSignServerLatency(signInfo []SignInfo) { + var wg sync.WaitGroup + var mu sync.Mutex + c := &http.Client{ + Timeout: 3 * time.Second, + } + for _, si := range signInfo { + for _, server := range si.Servers { + wg.Add(1) + go func(server *SignServerInfo) { + defer wg.Done() + latency := testLatency(c, server.Url) + mu.Lock() + server.Latency = latency + mu.Unlock() + }(server) + } + } + wg.Wait() } + +func testLatency(c *http.Client, url string) int { + start := time.Now() + resp, err := c.Get(url) + if err != nil { + return 999 + } + defer resp.Body.Close() + duration := time.Since(start) + return int(duration.Milliseconds()) +} + +// 当自定义签名地址时,从/appinfo路径获取appinfo信息 +func lagrangeGetAppinfoFromSignServer(serverName string) ([]byte, error) { + c := http.Client{ + Timeout: 3 * time.Second, + } + resp, err := c.Get(serverName + "/appinfo") + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var test map[string]interface{} + err = json.Unmarshal(body, &test) + if err != nil { + return nil, err + } + return body, nil +} + +var signInfoJson string = ` +[ + { + "version": "25765", + "appinfo": { + "AppClientVersion": 25765, + "AppId": 1600001615, + "AppIdQrCode": 13697054, + "CurrentVersion": "3.2.10-25765", + "Kernel": "Linux", + "MainSigMap": 169742560, + "MiscBitmap": 32764, + "NTLoginType": 1, + "Os": "Linux", + "PackageName": "com.tencent.qq", + "PtVersion": "2.0.0", + "SsoVersion": 19, + "SubAppId": 537234773, + "SubSigMap": 0, + "VendorOs": "linux", + "WtLoginSdk": "nt.wtlogin.0.0.1" + }, + "servers": [ + { + "name": "海豹", + "url": "https://lwxmagic.sealdice.com/api/sign/25765" + }, + { + "name": "Lagrange", + "url": "https://sign.lagrangecore.org/api/sign/25765" + } + ] + }, + { + "version": "30366", + "appinfo": { + "AppClientVersion": 30366, + "AppId": 1600001615, + "AppIdQrCode": 13697054, + "CurrentVersion": "3.2.15-30366", + "Kernel": "Linux", + "MainSigMap": 169742560, + "MiscBitmap": 32764, + "NTLoginType": 1, + "Os": "Linux", + "PackageName": "com.tencent.qq", + "PtVersion": "2.0.0", + "SsoVersion": 19, + "SubAppId": 537258424, + "SubSigMap": 0, + "VendorOs": "linux", + "WtLoginSdk": "nt.wtlogin.0.0.1" + }, + "servers": [ + { + "name": "海豹", + "url": "https://lwxmagic.sealdice.com/api/sign/30366", + "selected": true, + "note": "部分地区用户可能无法连接" + }, + { + "name": "Lagrange", + "url": "https://sign.lagrangecore.org/api/sign/30366" + } + ], + "selected": true + } +] + ` + +var defaultLagrangeConfig = ` + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "SignServerUrl": "{NTSignServer地址}", + "Account": { + "Uin": {账号UIN}, + "Password": "", + "Protocol": "Linux", + "AutoReconnect": true, + "GetOptimumServer": true + }, + "Message": { + "IgnoreSelf": true, + "StringPost": false + }, + "QrCode": { + "ConsoleCompatibilityMode": false + }, + "Implementations": [ + { + "Type": "ForwardWebSocket", + "Host": "127.0.0.1", + "Port": {WS端口}, + "HeartBeatInterval": 5000, + "AccessToken": "" + } + ] + } + ` +var defaultLagrangeGocqConfig = ` +account: + uin: {账号UIN} + password: '' + encrypt: false + status: 0 + relogin: + delay: 3 + interval: 3 + max-times: 0 + use-sso-address: true + allow-temp-session: false + sign-servers: + - url: '{NTSignServer地址}' + max-check-count: 0 + sign-server-timeout: 60 + +heartbeat: + interval: 5 + +message: + post-format: array + ignore-invalid-cqcode: false + force-fragment: false + fix-url: false + proxy-rewrite: '' + report-self-message: false + remove-reply-at: false + extra-reply-data: false + skip-mime-scan: false + convert-webp-image: false + http-timeout: 15 + +output: + log-level: warn + log-aging: 15 + log-force-new: true + log-colorful: true + debug: false + +default-middlewares: &default + access-token: '' + filter: '' + rate-limit: + enabled: false + frequency: 1 + bucket: 1 + +database: + leveldb: + enable: true + sqlite3: + enable: false + cachettl: 3600000000000 + +servers: + - ws: + address: 127.0.0.1:{WS端口} + middlewares: + <<: *default +` diff --git a/dice/platform_adapter_official_qq.go b/dice/platform_adapter_official_qq.go index 2e4f6994..11296b98 100644 --- a/dice/platform_adapter_official_qq.go +++ b/dice/platform_adapter_official_qq.go @@ -2,6 +2,7 @@ package dice import ( "context" + "errors" "fmt" "path/filepath" "strconv" @@ -42,7 +43,7 @@ func (pa *PlatformAdapterOfficialQQ) Serve() int { d := pa.Session.Parent log.Debug("official qq server") - qqbot.SetLogger(NewDummyLogger(log.Desugar())) + qqbot.SetLogger(NewDummyLogger()) token := qqtoken.BotToken(pa.AppID, pa.Token) pa.Api = qqbot.NewOpenAPI(token).WithTimeout(3 * time.Second) pa.Ctx, pa.CancelFunc = context.WithCancel(context.Background()) @@ -241,9 +242,9 @@ func (pa *PlatformAdapterOfficialQQ) SendToPerson(ctx *MsgContext, uid string, t pa.sendQQGuildDirectMsgRaw(ctx, rowID, guildID, channelID, text) } -func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel(ctx *MsgContext, guildID, userID string) (string, string, error) { +func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel( /* ctx */ _ *MsgContext, guildID, userID string) (string, string, error) { if guildID == "" || userID == "" { - err := fmt.Errorf("创建私信频道的参数不全") + err := errors.New("创建私信频道的参数不全") pa.Session.Parent.Logger.Error("official qq 创建私信频道失败:" + err.Error()) return "", "", err } @@ -260,7 +261,7 @@ func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel(ctx *MsgContext, return info.GuildID, info.ChannelID, nil } -func (pa *PlatformAdapterOfficialQQ) sendQQGuildDirectMsgRaw(ctx *MsgContext, rowMsgID string, guildID, channelID string, text string) { +func (pa *PlatformAdapterOfficialQQ) sendQQGuildDirectMsgRaw( /* ctx */ _ *MsgContext, rowMsgID string, guildID, channelID string, text string) { qctx := context.Background() elems := message.ConvertStringMessage(text) var ( @@ -308,7 +309,7 @@ func (pa *PlatformAdapterOfficialQQ) SendToGroup(ctx *MsgContext, uid string, te } } -func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw(ctx *MsgContext, rowMsgID, groupID string, text string) { +func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw( /* ctx */ _ *MsgContext, rowMsgID, groupID string, text string) { qctx := context.Background() elems := message.ConvertStringMessage(text) var ( @@ -383,7 +384,7 @@ func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw(ctx *MsgContext, rowMsgID } } -func (pa *PlatformAdapterOfficialQQ) sendQQChannelMsgRaw(ctx *MsgContext, rowMsgID, channelID string, text string) { +func (pa *PlatformAdapterOfficialQQ) sendQQChannelMsgRaw( /* ctx */ _ *MsgContext, rowMsgID, channelID string, text string) { qctx := context.Background() elems := message.ConvertStringMessage(text) var ( diff --git a/dice/platform_adapter_official_qq_helper.go b/dice/platform_adapter_official_qq_helper.go index b0b0220f..15ce105c 100644 --- a/dice/platform_adapter_official_qq_helper.go +++ b/dice/platform_adapter_official_qq_helper.go @@ -9,7 +9,8 @@ import ( "time" "github.com/google/uuid" - "go.uber.org/zap" + + log "sealdice-core/utils/kratos" ) func NewOfficialQQConnItem(appID uint64, token string, appSecret string, onlyQQGuild bool) *EndPointInfo { @@ -45,12 +46,12 @@ func ServerOfficialQQ(d *Dice, ep *EndPointInfo) { } type DummyLogger struct { - logger *zap.Logger + logger *log.Helper } -func NewDummyLogger(logger *zap.Logger) DummyLogger { +func NewDummyLogger() DummyLogger { return DummyLogger{ - logger: logger, + logger: log.NewHelper(log.With(log.GetLogger(), "caller", "officialQQ")), } } diff --git a/dice/platform_adapter_red.go b/dice/platform_adapter_red.go index 742ed74e..b1f6cd47 100644 --- a/dice/platform_adapter_red.go +++ b/dice/platform_adapter_red.go @@ -43,7 +43,7 @@ type PlatformAdapterRed struct { memberMap *SyncMap[string, *SyncMap[string, *GroupMember]] } -type RedPack[T interface{}] struct { +type RedPack[T any] struct { Type string `json:"type"` Payload *T `json:"payload"` } @@ -883,7 +883,7 @@ func (pa *PlatformAdapterRed) httpDo(method, action string, headers map[string]s } // encodeMessage 将带 cq code 的内容转换为 red 所需的格式 -func (pa *PlatformAdapterRed) encodeMessage(ctx *MsgContext, content string) []*RedElement { +func (pa *PlatformAdapterRed) encodeMessage( /* ctx */ _ *MsgContext, content string) []*RedElement { elems := message.ConvertStringMessage(content) var redElems []*RedElement for _, elem := range elems { diff --git a/dice/platform_adapter_satori.go b/dice/platform_adapter_satori.go index 858666d4..770d8e52 100644 --- a/dice/platform_adapter_satori.go +++ b/dice/platform_adapter_satori.go @@ -440,7 +440,7 @@ func (pa *PlatformAdapterSatori) SendToGroup(ctx *MsgContext, groupID string, te pa.sendMsgRaw(ctx, UserIDExtract(groupID), text, flag, "group") } -func (pa *PlatformAdapterSatori) sendMsgRaw(ctx *MsgContext, channelID string, text string, flag string, msgType string) { +func (pa *PlatformAdapterSatori) sendMsgRaw( /* ctx */ _ *MsgContext, channelID string, text string /* flag */, _ string, msgType string) { log := pa.Session.Parent.Logger req, err := json.Marshal(map[string]interface{}{ "channel_id": channelID, @@ -554,6 +554,7 @@ func (pa *PlatformAdapterSatori) post(resource string, body io.Reader) ([]byte, request.Header.Add("Authorization", "Bearer "+pa.Token) } request.Header.Add("X-Platform", pa.Platform) + //nolint:canonicalheader request.Header.Add("X-Self-ID", UserIDExtract(pa.EndPoint.UserID)) resp, err := client.Do(request) if err != nil { @@ -809,21 +810,21 @@ func (pa *PlatformAdapterSatori) guildRequestHandle(e *SatoriEvent) { eid := e.ID.String() // 邀请人在黑名单上 - banInfo, ok := d.BanList.GetByID(uid) + banInfo, ok := d.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && d.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && d.Config.BanList.BanBehaviorRefuseInvite { pa.sendGuildRequestResult(eid, false, "黑名单") return } } // 信任模式,如果不是信任,又不是 master 则拒绝拉群邀请 isMaster := d.IsMaster(uid) - if d.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { + if d.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { pa.sendGuildRequestResult(eid, false, "只允许骰主设置信任的人拉群") return } // 群在黑名单上 - banInfo, ok = d.BanList.GetByID(guildID) + banInfo, ok = d.Config.BanList.GetByID(guildID) if ok { if banInfo.Rank == BanRankBanned { pa.sendGuildRequestResult(eid, false, "群黑名单") @@ -831,7 +832,7 @@ func (pa *PlatformAdapterSatori) guildRequestHandle(e *SatoriEvent) { } } // 拒绝加群 - if d.RefuseGroupInvite { + if d.Config.RefuseGroupInvite { pa.sendGuildRequestResult(eid, false, "设置拒绝加群") return } @@ -870,15 +871,15 @@ func (pa *PlatformAdapterSatori) friendRequestHandle(e *SatoriEvent) { eid := e.ID.String() // 申请人在黑名单上 - banInfo, ok := d.BanList.GetByID(uid) + banInfo, ok := d.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && d.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && d.Config.BanList.BanBehaviorRefuseInvite { pa.sendGuildRequestResult(eid, false, "为被禁止用户,准备自动拒绝") return } } - if strings.TrimSpace(d.FriendAddComment) == "" { + if strings.TrimSpace(d.Config.FriendAddComment) == "" { pa.sendFriendRequestResult(eid, true, "") } else { pa.sendFriendRequestResult(eid, false, "存在好友问题校验,准备自动拒绝,请联系骰主") diff --git a/dice/platform_adapter_sealchat.go b/dice/platform_adapter_sealchat.go index e068e1b8..bc5d2c1c 100644 --- a/dice/platform_adapter_sealchat.go +++ b/dice/platform_adapter_sealchat.go @@ -24,18 +24,20 @@ type PlatformAdapterSealChat struct { EchoMap SyncMap[string, chan any] `yaml:"-" json:"-"` UserID string `yaml:"-" json:"-"` - Reconnecting bool `yaml:"-" json:"-"` - RetryTimes int `yaml:"-" json:"-"` + Reconnecting bool `yaml:"-" json:"-"` + RetryTimes int `yaml:"-" json:"-"` + RetryTimesLimit int `yaml:"-" json:"-"` } func (pa *PlatformAdapterSealChat) Serve() int { - if !strings.HasPrefix(pa.ConnectURL, "ws://") { + if !strings.HasPrefix(pa.ConnectURL, "ws://") && !strings.HasPrefix(pa.ConnectURL, "wss://") { pa.ConnectURL = "ws://" + pa.ConnectURL } socket := gowebsocket.New(pa.ConnectURL) pa.Socket = &socket pa.EndPoint.Nickname = "SealChat Bot" pa.EndPoint.UserID = "SEALCHAT:BOT" + pa.RetryTimesLimit = 1 d := pa.Session.Parent d.LastUpdatedTime = time.Now().Unix() d.Save(false) @@ -62,7 +64,6 @@ func (pa *PlatformAdapterSealChat) socketSetup() { pa.Reconnecting = true ep.State = 2 ep.Enable = true - pa.RetryTimes = 0 d := pa.Session.Parent d.LastUpdatedTime = time.Now().Unix() @@ -75,7 +76,7 @@ func (pa *PlatformAdapterSealChat) socketSetup() { }, }) - log.Info("SealChat 已连接,正在发送身份验证信息") + log.Info("SealChat 建立连接,正在发送身份验证信息") pa.Reconnecting = false } socket.OnTextMessage = func(message string, socket gowebsocket.Socket) { @@ -109,6 +110,10 @@ func (pa *PlatformAdapterSealChat) socketSetup() { ep.Nickname = data.Body.User.Nick ep.State = 1 log.Infof("SealChat 连接成功: %s", ep.Nickname) + + // 握手成功,通过验证 + pa.RetryTimes = 0 + pa.RetryTimesLimit = 15 } go func() { @@ -143,6 +148,7 @@ func (pa *PlatformAdapterSealChat) socketSetup() { log.Errorf("SealChat websocket出现错误: %s", err) if !socket.IsConnected && !pa.Reconnecting { // socket.Close() + time.Sleep(time.Duration(10) * time.Second) if !pa.tryReconnect(*pa.Socket) { log.Errorf("短时间内连接失败次数过多,不再进行重连") ep.State = 3 @@ -150,11 +156,11 @@ func (pa *PlatformAdapterSealChat) socketSetup() { } } socket.OnDisconnected = func(err error, socket gowebsocket.Socket) { - log.Errorf("与SealChat服务器断开连接") + log.Info("与SealChat服务器断开连接,尝试进行重连") time.Sleep(time.Duration(2) * time.Second) if !pa.tryReconnect(*pa.Socket) { - log.Errorf("尝试进行重连") ep.State = 3 + log.Errorf("到达连接次数上限,不再进行重连") } } pa.Socket = socket @@ -162,25 +168,26 @@ func (pa *PlatformAdapterSealChat) socketSetup() { func (pa *PlatformAdapterSealChat) tryReconnect(socket gowebsocket.Socket) bool { log := pa.Session.Parent.Logger - if pa.Reconnecting { - return false + if socket.IsConnected { + return true } pa.Reconnecting = true - pa.RetryTimes = 0 - allTimes := 500 - for pa.RetryTimes <= allTimes && !socket.IsConnected { - if !pa.EndPoint.Enable { - return false - } - pa.RetryTimes++ - log.Infof("尝试重新连接SealChat中[%d/%d]", pa.RetryTimes, allTimes) - socket = gowebsocket.New(pa.ConnectURL) - pa.Socket = &socket - pa.socketSetup() - socket.Connect() - time.Sleep(time.Duration(10) * time.Second) + if !pa.EndPoint.Enable { + return true + } + + if pa.RetryTimes >= pa.RetryTimesLimit { + return false } + + pa.RetryTimes++ + log.Infof("尝试重新连接SealChat中[%d/%d]", pa.RetryTimes, pa.RetryTimesLimit) + socket = gowebsocket.New(pa.ConnectURL) + pa.Socket = &socket + pa.socketSetup() + socket.Connect() + pa.Reconnecting = false return true } @@ -378,7 +385,7 @@ func (pa *PlatformAdapterSealChat) dispatchMessage(msg string) { ev := satori.Event{} err := json.Unmarshal([]byte(msg), &ev) if err != nil { - fmt.Println(err) + pa.Session.Parent.Logger.Error("PlatformAdapterSealChat.dispatchMessage", err) return } diff --git a/dice/platform_adapter_telegram.go b/dice/platform_adapter_telegram.go index df09c5e8..b8729fcf 100644 --- a/dice/platform_adapter_telegram.go +++ b/dice/platform_adapter_telegram.go @@ -13,8 +13,21 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "sealdice-core/message" + log "sealdice-core/utils/kratos" ) +type BotLoggerWrapper struct { + Logger *log.Helper +} + +func (b *BotLoggerWrapper) Println(v ...interface{}) { + b.Logger.Error(v...) +} + +func (b *BotLoggerWrapper) Printf(format string, v ...interface{}) { + b.Logger.Errorf(format, v...) +} + type PlatformAdapterTelegram struct { Session *IMSession `yaml:"-" json:"-"` Token string `yaml:"token" json:"token"` @@ -56,6 +69,7 @@ func (pa *PlatformAdapterTelegram) Serve() int { var bot *tgbotapi.BotAPI var err error + _ = tgbotapi.SetLogger(&BotLoggerWrapper{Logger: logger}) if len(pa.ProxyURL) > 0 { var u *url.URL u, err = url.Parse(pa.ProxyURL) diff --git a/dice/platform_adapter_walleq.go b/dice/platform_adapter_walleq.go index c1c041d0..a1c36fbb 100644 --- a/dice/platform_adapter_walleq.go +++ b/dice/platform_adapter_walleq.go @@ -205,7 +205,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { // tempFriendInviteSent := map[string]int64{} // gocq会重新发送已经发过的邀请 socket.OnTextMessage = func(message string, socket gowebsocket.Socket) { - fmt.Println(message) + log.Debug(message) event := new(EventWalleQBase) err := json.Unmarshal([]byte(message), event) if err != nil { @@ -404,7 +404,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { if event.UserID == event.Self.UserID { skip := false skipReason := "" - banInfo, ok := ctx.Dice.BanList.GetByID(opUID) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(opUID) if ok { if banInfo.Rank == 30 { skip = true @@ -420,7 +420,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { if skip { extra = fmt.Sprintf("\n取消处罚,原因为%s", skipReason) } else { - ctx.Dice.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx) } txt := fmt.Sprintf("被踢出群: 在QQ群组<%s>(%s)中被踢出,操作者:<%s>(%s)%s", groupName, event.GroupID, userName, n.OperatorID, extra) @@ -429,7 +429,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { } case "group_member_ban": // 被禁言 if event.UserID == event.Self.UserID { - ctx.Dice.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx) + ctx.Dice.Config.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx) txt := fmt.Sprintf("被禁言: 在群组<%s>(%s)中被禁言,时长%d秒,操作者:<%s>(%s)", groupName, msg.GroupID, n.Duration, userName, n.OperatorID) log.Info(txt) ctx.Notice(txt) @@ -470,7 +470,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { comment = strings.ReplaceAll(comment, "\u00a0", "") } - toMatch := strings.TrimSpace(s.Parent.FriendAddComment) + toMatch := strings.TrimSpace(s.Parent.Config.FriendAddComment) willAccept := comment == DiceFormat(ctx, toMatch) if toMatch == "" { willAccept = true @@ -491,7 +491,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { if len(m2) == len(items) { ok := true - for i := 0; i < len(m2); i++ { + for i := range m2 { if m2[i] != items[i] { ok = false break @@ -510,9 +510,9 @@ func (pa *PlatformAdapterWalleQ) Serve() int { // 检查黑名单 extra := "" uid := msg.Sender.UserID - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { if willAccept { extra = "。回答正确,但为被禁止用户,准备自动拒绝" } else { @@ -564,21 +564,21 @@ func (pa *PlatformAdapterWalleQ) Serve() int { // tempInviteMap2[msg.GroupId] = uid // 邀请人在黑名单上 - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { pa.SetGroupAddRequest(req.RequestID, event.GroupID, false) return } } // 信任模式,如果不是信任,又不是master则拒绝拉群邀请 isMaster := ctx.Dice.IsMaster(uid) - if ctx.Dice.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { + if ctx.Dice.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) { pa.SetGroupAddRequest(req.RequestID, event.GroupID, false) return } // 群在黑名单上 - banInfo, ok = ctx.Dice.BanList.GetByID(gid) + banInfo, ok = ctx.Dice.Config.BanList.GetByID(gid) if ok { if banInfo.Rank == BanRankBanned { pa.SetGroupAddRequest(req.RequestID, event.GroupID, false) @@ -586,7 +586,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { } } // 拒绝加入新群 - if ctx.Dice.RefuseGroupInvite { + if ctx.Dice.Config.RefuseGroupInvite { pa.SetGroupAddRequest(req.RequestID, event.GroupID, false) return } @@ -598,7 +598,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { // 事件都有 ID,没有就是响应 but 有几个元事件 ID 是 "" ;把响应处理放到最后吧 //nolint:nestif if event.ID == "" { - fmt.Println(message) + log.Debug(message) echo := new(EchoWalleQ) err = json.Unmarshal([]byte(message), echo) if err != nil { @@ -643,9 +643,9 @@ func (pa *PlatformAdapterWalleQ) Serve() int { // 处理被强制拉群的情况 uid := groupInfo.InviteUserID - banInfo, ok := ctx.Dice.BanList.GetByID(uid) + banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid) if ok { - if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite { + if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite { // 如果是被ban之后拉群,判定为强制拉群 if groupInfo.EnteredTime > 0 && groupInfo.EnteredTime > banInfo.BanTime { text := fmt.Sprintf("本次入群为遭遇强制邀请,即将主动退群,因为邀请人%s正处于黑名单上。打扰各位还请见谅。感谢使用海豹核心。", groupInfo.InviteUserID) @@ -658,7 +658,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int { } // 强制拉群情况2 - 群在黑名单 - banInfo, ok = ctx.Dice.BanList.GetByID(groupID) + banInfo, ok = ctx.Dice.Config.BanList.GetByID(groupID) if ok { if banInfo.Rank == BanRankBanned { // 如果是被ban之后拉群,判定为强制拉群 @@ -701,11 +701,11 @@ func (pa *PlatformAdapterWalleQ) Serve() int { socket.Connect() defer func() { - fmt.Println("socket close") + log.Info("socket close") go func() { defer func() { if r := recover(); r != nil { - fmt.Println("关闭连接时遭遇异常") + log.Error("关闭连接时遭遇异常", r) } }() socket.Close() diff --git a/dice/platform_adapter_walleq_helper.go b/dice/platform_adapter_walleq_helper.go index 91f9ec2a..15c28848 100644 --- a/dice/platform_adapter_walleq_helper.go +++ b/dice/platform_adapter_walleq_helper.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/pelletier/go-toml/v2" + log2 "sealdice-core/utils/kratos" "sealdice-core/utils/procs" ) @@ -133,7 +134,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int, pa.CurLoginIndex++ loginIndex := pa.CurLoginIndex pa.WalleQState = WqStateCodeInLogin - fmt.Println("WalleQServe begin") + log2.Debug("WalleQServe begin") workDir := filepath.Join(dice.BaseConfig.DataDir, conn.RelWorkDir) _ = os.MkdirAll(workDir, 0o755) log := dice.Logger @@ -173,7 +174,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int, wqc := new(WalleQConfig) _, err = toml.DecodeFile(configFilePath,wqc) if err != nil { - dice.Logger.Error("读取 Walle-q 配置文件失败,请检查!") + dice.Zlogger.Error("读取 Walle-q 配置文件失败,请检查!") return } id, _ := pa.mustExtractId(conn.UserId) @@ -197,7 +198,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int, isSeldKilling := false p.OutputHandler = func(line string, _type string) string { - fmt.Println(line) + log.Debug(line) if loginIndex != pa.CurLoginIndex { // 当前连接已经无用,进程自杀 if !isSeldKilling { @@ -271,9 +272,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int, dice.Logger.Warn("添加到进程组失败,若主进程崩溃,walle-q 进程可能需要手动结束") } } - fmt.Println("wait!") err = p.Wait() - fmt.Println(err) } if err != nil { diff --git a/dice/rollvm.go b/dice/rollvm.go index 095b972d..4e06ac74 100644 --- a/dice/rollvm.go +++ b/dice/rollvm.go @@ -559,7 +559,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e num := int(code.Value) outStr := "" - for index := 0; index < num; index++ { + for index := range num { var val VMStack if top-num+index < 0 { return nil, "", errors.New("E3:无效的表达式") @@ -733,7 +733,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e return nil, "", getE5() } - for i := int64(0); i < t.Value.(int64); i++ { + for range t.Value.(int64) { n := DiceRoll64x(ctx._v1Rand, 10) if n == 10 { @@ -1269,7 +1269,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e checkDice(&code) text := "" sum := int64(0) - for i := 0; i < 4; i++ { + for range 4 { n := rand.Int63()%3 - 1 sum += n switch n { @@ -1313,7 +1313,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e } var nums []int64 - for i := int64(0); i < aInt; i++ { + for range aInt { if e.flags.BigFailDiceOn { nums = append(nums, bInt) } else { @@ -1328,7 +1328,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e } num := int64(0) - for i := int64(0); i < diceKQ; i++ { + for i := range diceKQ { // 当取数大于上限 跳过 if i >= int64(len(nums)) { continue @@ -1337,8 +1337,8 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e } text := "{" - for i := int64(0); i < int64(len(nums)); i++ { - if i == diceKQ { + for i := range nums { + if int64(i) == diceKQ { text += "| " } text += fmt.Sprintf("%d ", nums[i]) @@ -1355,7 +1355,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e // XXX Dice YYY, 如 3d100 var num int64 text := "" - for i := int64(0); i < aInt; i++ { + for range aInt { var curNum int64 if e.flags.BigFailDiceOn { curNum = bInt @@ -1439,7 +1439,7 @@ func DiceDCRoll(randSrc *rand2.PCGSource, addLine int64, pool int64, points int6 var detailsOne []string maxDice := int64(0) - for i := int64(0); i < pool; i++ { + for range pool { one := DiceRoll64x(randSrc, points) if one > maxDice { maxDice = one @@ -1496,7 +1496,7 @@ func DiceWodRoll(randSrc *rand2.PCGSource, addLine int64, pool int64, points int addCount := int64(0) var detailsOne []string - for i := int64(0); i < pool; i++ { + for range pool { var reachSuccess bool var reachAddRound bool one := DiceRoll64x(randSrc, points) diff --git a/dice/rollvm_migrate.go b/dice/rollvm_migrate.go index b4762804..4a7adb1f 100644 --- a/dice/rollvm_migrate.go +++ b/dice/rollvm_migrate.go @@ -12,8 +12,209 @@ import ( "github.com/samber/lo" ds "github.com/sealdice/dicescript" + + log "sealdice-core/utils/kratos" ) +func (ctx *MsgContext) GenDefaultRollVmConfig() *ds.RollConfig { + config := ds.RollConfig{} + + // 根据当前规则开语法 - 暂时是都开 + config.EnableDiceWoD = true + config.EnableDiceCoC = true + config.EnableDiceFate = true + config.EnableDiceDoubleCross = true + config.OpCountLimit = 30000 + + am := ctx.Dice.AttrsManager + config.HookFuncValueStore = func(vm *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) { + // 临时变量 + if strings.HasPrefix(name, "$t") { + if ctx.Player.ValueMapTemp == nil { + ctx.Player.ValueMapTemp = &ds.ValueMap{} + } + ctx.Player.ValueMapTemp.Store(name, v) + // 继续存入local 因此solved为false + return nil, false + } + + // 个人变量 + if strings.HasPrefix(name, "$m") { + if ctx.Session != nil && ctx.Player != nil { + playerAttrs := lo.Must(am.LoadById(ctx.Player.UserID)) + playerAttrs.Store(name, v) + } + return nil, true + } + + // 群变量 + if ctx.Group != nil && strings.HasPrefix(name, "$g") { + groupAttrs := lo.Must(am.LoadById(ctx.Group.GroupID)) + groupAttrs.Store(name, v) + return nil, true + } + return nil, false + } + + reSimpleBP := regexp.MustCompile(`^[bpBP]\d*$`) + mctx := ctx + config.CustomMakeDetailFunc = func(ctx *ds.Context, details []ds.BufferSpan, dataBuffer []byte, parsedOffset int) string { + detailResult := dataBuffer[:parsedOffset] + + var curPoint ds.IntType + lastEnd := ds.IntType(-1) //nolint:ineffassign + + type Group struct { + begin ds.IntType + end ds.IntType + tag string + spans []ds.BufferSpan + val *ds.VMValue + } + + // 特殊机制: 从模板读取detail进行覆盖 + for index, i := range details { + if i.Tag == "load" && mctx.SystemTemplate != nil && ctx.UpCtx == nil { + expr := string(detailResult[i.Begin:i.End]) + detailExpr := mctx.SystemTemplate.DetailOverwrite[expr] + if detailExpr == "" { + // 如果没有,尝试使用通配 + detailExpr = mctx.SystemTemplate.DetailOverwrite["*"] + if detailExpr != "" { + // key 应该是等于expr的 + ctx.StoreNameLocal("name", ds.NewStrVal(expr)) + } + } + if detailExpr != "" { + v, err := ctx.RunExpr(detailExpr, true) + if v != nil { + details[index].Text = v.ToString() + } + if err != nil { + details[index].Text = err.Error() + } + } + } + } + + var m []Group + for _, i := range details { + // fmt.Println("?", i, lastEnd) + if i.Begin > lastEnd { + curPoint = i.Begin + m = append(m, Group{begin: curPoint, end: i.End, tag: i.Tag, spans: []ds.BufferSpan{i}, val: i.Ret}) + } else { + m[len(m)-1].spans = append(m[len(m)-1].spans, i) + if i.End > m[len(m)-1].end { + m[len(m)-1].end = i.End + } + } + + if i.End > lastEnd { + lastEnd = i.End + } + } + + var detailArr []*ds.VMValue + for i := len(m) - 1; i >= 0; i-- { + buf := bytes.Buffer{} + writeBuf := func(p []byte) { + buf.Write(p) + } + writeBufStr := func(s string) { + buf.WriteString(s) + } + + item := m[i] + size := len(item.spans) + sort.Sort(spanByEnd(item.spans)) + last := item.spans[size-1] + + subDetailsText := "" + if size > 1 { + // 次级结果,如 (10d3)d5 中,此处为10d3的结果 + // 例如 (10d3)d5=63[(10d3)d5=...,10d3=19] + for j := range len(item.spans) - 1 { + span := item.spans[j] + subDetailsText += "," + string(detailResult[span.Begin:span.End]) + "=" + span.Ret.ToString() + } + } + + exprText := last.Expr + baseExprText := string(detailResult[item.begin:item.end]) + if last.Expr == "" { + exprText = baseExprText + } + + writeBuf(detailResult[:item.begin]) + + // 主体结果部分,如 (10d3)d5=63[(10d3)d5=2+2+2+5+2+5+5+4+1+3+4+1+4+5+4+3+4+5+2,10d3=19] + partRet := last.Ret.ToString() + + detail := "[" + exprText + if last.Text != "" && partRet != last.Text { // 规则1.1 + detail += "=" + last.Text + } + + switch item.tag { + case "dnd-rc": + detail = "[" + last.Text + case "load": + detail = "[" + exprText + if last.Text != "" { + detail += "," + last.Text + } + case "dice-coc-bonus", "dice-coc-penalty": + // 对简单式子进行结果简化,未来或许可以做成通配规则(给左式加个规则进行消除) + if reSimpleBP.MatchString(exprText) { + detail = "[" + last.Text[1:len(last.Text)-1] + } + case "load.computed": + detail += "=" + partRet + } + + detail += subDetailsText + "]" + if len(m) == 1 && detail == "["+baseExprText+"]" { + detail = "" // 规则1.3 + } + if len(detail) > 400 { + detail = "[略]" + } + writeBufStr(partRet + detail) + writeBuf(detailResult[item.end:]) + detailResult = buf.Bytes() + + d := ds.NewDictValWithArrayMust( + ds.NewStrVal("tag"), ds.NewStrVal(item.tag), + ds.NewStrVal("expr"), ds.NewStrVal(exprText), + ds.NewStrVal("val"), item.val, + ) + detailArr = append(detailArr, d.V()) + } + + // TODO: 此时加了TrimSpace表现正常,但深层原因是ds在处理"d3 x"这个表达式时多吃了一个空格,修复后取消trim + detailStr := strings.TrimSpace(string(detailResult)) + if detailStr == ctx.Ret.ToString() { + detailStr = "" // 如果detail和结果值完全一致,那么将其置空 + } + ctx.StoreNameLocal("details", ds.NewArrayValRaw(lo.Reverse(detailArr))) + return detailStr + } + + // 设置默认骰子面数 + if ctx.Group != nil { + // 情况不明,在sealchat的第一次测试中出现Group为nil + config.DefaultDiceSideExpr = strconv.FormatInt(ctx.Group.DiceSideNum, 10) + if config.DefaultDiceSideExpr == "0" { + config.DefaultDiceSideExpr = "100" + } + } else { + config.DefaultDiceSideExpr = "100" + } + + return &config +} + func (v *VMValue) ConvertToV2() *ds.VMValue { switch v.TypeID { case VMTypeInt64: @@ -78,13 +279,19 @@ func DiceFormatV1(ctx *MsgContext, s string) (string, error) { //nolint:revive } func DiceFormat(ctx *MsgContext, s string) string { - ret, err := DiceFormatV2(ctx, s) - if err != nil { - // 遇到异常,尝试一下V1 - ret, _ = DiceFormatV1(ctx, s) + engineVersion := ctx.Dice.getTargetVmEngineVersion(VmVersionMsg) + if engineVersion == "v2" { + ret, err := DiceFormatV2(ctx, s) + if err != nil { + // 遇到异常,尝试一下V1 + ret, _ = DiceFormatV1(ctx, s) + return ret + } + return ret + } else { + ret, _ := DiceFormatV1(ctx, s) return ret } - return ret } func DiceFormatTmpl(ctx *MsgContext, s string) string { @@ -96,7 +303,7 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string { text = ctx.Dice.TextMap[s].PickSource(randSourceDrawAndTmplSelect).(string) // 找出其兼容情况,以决定使用什么版本的引擎 - engineVersion := "v2" + engineVersion := ctx.Dice.getTargetVmEngineVersion(VMVersionCustomText) if items, exists := ctx.Dice.TextMapCompatible.Load(s); exists { if info, exists := items.Load(text); exists { if info.Version == "v1" { @@ -108,7 +315,7 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string { if engineVersion == "v2" { ret, _ := DiceFormatV2(ctx, text) return ret - } else if engineVersion == "v1" { + } else { ret, _ := DiceFormatV1(ctx, text) return ret } @@ -120,10 +327,12 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string { func (ctx *MsgContext) Eval(expr string, flags *ds.RollConfig) *VMResultV2 { ctx.CreateVmIfNotExists() vm := ctx.vm + prevConfig := vm.Config if flags != nil { vm.Config = *flags } err := vm.Run(expr) + vm.Config = prevConfig if err != nil { return &VMResultV2{vm: vm} @@ -138,7 +347,7 @@ func (ctx *MsgContext) EvalFString(expr string, flags *ds.RollConfig) *VMResultV // 隐藏的内置字符串符号 \x1e r := ctx.Eval("\x1e"+expr+"\x1e", flags) if r.vm.Error != nil { - fmt.Println("脚本执行出错: ", expr, "->", r.vm.Error) + log.Error("脚本执行出错: ", expr, "->", r.vm.Error) } return r } @@ -235,7 +444,7 @@ func DiceExprEvalBase(ctx *MsgContext, s string, flags RollExtraFlags) (*VMResul if flags.V2Only { return nil, "", err } - fmt.Println("脚本执行出错V2: ", strings.ReplaceAll(s, "\x1e", "`"), "->", err) + log.Error("脚本执行出错V2: ", strings.ReplaceAll(s, "\x1e", "`"), "->", err) errV2 := err // 某种情况下没有这个值,很奇怪 // 尝试一下V1 @@ -426,43 +635,9 @@ func (ctx *MsgContext) CreateVmIfNotExists() { // 初始化骰子 ctx.vm = ds.NewVM() - // 根据当前规则开语法 - 暂时是都开 - ctx.vm.Config.EnableDiceWoD = true - ctx.vm.Config.EnableDiceCoC = true - ctx.vm.Config.EnableDiceFate = true - ctx.vm.Config.EnableDiceDoubleCross = true - ctx.vm.Config.OpCountLimit = 30000 + ctx.vm.Config = *ctx.GenDefaultRollVmConfig() am := ctx.Dice.AttrsManager - ctx.vm.Config.HookFuncValueStore = func(vm *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) { - // 临时变量 - if strings.HasPrefix(name, "$t") { - if ctx.Player.ValueMapTemp == nil { - ctx.Player.ValueMapTemp = &ds.ValueMap{} - } - ctx.Player.ValueMapTemp.Store(name, v) - // 继续存入local 因此solved为false - return nil, false - } - - // 个人变量 - if strings.HasPrefix(name, "$m") { - if ctx.Session != nil && ctx.Player != nil { - playerAttrs := lo.Must(am.LoadById(ctx.Player.UserID)) - playerAttrs.Store(name, v) - } - return nil, true - } - - // 群变量 - if ctx.Group != nil && strings.HasPrefix(name, "$g") { - groupAttrs := lo.Must(am.LoadById(ctx.Group.GroupID)) - groupAttrs.Store(name, v) - return nil, true - } - return nil, false - } - ctx.vm.GlobalValueLoadOverwriteFunc = func(name string, curVal *ds.VMValue) *ds.VMValue { // 临时变量 if strings.HasPrefix(name, "$t") { @@ -538,162 +713,6 @@ func (ctx *MsgContext) CreateVmIfNotExists() { return curVal } - - reSimpleBP := regexp.MustCompile(`^[bpBP]\d*$`) - - mctx := ctx - ctx.vm.Config.CustomMakeDetailFunc = func(ctx *ds.Context, details []ds.BufferSpan, dataBuffer []byte, parsedOffset int) string { - detailResult := dataBuffer[:parsedOffset] - - var curPoint ds.IntType - lastEnd := ds.IntType(-1) //nolint:ineffassign - - type Group struct { - begin ds.IntType - end ds.IntType - tag string - spans []ds.BufferSpan - val *ds.VMValue - } - - // 特殊机制: 从模板读取detail进行覆盖 - for index, i := range details { - if i.Tag == "load" && mctx.SystemTemplate != nil && ctx.UpCtx == nil { - expr := string(detailResult[i.Begin:i.End]) - detailExpr := mctx.SystemTemplate.DetailOverwrite[expr] - if detailExpr == "" { - // 如果没有,尝试使用通配 - detailExpr = mctx.SystemTemplate.DetailOverwrite["*"] - if detailExpr != "" { - // key 应该是等于expr的 - ctx.StoreNameLocal("name", ds.NewStrVal(expr)) - } - } - if detailExpr != "" { - v, err := ctx.RunExpr(detailExpr, true) - if v != nil { - details[index].Text = v.ToString() - } - if err != nil { - details[index].Text = err.Error() - } - } - } - } - - var m []Group - for _, i := range details { - // fmt.Println("?", i, lastEnd) - if i.Begin > lastEnd { - curPoint = i.Begin - m = append(m, Group{begin: curPoint, end: i.End, tag: i.Tag, spans: []ds.BufferSpan{i}, val: i.Ret}) - } else { - m[len(m)-1].spans = append(m[len(m)-1].spans, i) - if i.End > m[len(m)-1].end { - m[len(m)-1].end = i.End - } - } - - if i.End > lastEnd { - lastEnd = i.End - } - } - - var detailArr []*ds.VMValue - for i := len(m) - 1; i >= 0; i-- { - buf := bytes.Buffer{} - writeBuf := func(p []byte) { - buf.Write(p) - } - writeBufStr := func(s string) { - buf.WriteString(s) - } - - item := m[i] - size := len(item.spans) - sort.Sort(spanByEnd(item.spans)) - last := item.spans[size-1] - - subDetailsText := "" - if size > 1 { - // 次级结果,如 (10d3)d5 中,此处为10d3的结果 - // 例如 (10d3)d5=63[(10d3)d5=...,10d3=19] - for j := 0; j < len(item.spans)-1; j++ { - span := item.spans[j] - subDetailsText += "," + string(detailResult[span.Begin:span.End]) + "=" + span.Ret.ToString() - } - } - - exprText := last.Expr - baseExprText := string(detailResult[item.begin:item.end]) - if last.Expr == "" { - exprText = baseExprText - } - - writeBuf(detailResult[:item.begin]) - - // 主体结果部分,如 (10d3)d5=63[(10d3)d5=2+2+2+5+2+5+5+4+1+3+4+1+4+5+4+3+4+5+2,10d3=19] - partRet := last.Ret.ToString() - - detail := "[" + exprText - if last.Text != "" && partRet != last.Text { // 规则1.1 - detail += "=" + last.Text - } - - switch item.tag { - case "dnd-rc": - detail = "[" + last.Text - case "load": - detail = "[" + exprText - if last.Text != "" { - detail += "," + last.Text - } - case "dice-coc-bonus", "dice-coc-penalty": - // 对简单式子进行结果简化,未来或许可以做成通配规则(给左式加个规则进行消除) - if reSimpleBP.MatchString(exprText) { - detail = "[" + last.Text[1:len(last.Text)-1] - } - case "load.computed": - detail += "=" + partRet - } - - detail += subDetailsText + "]" - if len(m) == 1 && detail == "["+baseExprText+"]" { - detail = "" // 规则1.3 - } - if len(detail) > 400 { - detail = "[略]" - } - writeBufStr(partRet + detail) - writeBuf(detailResult[item.end:]) - detailResult = buf.Bytes() - - d := ds.NewDictValWithArrayMust( - ds.NewStrVal("tag"), ds.NewStrVal(item.tag), - ds.NewStrVal("expr"), ds.NewStrVal(exprText), - ds.NewStrVal("val"), item.val, - ) - detailArr = append(detailArr, d.V()) - } - - detailStr := string(detailResult) - if detailStr == ctx.Ret.ToString() { - detailStr = "" // 如果detail和结果值完全一致,那么将其置空 - } - ctx.StoreNameLocal("details", ds.NewArrayValRaw(lo.Reverse(detailArr))) - return detailStr - } - - // 设置默认骰子面数 - if ctx.Group != nil { - // 情况不明,在sealchat的第一次测试中出现Group为nil - ctx.vm.Config.DefaultDiceSideExpr = fmt.Sprintf("%d", ctx.Group.DiceSideNum) - if ctx.vm.Config.DefaultDiceSideExpr == "0" { - ctx.vm.Config.DefaultDiceSideExpr = "100" - } - } else { - ctx.vm.Config.DefaultDiceSideExpr = "100" - } } func DiceFormatV2(ctx *MsgContext, s string) (string, error) { //nolint:revive diff --git a/dice/storylog/storylog.go b/dice/storylog/storylog.go index 156c96ae..28dd9bef 100644 --- a/dice/storylog/storylog.go +++ b/dice/storylog/storylog.go @@ -9,16 +9,16 @@ import ( "net/http" "strconv" - "github.com/jmoiron/sqlx" - "go.uber.org/zap" + "gorm.io/gorm" "sealdice-core/dice/model" + log "sealdice-core/utils/kratos" ) type UploadEnv struct { Dir string - Db *sqlx.DB - Log *zap.SugaredLogger + Db *gorm.DB + Log *log.Helper Backends []string Version StoryVersion diff --git a/dice/utils.go b/dice/utils.go index 30ca375a..a161267c 100644 --- a/dice/utils.go +++ b/dice/utils.go @@ -573,3 +573,25 @@ func UnpackGroupUserId(id string) (groupIdPart, userIdPart string, ok bool) { return "", "", false } + +const ( + VMVersionReply = "reply" + VMVersionDeck = "deck" + VMVersionCustomText = "custom-text" + VmVersionMsg = "msg" +) + +func (d *Dice) getTargetVmEngineVersion(targetType string) string { + switch targetType { + case VMVersionReply: + return d.Config.VMVersionForReply + case VMVersionDeck: + return d.Config.VMVersionForDeck + case VMVersionCustomText: + return d.Config.VMVersionForCustomText + case VmVersionMsg: + return d.Config.VMVersionForMsg + default: + return "v2" + } +} diff --git a/dice/utils_email.go b/dice/utils_email.go index f23b0835..782e4f92 100644 --- a/dice/utils_email.go +++ b/dice/utils_email.go @@ -1,6 +1,7 @@ package dice import ( + "errors" "fmt" "strings" @@ -23,7 +24,7 @@ const ( ) func (d *Dice) CanSendMail() bool { - if d.MailFrom == "" || d.MailPassword == "" || d.MailSMTP == "" { + if d.Config.MailFrom == "" || d.Config.MailPassword == "" || d.Config.MailSMTP == "" { return false } return true @@ -31,7 +32,7 @@ func (d *Dice) CanSendMail() bool { func (d *Dice) SendMail(body string, m MailCode) error { if !d.CanSendMail() { - return fmt.Errorf("邮件配置不完整") + return errors.New("邮件配置不完整") } sub := "Seal News: " switch m { @@ -47,7 +48,7 @@ func (d *Dice) SendMail(body string, m MailCode) error { sub += "Test 测试邮件" } var to []string - for _, id := range d.NoticeIDs { + for _, id := range d.Config.NoticeIDs { if strings.HasPrefix(id, "QQ:") { to = append(to, id[3:]+"@qq.com") } @@ -70,7 +71,7 @@ func (d *Dice) SendMailRow(subject string, to []string, content string, attachme } } m.SetHeader("Subject", fmt.Sprintf("[%s] %s", diceName, subject)) - m.SetHeader("From", d.MailFrom) + m.SetHeader("From", d.Config.MailFrom) m.SetHeader("To", to...) if content == "" { m.SetBody("text/plain", "***自动邮件,无需回复***") @@ -83,7 +84,7 @@ func (d *Dice) SendMailRow(subject string, to []string, content string, attachme } } - dialer := gomail.NewDialer(d.MailSMTP, 25, d.MailFrom, d.MailPassword) + dialer := gomail.NewDialer(d.Config.MailSMTP, 25, d.Config.MailFrom, d.Config.MailPassword) if err := dialer.DialAndSend(m); err != nil { d.Logger.Error(err) } else { diff --git a/dice/verify.go b/dice/verify.go index 2521556c..7ce89937 100644 --- a/dice/verify.go +++ b/dice/verify.go @@ -1,6 +1,7 @@ package dice import ( + "crypto/sha256" "encoding/base64" "fmt" "os" @@ -10,6 +11,7 @@ import ( "github.com/vmihailenco/msgpack" "sealdice-core/utils/crypto" + log "sealdice-core/utils/kratos" ) var ( @@ -22,8 +24,8 @@ func initVerify() { key := os.Getenv("SEAL_TRUSTED_PRIVATE_KEY") if len(key) > 0 { SealTrustedClientPrivateKey = key - } else { - fmt.Println("SEAL_TRUSTED_PRIVATE_KEY not found, maybe in development mode") + } else if len(SealTrustedClientPrivateKey) == 0 { + log.Warn("SEAL_TRUSTED_PRIVATE_KEY not found, maybe in development mode") } } @@ -70,3 +72,37 @@ func GenerateVerificationCode(platform string, userID string, username string, u return fmt.Sprintf("SEAL%%%s", base2048.DefaultEncoding.EncodeToString(dp)) } } + +type payloadPublicDice struct { + Version string `msgpack:"version,omitempty"` + Sign []byte `msgpack:"sign,omitempty"` +} + +func GenerateVerificationKeyForPublicDice(data any) string { + doEcdsaSign := len(SealTrustedClientPrivateKey) > 0 + pp, _ := msgpack.Marshal(data) + + var sign []byte + if doEcdsaSign { + var err error + sign, err = crypto.EcdsaSignRow(pp, SealTrustedClientPrivateKey) + if err != nil { + return "" + } + } else { + h := sha256.New() + h.Write(pp) + sign = h.Sum(nil) + } + + d := payloadPublicDice{ + Version: VERSION.String(), + Sign: sign, + } + + dp, _ := msgpack.Marshal(d) + if doEcdsaSign { + return fmt.Sprintf("SEAL#%s", base64.StdEncoding.EncodeToString(dp)) + } + return fmt.Sprintf("SEAL~%s", base64.StdEncoding.EncodeToString(dp)) +} diff --git a/dice/version.go b/dice/version.go index 178d469f..d32a46a3 100644 --- a/dice/version.go +++ b/dice/version.go @@ -21,7 +21,7 @@ var ( // APP_CHANNEL 更新频道,stable/dev,在 action 构建时自动注入 APP_CHANNEL = "dev" //nolint:revive - VERSION_CODE = int64(1004006) //nolint:revive + VERSION_CODE = int64(1004101) //nolint:revive VERSION_JSAPI_COMPATIBLE = []*semver.Version{ VERSION, diff --git a/go.mod b/go.mod index c7bbda3f..f684dfcb 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module sealdice-core -go 1.20 +go 1.22 require ( - github.com/Masterminds/semver/v3 v3.2.1 + github.com/Masterminds/semver/v3 v3.3.0 github.com/Milly/go-base2048 v0.1.0 github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a github.com/Szzrain/DingTalk-go v0.0.8-alpha @@ -12,10 +12,10 @@ require ( github.com/alexmullins/zip v0.0.0-20180717182244-4affb64b04d0 github.com/antlabs/strsim v0.0.3 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 - github.com/blevesearch/bleve/v2 v2.3.10 + github.com/blevesearch/bleve/v2 v2.4.3 github.com/bwmarrin/discordgo v0.28.1 - github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d - github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab + github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd + github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc github.com/evanw/esbuild v0.23.1 github.com/fy0/go-autostart v0.0.0-20220515100644-a25d81ed766b github.com/fy0/gojax v0.0.0-20221225152702-4140cf8509bd @@ -23,10 +23,10 @@ require ( github.com/fyrchik/go-shlex v0.0.0-20210215145004-cd7f49bfd959 github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc github.com/glebarez/go-sqlite v1.22.0 + github.com/glebarez/sqlite v1.11.0 github.com/go-creed/sat v1.0.3 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/golang-module/carbon v1.7.3 - github.com/gonutz/w32/v2 v2.11.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.1 github.com/grokify/html-strip-tags-go v0.0.1 @@ -39,7 +39,7 @@ require ( github.com/lonelyevil/kook/log_adapter/plog v0.0.31 github.com/lxn/win v0.0.0-20210218163916-a377121e959e github.com/matoous/go-nanoid/v2 v2.1.0 - github.com/mattn/go-sqlite3 v1.14.23 + github.com/mattn/go-sqlite3 v1.14.24 github.com/mitchellh/mapstructure v1.5.0 github.com/monaco-io/request v1.0.16 github.com/mozillazg/go-pinyin v0.20.0 @@ -54,50 +54,69 @@ require ( github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 github.com/sahilm/fuzzy v0.1.1 github.com/samber/lo v1.44.0 - github.com/schollz/progressbar/v3 v3.14.6 + github.com/schollz/progressbar/v3 v3.17.0 github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083 github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c - github.com/slack-go/slack v0.13.0 + github.com/slack-go/slack v0.15.0 github.com/sunshineplan/imgconv v1.1.4 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tdewolff/minify/v2 v2.20.37 github.com/tidwall/buntdb v1.3.1 github.com/vmihailenco/msgpack v4.0.4+incompatible - github.com/xuri/excelize/v2 v2.8.1 + github.com/xuri/excelize/v2 v2.9.0 github.com/yuin/goldmark v1.7.4 - go.etcd.io/bbolt v1.3.9 + go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.29.0 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 - golang.org/x/sys v0.25.0 - golang.org/x/text v0.18.0 + golang.org/x/sys v0.27.0 + golang.org/x/text v0.20.0 golang.org/x/time v0.5.0 gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/blevesearch/bleve_index_api v1.1.13 + github.com/go-gorm/caches/v4 v4.0.5 + github.com/gofrs/flock v0.12.1 + github.com/gonutz/w32/v2 v2.11.1 + github.com/joho/godotenv v1.5.1 + github.com/spaolacci/murmur3 v1.1.0 + github.com/tidwall/gjson v1.17.0 + github.com/tidwall/sjson v1.2.5 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.11 + moul.io/zapfilter v1.7.0 ) require ( - github.com/RoaringBitmap/roaring v1.2.3 // indirect - github.com/bits-and-blooms/bitset v1.2.2 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/RoaringBitmap/roaring v1.9.3 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/bits-and-blooms/bloom/v3 v3.2.0 // indirect - github.com/blevesearch/bleve_index_api v1.0.6 // indirect - github.com/blevesearch/geo v0.1.18 // indirect + github.com/blevesearch/geo v0.1.20 // indirect + github.com/blevesearch/go-faiss v1.0.23 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.0.10 // indirect + github.com/blevesearch/vellum v1.0.11 // indirect github.com/blevesearch/zapx/v11 v11.3.10 // indirect github.com/blevesearch/zapx/v12 v12.3.10 // indirect github.com/blevesearch/zapx/v13 v13.3.10 // indirect github.com/blevesearch/zapx/v14 v14.3.10 // indirect - github.com/blevesearch/zapx/v15 v15.3.13 // indirect + github.com/blevesearch/zapx/v15 v15.3.16 // indirect + github.com/blevesearch/zapx/v16 v16.1.8 // indirect github.com/disintegration/imaging v1.6.2 // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a // indirect github.com/elazarl/goproxy/ext v0.0.0-20230808193330-2592e75ae04a // indirect @@ -107,9 +126,10 @@ require ( github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect - github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect - github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/gobuffalo/envy v1.7.0 // indirect @@ -118,18 +138,23 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/geo v0.0.0-20230404232722-c4acd7a044dc // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect - github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7 // indirect - github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae // indirect - github.com/joho/godotenv v1.3.0 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/tiff v1.0.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -137,19 +162,19 @@ require ( github.com/mschoch/smat v0.2.0 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect - github.com/pdfcpu/pdfcpu v0.4.0 // indirect + github.com/pdfcpu/pdfcpu v0.8.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d // indirect - github.com/sunshineplan/pdf v1.0.3 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/sunshineplan/pdf v1.0.7 // indirect github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tdewolff/parse/v2 v2.7.15 // indirect github.com/tidwall/btree v1.7.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/grect v0.1.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -157,14 +182,14 @@ require ( github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect - github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/image v0.18.0 // indirect + golang.org/x/image v0.19.0 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/term v0.24.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/term v0.26.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect @@ -178,6 +203,10 @@ require ( replace ( github.com/Szzrain/dodo-open-go v0.2.7 => github.com/sealdice/dodo-open-go v0.2.8 + // Try to fix arm64 bug with better snappy. + github.com/blevesearch/zapx/v16 v16.1.8 => github.com/PaienNate/zapx/v16 v16.1.9 + // Try to fix sqlite in cgofree + github.com/glebarez/sqlite v1.11.0 => github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685 github.com/lonelyevil/kook v0.0.31 => github.com/sealdice/kook v0.0.3 github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 => github.com/fy0/GoWebsocket v0.0.0-20231128163937-aa5c110b25c6 ) diff --git a/go.sum b/go.sum index 27aa379c..8f9459d1 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Milly/go-base2048 v0.1.0 h1:7ZgpCR3cjcAAVqIo+B8Q3P1+VFHRS8zilzAq062rUUk= github.com/Milly/go-base2048 v0.1.0/go.mod h1:kl6eYBwGnoIjv8k9UmgS+bekm6870ojptcVnT11e3jE= -github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY= -github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= +github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685 h1:O6OPpCufcEJD+eWENAwuwVZHONKmP77X2wlzMTQZ5gg= +github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685/go.mod h1:GajiCpqLxU0a1gP13oAEiJAx9r87kVSdfEQy4O69ZTo= +github.com/PaienNate/zapx/v16 v16.1.9 h1:GA4jIOx9OPFqTGym7ucqKNNSw01BaIyOIjDPxR3w47A= +github.com/PaienNate/zapx/v16 v16.1.9/go.mod h1:zuxVgVaLZ0g4lZvrv06xDc24N6nLCOzXYHVkXI7LMHM= +github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= +github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a h1:NPnGVqpua4c1iEFVdxnBJA9viP5bo2Zp2jfflbcjdto= github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a/go.mod h1:5LI6VqIHoGmWsR0EJLbct5bBrtM/0pTonaAyGKmFk9U= github.com/Szzrain/DingTalk-go v0.0.8-alpha h1:mSR/ORDDjtndoR12WrEdd3hdxxXXm9VMQ/r75NJkkkE= @@ -23,33 +28,37 @@ github.com/antlabs/strsim v0.0.3/go.mod h1:bIcymn+2jtt01korFun0bs8PsYZeQa82aHoYM github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk= github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bloom/v3 v3.2.0 h1:N+g3GTQ0TVbghahYyzwkQbMZR+IwIwFFC8dpIChtN0U= github.com/bits-and-blooms/bloom/v3 v3.2.0/go.mod h1:MC8muvBzzPOFsrcdND/A7kU7kMhkqb9KI70JlZCP+C8= -github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg= -github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA= -github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8= -github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms= -github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw= -github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM= +github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4= +github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY= +github.com/blevesearch/bleve_index_api v1.1.13 h1:+nrA6oRJr85aCPyqaeZtsruObwKojutfonHJin/BP48= +github.com/blevesearch/bleve_index_api v1.1.13/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= +github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= +github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= +github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc= +github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.1.6 h1:CdekX/Ob6YCYmeHzD72cKpwzBjvkOGegHOqhAkXp6yA= -github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc= +github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= +github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= -github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= +github.com/blevesearch/vellum v1.0.11 h1:SJI97toEFTtA9WsDZxkyGTaBWFdWl1n2LEDCXLCq/AU= +github.com/blevesearch/vellum v1.0.11/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= @@ -58,16 +67,15 @@ github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIq github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= -github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ= -github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= +github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= +github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= -github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -79,17 +87,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= -github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= -github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= -github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= -github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab h1:LrVf0AFnp5WiGKJ0a6cFf4RwNIN327uNUeVGJtmAFEE= -github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE= +github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co= +github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= @@ -129,14 +132,18 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/go-creed/sat v1.0.3 h1:V1IkiYYFDPKXaRhdg95oAh5IHZ3Qhs5AEVlhteM+6XA= github.com/go-creed/sat v1.0.3/go.mod h1:ZxAhQ0ikMzjqeMbFeoMdCr6es8p10Y87F2nHkqNjSbY= -github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-gorm/caches/v4 v4.0.5 h1:Sdj9vxbEM0sCmv5+s5o6GzoVMuraWF0bjJJvUU+7c1U= +github.com/go-gorm/caches/v4 v4.0.5/go.mod h1:Ms8LnWVoW4GkTofpDzFH8OfDGNTjLxQDyxBmRN67Ujw= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= @@ -158,6 +165,8 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U= @@ -173,8 +182,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gonutz/w32/v2 v2.11.1 h1:plG738ZY7VIkPGf3adZ6lFeAf2evCKrULKyZT5GrPoc= @@ -186,10 +195,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -200,24 +209,35 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7 h1:oYOKPR69u1kReWwnVhZlkduTrEtXRYJTDj5rUCMyPLY= -github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= -github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae h1:cpxrFNY+FIz7W4nuaG5McM/OyOBQt44Thl0Q/hFBhGo= -github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae/go.mod h1:zluYmeCkNexc8HFzfc2MTVwA8gcPuFQp/ngjvIQ0CFo= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= +github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juliangruber/go-intersect v1.1.0 h1:sc+y5dCjMMx0pAdYk/N6KBm00tD/f3tq+Iox7dYDUrY= github.com/juliangruber/go-intersect v1.1.0/go.mod h1:WMau+1kAmnlQnKiikekNJbtGtfmILU/mMU6H7AgKbWQ= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= @@ -226,8 +246,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -254,11 +274,11 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -297,16 +317,19 @@ github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= -github.com/pdfcpu/pdfcpu v0.4.0 h1:381iGNvMeLP+GFqIAqgd0LSj36AsK3JH4UTaF6D5jRc= -github.com/pdfcpu/pdfcpu v0.4.0/go.mod h1:9NDeS6hrCheauxw6YUlzgL/q6At2+PMzUKyFcfUzLLY= +github.com/pdfcpu/pdfcpu v0.8.1 h1:AiWUb8uXlrXqJ73OmiYXBjDF0Qxt4OuM281eAfkAOMA= +github.com/pdfcpu/pdfcpu v0.8.1/go.mod h1:M5SFotxdaw0fedxthpjbA/PADytAo6wJnGH0SSBWJ7s= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/phuslu/log v1.0.80/go.mod h1:kzJN3LRifrepxThMjufQwS7S35yFAB+jAV1qgA7eBW4= github.com/phuslu/log v1.0.88 h1:kivXMpYQ2hd9BxiJNhRM5xnaEZaGunQYlnRQdk/aBw8= github.com/phuslu/log v1.0.88/go.mod h1:F8osGJADo5qLK/0F88djWwdyoZZ9xDJQL1HYRHFEkS0= +github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -316,8 +339,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= -github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -326,7 +349,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -336,18 +358,19 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA= github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= -github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs= -github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= +github.com/schollz/progressbar/v3 v3.17.0 h1:Fv+vG6O6jnJwdjCelvfyYO7sF2jaUGQVmdH4CxcZdsQ= +github.com/schollz/progressbar/v3 v3.17.0/go.mod h1:5H4fLgifX+KeQCsEJnZTOepgZLe1jFF1lpPXb68IJTA= github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083 h1:s/jzCGYlM/0+TYTXwva5574EFnIv/ggPCoXHFpdbSUw= github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083/go.mod h1:MGtR0REQDslBwQE+Rln4P9iDjH/ZInlu5qzOLdvBWJU= github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c h1:Z+H+yMma3IcZfX2nLF7nOP50XWmOytLVlaIkT7QgbsA= github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c/go.mod h1:uof752qJvEQ4Kze+NVag+RKGgj5C4K3kMHoK3e2vOLg= github.com/sealdice/kook v0.0.3 h1:STMtiKRMFjhSFmUxi0BU5ktNkCQ8qi7Y5EEfrmYKvWY= github.com/sealdice/kook v0.0.3/go.mod h1:WjHC7AmbmNjInT/U/etBVOmAw7T6EqdCwApceRGs1sk= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ= -github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0= +github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -363,22 +386,25 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/sunshineplan/imgconv v1.1.4 h1:lViOZUbDIgW8o74naySXJqZOFgXSW1AdU/cdzZRnVTo= github.com/sunshineplan/imgconv v1.1.4/go.mod h1:Bc4qh4Z+nslcq+Csck01QZgzWvirKUdltRI7vnEAKd8= -github.com/sunshineplan/pdf v1.0.3 h1:Ng+/f35i0jlB87STk6sXaINqhF0JsIyXLZntWWOcGhg= -github.com/sunshineplan/pdf v1.0.3/go.mod h1:4JqkeywDS6kIsqODkNKZ847P2K8eRpSSzf12FTRmUVg= +github.com/sunshineplan/pdf v1.0.7 h1:62xlc079jh4tGLDjiihyyhwVFkn0IsxLyDpHplbG9Ew= +github.com/sunshineplan/pdf v1.0.7/go.mod h1:QsEmZCWBE3uFK8PCrM0pua1WDWLNU77YusiDEcY56OQ= github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906 h1:+yYRCj+PGQNnnen4+/Q7eKD2J87RJs+O39bjtHhPauk= github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906/go.mod h1:O+Ar7ouRbdfxLgoZLFz447/dvdM1NVKk1VpOQaijvAU= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= @@ -387,18 +413,22 @@ github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGp github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o= github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -406,6 +436,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -416,21 +448,29 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+ github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= -github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ= -github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE= -github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= -github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= -go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -441,14 +481,17 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= +golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= @@ -467,15 +510,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -483,6 +527,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -493,9 +538,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -505,18 +550,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -525,15 +568,18 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -570,12 +616,23 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276 h1:IHpexPpZZkm4NqbKneioNEYxTpOGZnDm8HPjabyX+Uw= +gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= @@ -584,3 +641,5 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +moul.io/zapfilter v1.7.0 h1:7aFrG4N72bDH9a2BtYUuUaDS981Dxu3qybWfeqaeBDU= +moul.io/zapfilter v1.7.0/go.mod h1:M+N2s+qZiA+bzRoyKMVRxyuERijS2ovi2pnMyiOGMvc= diff --git a/install.go b/install.go index ef667756..4c1e675f 100644 --- a/install.go +++ b/install.go @@ -45,31 +45,30 @@ func serviceInstall(isInstall bool, serviceName string, user string) { } prg := &program{} - fmt.Println("正在试图访问系统服务 ...") + fmt.Fprintln(os.Stdout, "正在试图访问系统服务 ...") s, err := service.New(prg, svcConfig) if isInstall { - fmt.Println("正在安装系统服务,安装完成后,SealDice将自动随系统启动") + fmt.Fprintln(os.Stdout, "正在安装系统服务,安装完成后,SealDice将自动随系统启动") if err != nil { - fmt.Printf("安装失败: %s\n", err.Error()) + fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error()) } _, err = s.Logger(nil) if err != nil { - fmt.Printf("安装失败: %s\n", err.Error()) - fmt.Println(err) + fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error()) } err = s.Install() if err != nil { - fmt.Printf("安装失败: %s\n", err.Error()) + fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error()) return } - fmt.Println("安装完成,正在启动……") + fmt.Fprintln(os.Stdout, "安装完成,正在启动……") _ = s.Start() } else { - fmt.Println("正在卸载系统服务……") + fmt.Fprintln(os.Stdout, "正在卸载系统服务……") _ = s.Stop() _ = s.Uninstall() - fmt.Println("系统服务已删除") + fmt.Fprintln(os.Stdout, "系统服务已删除") } } diff --git a/logger.go b/logger.go deleted file mode 100644 index e882563c..00000000 --- a/logger.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "os" - - "github.com/natefinch/lumberjack" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -func MainLoggerInit(path string, enableConsoleLog bool) { - lumlog := &lumberjack.Logger{ - Filename: path, - MaxSize: 10, // megabytes - MaxBackups: 3, // number of log files - MaxAge: 7, // days - } - - encoder := getEncoder() - cores := []zapcore.Core{ - zapcore.NewCore(encoder, zapcore.AddSync(lumlog), zapcore.DebugLevel), - } - - if enableConsoleLog { - pe2 := zap.NewProductionEncoderConfig() - pe2.EncodeTime = zapcore.ISO8601TimeEncoder - - consoleEncoder := zapcore.NewConsoleEncoder(pe2) - cores = append(cores, zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zapcore.InfoLevel)) - } - - core := zapcore.NewTee(cores...) - - loggerRaw := zap.New(core, zap.AddCaller()) - defer func(loggerRaw *zap.Logger) { - _ = loggerRaw.Sync() - }(loggerRaw) // flushes buffer, if any - - logger = loggerRaw.Sugar() - logger.Infow("核心日志开始记录") -} - -var logger *zap.SugaredLogger - -func getEncoder() zapcore.Encoder { - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder - - return zapcore.NewConsoleEncoder(encoderConfig) -} diff --git a/main.go b/main.go index 932317a6..12dd4bd4 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main +// _ "net/http/pprof" import ( + "errors" "fmt" "io/fs" "mime" @@ -15,44 +17,51 @@ import ( "syscall" "time" - // _ "net/http/pprof" - + "github.com/gofrs/flock" "github.com/jessevdk/go-flags" + "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - "go.uber.org/zap" "go.uber.org/zap/zapcore" "sealdice-core/api" "sealdice-core/dice" - diceLogger "sealdice-core/dice/logger" "sealdice-core/dice/model" "sealdice-core/migrate" "sealdice-core/static" "sealdice-core/utils/crypto" + log "sealdice-core/utils/kratos" + "sealdice-core/utils/oschecker" + "sealdice-core/utils/paniclog" ) -/** +/* +* 二进制目录结构: data/configs data/extensions data/logs - extensions/ */ +var sealLock = flock.New("sealdice-lock.lock") + func cleanupCreate(diceManager *dice.DiceManager) func() { return func() { - logger.Info("程序即将退出,进行清理……") + log.Info("程序即将退出,进行清理……") err := recover() if err != nil { showWindow() - logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack())) + log.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack())) // 顺便修正一下上面这个,应该是木落忘了。 if runtime.GOOS == "windows" { exec.Command("pause") // windows专属 } } + err = sealLock.Unlock() + if err != nil { + log.Errorf("文件锁归还出现异常 %v", err) + } if !diceManager.CleanupFlag.CompareAndSwap(0, 1) { // 尝试更新cleanup标记,如果已经为1则退出 @@ -61,15 +70,16 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { for _, i := range diceManager.Dice { if i.IsAlreadyLoadConfig { - i.BanList.SaveChanged(i) + i.Config.BanList.SaveChanged(i) i.Save(true) + i.AttrsManager.Stop() for _, j := range i.ExtList { if j.Storage != nil { // 关闭 err := j.StorageClose() if err != nil { showWindow() - logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack())) + log.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack())) // 木落没有加该检查 补充上 if runtime.GOOS == "windows" { exec.Command("pause") // windows专属 @@ -90,7 +100,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { dbData := d.DBData if dbData != nil { d.DBData = nil - _ = dbData.Close() + db, err := dbData.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -101,7 +115,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { dbLogs := d.DBLogs if dbLogs != nil { d.DBLogs = nil - _ = dbLogs.Close() + db, err := dbLogs.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -113,7 +131,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { if cm != nil && cm.DB != nil { dbCensor := cm.DB cm.DB = nil - _ = dbCensor.Close() + db, err := dbCensor.DB() + if err != nil { + return + } + _ = db.Close() } })() } @@ -192,18 +214,46 @@ func main() { LogLevel int8 `long:"log-level" description:"设置日志等级" default:"0" choice:"-1" choice:"0" choice:"1" choice:"2" choice:"3" choice:"4" choice:"5"` ContainerMode bool `long:"container-mode" description:"容器模式,该模式下禁用内置客户端"` } - + // 读取命令行传参 _, err := flags.ParseArgs(&opts, os.Args) if err != nil { return } - + // 提前到最开始初始化所有日志 + // 1. 初始化全局Kartos日志 + log.InitZapWithKartosLog(zapcore.Level(opts.LogLevel)) + // 2. 初始化全局panic捕获日志 + paniclog.InitPanicLog() + // 3. 提示日志打印 + log.Info("运行日志开始记录,海豹出现故障时可查看 data/main.log 与 data/panic.log 获取更多信息") + // 加载env相关 + err = godotenv.Load() + if err != nil { + log.Errorf("未读取到.env参数,若您未使用docker或第三方数据库,可安全忽略。") + } + // 初始化文件加锁系统 + locked, err := sealLock.TryLock() + // 如果有错误,或者未能取到锁 + if err != nil || !locked { + // 打日志的时候防止打出nil + if err == nil { + err = errors.New("海豹正在运行中") + } + log.Errorf("获取锁文件失败,原因为: %v", err) + showMsgBox("获取锁文件失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份海豹程序!") + return + } + judge, osr := oschecker.OldVersionCheck() + // 预留收集信息的接口,如果有需要可以考虑从这里拿数据。不从这里做提示的原因是Windows和Linux的展示方式不同。 + if judge { + log.Info(osr) + } if opts.Version { - fmt.Println(dice.VERSION.String()) + fmt.Fprintln(os.Stdout, dice.VERSION.String()) return } if opts.DBCheck { - model.DBCheck("data/default") + model.DBCheck() return } if opts.VacuumDB { @@ -212,14 +262,14 @@ func main() { } if opts.ShowEnv { for i, e := range os.Environ() { - println(i, e) + fmt.Fprintln(os.Stdout, i, e) } return } deleteOldWrongFile() if opts.Delay != 0 { - fmt.Println("延迟启动", opts.Delay, "秒") + log.Infof("延迟启动 %d 秒", opts.Delay) time.Sleep(time.Duration(opts.Delay) * time.Second) } @@ -228,15 +278,12 @@ func main() { } _ = os.MkdirAll("./data", 0o755) - MainLoggerInit("./data/main.log", true) - - diceLogger.SetEnableLevel(zapcore.Level(opts.LogLevel)) // 提早初始化是为了读取ServiceName diceManager := &dice.DiceManager{} if opts.ContainerMode { - logger.Info("当前为容器模式,内置适配器与更新功能已被禁用") + log.Info("当前为容器模式,内置适配器与更新功能已被禁用") diceManager.ContainerMode = true } @@ -244,7 +291,7 @@ func main() { diceManager.IsReady = true if opts.Address != "" { - fmt.Println("由参数输入了服务地址:", opts.Address) + log.Infof("由参数输入了服务地址: %s", opts.Address) diceManager.ServeAddress = opts.Address } @@ -281,17 +328,17 @@ func main() { // 只有不同文件才进行校验 // windows平台旧版本到1.4.0流程 _ = os.WriteFile("./升级失败指引.txt", []byte("如果升级成功不用理会此文档,直接删除即可。\r\n\r\n如果升级后无法启动,或再次启动后恢复到旧版本,先不要紧张。\r\n你升级前的数据备份在backups目录。\r\n如果无法启动,请删除海豹目录中的\"update\"、\"auto_update.exe\"并手动进行升级。\n如果升级成功但在再次重启后回退版本,同上。\n\n如有其他问题可以加企鹅群询问:524364253 562897832"), 0o644) - logger.Warn("检测到 auto_update.exe,即将自动退出当前程序并进行升级") - logger.Warn("程序目录下会出现“升级日志.log”,这代表升级正在进行中,如果失败了请检查此文件。") + log.Warn("检测到 auto_update.exe,即将自动退出当前程序并进行升级") + log.Warn("程序目录下会出现“升级日志.log”,这代表升级正在进行中,如果失败了请检查此文件。") - err := CheckUpdater(diceManager) + err = CheckUpdater(diceManager) if err != nil { - logger.Error("升级程序检查失败: ", err.Error()) + log.Error("升级程序检查失败: ", err.Error()) } else { _ = os.Remove("./auto_update.exe") // ui资源已经内置,删除旧的ui文件,这里有点风险,但是此时已经不考虑升级失败的情况 _ = os.RemoveAll("./frontend") - UpdateByFile(diceManager, nil, "./update/update.zip", true) + UpdateByFile(diceManager, "./update/update.zip", true) } return } @@ -306,14 +353,14 @@ func main() { } if doNext { - err := CheckUpdater(diceManager) + err = CheckUpdater(diceManager) if err != nil { - logger.Error("升级程序检查失败: ", err.Error()) + log.Error("升级程序检查失败: ", err.Error()) } else { _ = os.Remove("./auto_update") // ui资源已经内置,删除旧的ui文件,这里有点风险,但是此时已经不考虑升级失败的情况 _ = os.RemoveAll("./frontend") - UpdateByFile(diceManager, nil, "./update/update.tar.gz", true) + UpdateByFile(diceManager, "./update/update.tar.gz", true) } return } @@ -321,28 +368,27 @@ func main() { removeUpdateFiles() if opts.UpdateTest { - err := CheckUpdater(diceManager) + err = CheckUpdater(diceManager) if err != nil { - logger.Error("升级程序检查失败: ", err.Error()) + log.Error("升级程序检查失败: ", err.Error()) } else { - UpdateByFile(diceManager, nil, "./xx.zip", true) + UpdateByFile(diceManager, "./xx.zip", true) } } // 先临时放这里,后面再整理一下升级模块 - diceManager.UpdateSealdiceByFile = func(packName string, log *zap.SugaredLogger) bool { - err := CheckUpdater(diceManager) + diceManager.UpdateSealdiceByFile = func(packName string, log *log.Helper) bool { + err = CheckUpdater(diceManager) if err != nil { - logger.Error("升级程序检查失败: ", err.Error()) + log.Error("升级程序检查失败: ", err.Error()) return false } else { - return UpdateByFile(diceManager, log, packName, false) + return UpdateByFile(diceManager, packName, false) } } cwd, _ := os.Getwd() - fmt.Printf("%s %s\n", dice.APPNAME, dice.VERSION.String()) - fmt.Println("工作路径: ", cwd) + log.Info(dice.APPNAME, dice.VERSION.String(), "当前工作路径: ", cwd) if strings.HasPrefix(cwd, os.TempDir()) { // C:\Users\XXX\AppData\Local\Temp @@ -353,47 +399,51 @@ func main() { useBuiltinUI := false checkFrontendExists := func() bool { - stat, err := os.Stat("./frontend_overwrite") + var stat os.FileInfo + stat, err = os.Stat("./frontend_overwrite") return err == nil && stat.IsDir() } if !checkFrontendExists() { - logger.Info("未检测到外置的UI资源文件,将使用内置资源启动UI") + log.Info("未检测到外置的UI资源文件,将使用内置资源启动UI") useBuiltinUI = true } else { - logger.Info("检测到外置的UI资源文件,将使用frontend_overwrite文件夹内的资源启动UI") + log.Info("检测到外置的UI资源文件,将使用frontend_overwrite文件夹内的资源启动UI") } // 删除遗留的shm和wal文件 - if !model.DBCacheDelete() { - logger.Error("数据库缓存文件删除失败") - showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!") - return - } + // if !model.DBCacheDelete() { + // log.Error("数据库缓存文件删除失败") + // showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!") + // return + // } // 尝试进行升级 migrate.TryMigrateToV12() // 尝试修正log_items表的message字段类型 if migrateErr := migrate.LogItemFixDatatype(); migrateErr != nil { - logger.Errorf("修正log_items表时出错,%s", migrateErr.Error()) + log.Fatalf("修正log_items表时出错,%s", migrateErr.Error()) return } // v131迁移历史设置项到自定义文案 if migrateErr := migrate.V131DeprecatedConfig2CustomText(); migrateErr != nil { - logger.Errorf("迁移历史设置项时出错,%s", migrateErr.Error()) + log.Fatalf("迁移历史设置项时出错,%s", migrateErr.Error()) return } // v141重命名刷屏警告字段 if migrateErr := migrate.V141DeprecatedConfigRename(); migrateErr != nil { - logger.Errorf("迁移历史设置项时出错,%s", migrateErr.Error()) + log.Fatalf("迁移历史设置项时出错,%s", migrateErr.Error()) return } // v144删除旧的帮助文档 if migrateErr := migrate.V144RemoveOldHelpdoc(); migrateErr != nil { - logger.Errorf("移除旧帮助文档时出错,%v", migrateErr) + log.Fatalf("移除旧帮助文档时出错,%v", migrateErr) } // v150升级 - if !migrate.V150Upgrade() { - return + err = migrate.V150Upgrade() + if err != nil { + // Fatalf将会退出程序...或许应该用Errorf一类的吗? + log.Fatalf("您的146数据库可能存在问题,为保护数据,已经停止执行150升级命令。请尝试联系开发者,并提供你的日志。\n"+ + "数据已回滚,您可暂时使用旧版本等待进一步的修复和更新。您的报错内容为: %v", err) } if !opts.ShowConsole || opts.MultiInstanceOnWindows { @@ -437,7 +487,7 @@ func main() { })() if opts.Address != "" { - fmt.Println("由参数输入了服务地址:", opts.Address) + log.Infof("由参数输入了服务地址: %s", opts.Address) } for _, d := range diceManager.Dice { @@ -454,7 +504,7 @@ func main() { // err = nil // err = http.ListenAndServe(":9090", nil) // if err != nil { - // fmt.Printf("ListenAndServe: %s", err) + // fmt.Fprintf(os.Stdout, "ListenAndServe: %s", err) // } // darwin 的托盘菜单似乎需要在主线程启动才能工作,调整到这里 @@ -475,7 +525,7 @@ func removeUpdateFiles() { func diceServe(d *dice.Dice) { defer dice.CrashLog() if len(d.ImSession.EndPoints) == 0 { - d.Logger.Infof("未检测到任何帐号,请先到“帐号设置”进行添加") + log.Infof("未检测到任何帐号,请先到“帐号设置”进行添加") } d.UIEndpoint = new(dice.EndPointInfo) @@ -502,7 +552,7 @@ func diceServe(d *dice.Dice) { } if conn.EndPointInfoBase.ProtocolType == "onebot" { pa := conn.Adapter.(*dice.PlatformAdapterGocq) - if pa.BuiltinMode == "lagrange" { + if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" { dice.LagrangeServe(d, conn, dice.LagrangeLoginInfo{ IsAsyncRun: true, }) @@ -558,13 +608,13 @@ func diceServe(d *dice.Dice) { } func uiServe(dm *dice.DiceManager, hideUI bool, useBuiltin bool) { - logger.Info("即将启动webui") + log.Info("即将启动webui") // Echo instance e := echo.New() - // Middleware - // e.Use(middleware.Logger()) - // e.Use(middleware.Recover()) + // 为UI添加日志,以echo方式输出 + echoHelper := log.GetWebLogger() + e.Use(log.EchoMiddleLogger(echoHelper)) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ Skipper: middleware.DefaultSkipper, AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, "token"}, diff --git a/migrate/convert_logs.go b/migrate/convert_logs.go index 4216f0d3..b099e6bf 100644 --- a/migrate/convert_logs.go +++ b/migrate/convert_logs.go @@ -4,7 +4,6 @@ import ( "encoding/binary" "encoding/json" "errors" - "fmt" "path/filepath" "strconv" "strings" @@ -12,6 +11,8 @@ import ( "github.com/jmoiron/sqlx" "go.etcd.io/bbolt" + + log "sealdice-core/utils/kratos" ) type LogOneItem struct { @@ -212,7 +213,7 @@ func LogAppend(ctx *MsgContext, group *GroupInfo, l *LogOneItem) error { return ctx.Dice.DB.Update(func(tx *bbolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte("logs")) if err != nil { - // ctx.Dice.Logger.Error("日志写入问题", err.Error()) + // ctx.Dice.Zlogger.Error("日志写入问题", err.Error()) return err } @@ -353,7 +354,6 @@ create index if not exists idx_log_items_log_id dbSQL, err := openDB(dbDataLogsPath) if err != nil { - fmt.Println("xxx", err) return err } defer func(dbSql *sqlx.DB) { @@ -380,7 +380,7 @@ create index if not exists idx_log_items_log_id }) }) - fmt.Println("群组数量", len(groupIds)) + log.Info("群组数量", len(groupIds)) times := 0 itemNumber := 0 @@ -409,14 +409,14 @@ create index if not exists idx_log_items_log_id } exec, errExec := dbSQL.NamedExec(`insert into logs (name, group_id, created_at, updated_at) VALUES (:name, :group_id, :created_at, :updated_at)`, args) if errExec != nil { - fmt.Println("错误:", errExec, i, j) + log.Error("错误:", errExec, i, j) return errExec } logID, _ := exec.LastInsertId() logNum++ if logNum%10 == 0 { - fmt.Printf("进度: %d\n", logNum) + log.Infof("进度: %d\n", logNum) } tx := dbSQL.MustBegin() @@ -450,15 +450,15 @@ create index if not exists idx_log_items_log_id } } - fmt.Println("群组数量", len(groupIds)) - fmt.Println("log完成", times) - fmt.Println("行数", itemNumber) + log.Info("群组数量", len(groupIds)) + log.Info("log完成", times) + log.Info("行数", itemNumber) err = dbSQL.Get(&num, "select count(id) from log_items") if err != nil { return err } - fmt.Println("行数确认", num) + log.Info("行数确认", num) _ = dbSQL.Close() return nil diff --git a/migrate/convert_serve.go b/migrate/convert_serve.go index 0e6329cc..7d0e5db8 100644 --- a/migrate/convert_serve.go +++ b/migrate/convert_serve.go @@ -69,7 +69,7 @@ type EndPointInfoBase struct { Enable bool `yaml:"enable" json:"enable"` // 是否启用 ProtocolType string `yaml:"protocolType"` // 协议类型,如onebot、koishi等 - IsPublic bool `yaml:"isPublic"` + IsPublic bool `yaml:"isPublic" json:"isPublic"` } type EndPointInfo struct { @@ -232,7 +232,7 @@ create table if not exists ban_info now := time.Now() nowTimestamp := now.Unix() - fmt.Println("处理serve.yaml") + fmt.Fprintln(os.Stdout, "处理serve.yaml") times := 0 dNew := &Dice{} @@ -240,7 +240,7 @@ create table if not exists ban_info tx := dbSql.MustBegin() for k, v := range dNew.ImSession.ServiceAtNew { - fmt.Println("群组", k) + fmt.Fprintln(os.Stdout, "群组", k) times += len(v.Players) for _, playerInfo := range v.Players { args := map[string]interface{}{ @@ -271,13 +271,13 @@ create table if not exists ban_info errTx := tx.Commit() if errTx != nil { - fmt.Println("???", errTx) + fmt.Fprintln(os.Stdout, "???", errTx) _ = tx.Rollback() } - fmt.Println("群组信息处理完成") - fmt.Println("群数量", len(dNew.ImSession.ServiceAtNew)) - fmt.Println("群成员数量", times) + fmt.Fprintln(os.Stdout, "群组信息处理完成") + fmt.Fprintln(os.Stdout, "群数量", len(dNew.ImSession.ServiceAtNew)) + fmt.Fprintln(os.Stdout, "群成员数量", times) } _ = os.WriteFile("./data/default/serve.yaml.old", data, 0644) @@ -289,7 +289,7 @@ create table if not exists ban_info _ = db.Close() }(db) - fmt.Println("处理属性部分") + fmt.Fprintln(os.Stdout, "处理属性部分") copyByName := func(table string) { times = 0 tx2 := dbSql.MustBegin() @@ -308,7 +308,7 @@ create table if not exists ban_info }) }) - fmt.Println("条目数量"+table, times) + fmt.Fprintln(os.Stdout, "条目数量"+table, times) if tx2.Commit() != nil { _ = tx2.Rollback() @@ -319,7 +319,7 @@ create table if not exists ban_info copyByName("attrs_group") copyByName("attrs_user") copyByName("attrs_group_user") - fmt.Println("完成") + fmt.Fprintln(os.Stdout, "完成") times = 0 tx2 := dbSql.MustBegin() @@ -356,8 +356,8 @@ create table if not exists ban_info _ = tx2.Rollback() } - fmt.Println("黑名单条目数量", times) - fmt.Println("完成") + fmt.Fprintln(os.Stdout, "黑名单条目数量", times) + fmt.Fprintln(os.Stdout, "完成") return nil } diff --git a/migrate/db_util.go b/migrate/db_util.go index 465f0f58..9ed44029 100644 --- a/migrate/db_util.go +++ b/migrate/db_util.go @@ -5,11 +5,24 @@ package migrate import ( _ "github.com/glebarez/go-sqlite" + "github.com/glebarez/sqlite" "github.com/jmoiron/sqlx" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/utils" ) func openDB(path string) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite", path) + gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + panic(err) + } + db, err := utils.GetSQLXDB(gdb) + // db, err := sqlx.Open("sqlite", path) if err != nil { panic(err) } diff --git a/migrate/db_util_cgo.go b/migrate/db_util_cgo.go index 4c9f17e4..4b8ba915 100644 --- a/migrate/db_util_cgo.go +++ b/migrate/db_util_cgo.go @@ -6,10 +6,22 @@ package migrate import ( "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/utils" ) func openDB(path string) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite3", path) + gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + panic(err) + } + db, err := utils.GetSQLXDB(gdb) if err != nil { panic(err) } diff --git a/migrate/log_item_fix_type.go b/migrate/log_item_fix_type.go index 5e2a37cd..76847c9c 100644 --- a/migrate/log_item_fix_type.go +++ b/migrate/log_item_fix_type.go @@ -60,8 +60,8 @@ func LogItemFixDatatype() error { return nil } - fmt.Println("开始修复log_items表message字段类型") - fmt.Println("【不要关闭海豹程序!】") + fmt.Fprintln(os.Stdout, "开始修复log_items表message字段类型") + fmt.Fprintln(os.Stdout, "【不要关闭海豹程序!】") done := make(chan interface{}, 1) @@ -88,7 +88,7 @@ func LogItemFixDatatype() error { } _, _ = db.Exec(`VACUUM;`) - fmt.Println("\n修复log_items表message字段类型成功") - fmt.Println("您现在可以正常使用海豹程序了") + fmt.Fprintln(os.Stdout, "\n修复log_items表message字段类型成功") + fmt.Fprintln(os.Stdout, "您现在可以正常使用海豹程序了") return nil } diff --git a/migrate/v12.go b/migrate/v12.go index 821c0d13..2e9fa821 100644 --- a/migrate/v12.go +++ b/migrate/v12.go @@ -11,9 +11,9 @@ func TryMigrateToV12() { return } - fmt.Println("检测到旧数据库存在,试图进行转换") + fmt.Fprintln(os.Stdout, "检测到旧数据库存在,试图进行转换") _ = ConvertServe() _ = ConvertLogs() _ = os.Remove("./data/default/data.bdb") - fmt.Println("V1.2 版本数据库升级完成") + fmt.Fprintln(os.Stdout, "V1.2 版本数据库升级完成") } diff --git a/migrate/v131_deprecated_config.go b/migrate/v131_deprecated_config.go index 5007dbe2..e94a565f 100644 --- a/migrate/v131_deprecated_config.go +++ b/migrate/v131_deprecated_config.go @@ -99,7 +99,7 @@ func V131DeprecatedConfig2CustomText() error { } if needUpdateCustomText { - fmt.Println("检测到旧设置项需要迁移到自定义文案,试图进行迁移") + fmt.Fprintln(os.Stdout, "检测到旧设置项需要迁移到自定义文案,试图进行迁移") // 保存修改了的 custom text 设置 newData, err := yaml.Marshal(customTexts) @@ -137,7 +137,7 @@ func V131DeprecatedConfig2CustomText() error { return err } - fmt.Println("旧设置项迁移到自定义文案成功") + fmt.Fprintln(os.Stdout, "旧设置项迁移到自定义文案成功") } return nil diff --git a/migrate/v144_remote_old_helpdoc.go b/migrate/v144_remote_old_helpdoc.go index ec15ddef..10eace17 100644 --- a/migrate/v144_remote_old_helpdoc.go +++ b/migrate/v144_remote_old_helpdoc.go @@ -19,29 +19,29 @@ func V144RemoveOldHelpdoc() error { return nil } if err != nil { - return fmt.Errorf("Get file info for %s failed: %w", oldName, err) + return fmt.Errorf("get file info for %s failed: %w", oldName, err) } _, err = os.Stat(newName) if errors.Is(err, os.ErrNotExist) { - fmt.Printf("New helpdoc %s not found. Skip removing old helpdoc %s\n", newName, oldName) + fmt.Fprintf(os.Stdout, "New helpdoc %s not found. Skip removing old helpdoc %s\n", newName, oldName) return nil } if err != nil { - return fmt.Errorf("Get file info for %s failed: %w", newName, err) + return fmt.Errorf("get file info for %s failed: %w", newName, err) } if crypto.Sha256Checksum(oldName) != oldSHA256 { - fmt.Printf("Old helpdoc %s checksum mismatch. You may have edited this file?\n", oldName) + fmt.Fprintf(os.Stdout, "Old helpdoc %s checksum mismatch. You may have edited this file?\n", oldName) return nil } if crypto.Sha256Checksum(newName) != newSHA256 { - fmt.Printf("New helpdoc %s checksum mismatch. Skip removing old helpdoc %s\n", newName, oldName) + fmt.Fprintf(os.Stdout, "New helpdoc %s checksum mismatch. Skip removing old helpdoc %s\n", newName, oldName) return nil } - fmt.Printf("Removing old helpdoc %s\n", oldName) + fmt.Fprintf(os.Stdout, "Removing old helpdoc %s\n", oldName) os.Remove(oldName) return nil } diff --git a/migrate/v150_attrs.go b/migrate/v150_attrs.go index 252fdc74..81e1d342 100644 --- a/migrate/v150_attrs.go +++ b/migrate/v150_attrs.go @@ -15,6 +15,8 @@ import ( "sealdice-core/dice" "sealdice-core/dice/model" "sealdice-core/utils" + + log "sealdice-core/utils/kratos" ) func convertToNew(name string, ownerId string, data []byte, updatedAt int64) (*model.AttributesItemModel, error) { @@ -121,8 +123,8 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) { _, userIdPart, ok := dice.UnpackGroupUserId(id) if !ok { countFailed += 1 - fmt.Println("数据库读取出错,退出转换") - fmt.Println("ID解析失败: ", id) + fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换") + fmt.Fprintln(os.Stdout, "ID解析失败: ", id) continue } @@ -131,7 +133,7 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) { if err != nil { countFailed += 1 - fmt.Println("解析失败: ", string(data)) + fmt.Fprintln(os.Stdout, "解析失败: ", string(data)) continue } @@ -156,11 +158,10 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) { rawData, err := ds.NewDictVal(m).V().ToJSON() if err != nil { countFailed += 1 - fmt.Printf("群-用户 %s 的数据无法转换\n", id) + fmt.Fprintf(os.Stdout, "群-用户 %s 的数据无法转换\n", id) continue } - // fmt.Println("UnpackID:", id, " UserPart:", userIdPart, " Sheet:", sheetIdBindByGroupUserId[id]) item := &model.AttributesItemModel{ Id: id, Data: rawData, @@ -212,7 +213,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) { ) if err != nil { - fmt.Println("数据库读取出错,退出转换") + fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换") return count, countFailed, err } @@ -221,7 +222,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) { if err != nil { countFailed += 1 - fmt.Println("解析失败: ", string(data)) + fmt.Fprintln(os.Stdout, "解析失败: ", string(data)) continue } @@ -234,7 +235,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) { rawData, err := ds.NewDictVal(m).V().ToJSON() if err != nil { countFailed += 1 - fmt.Printf("群 %s 的数据无法转换\n", id) + fmt.Fprintf(os.Stdout, "群 %s 的数据无法转换\n", id) continue } @@ -283,7 +284,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { ) if err != nil { - fmt.Println("数据库读取出错,退出转换") + fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换") return count, countSheetsNum, countFailed, err } @@ -295,7 +296,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { continue } - // fmt.Println("数据转换-用户:", ownerId) var newSheetsList []*model.AttributesItemModel var sheetNameBindByGroupId = map[string]string{} @@ -314,7 +314,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { groupId := k[len("$:group-bind:"):] name, _ := v.ReadString() sheetNameBindByGroupId[groupId] = name - // fmt.Println("绑卡关联:", groupId, name) continue } if strings.HasPrefix(k, "$ch:") { @@ -324,7 +323,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { toNew, err = convertToNew(name, ownerId, []byte(v.ToString()), updatedAt) if err != nil { - fmt.Printf("用户 %s 的角色卡 %s 无法转换", ownerId, name) + fmt.Fprintf(os.Stdout, "用户 %s 的角色卡 %s 无法转换", ownerId, name) continue } newSheetsList = append(newSheetsList, toNew) @@ -337,7 +336,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { // 一次性,双循环罢 for groupID, j := range sheetNameBindByGroupId { if j == i.Name { - // fmt.Println("GUID:", fmt.Sprintf("%s-%s", groupID, ownerId), " sheetID:", i.Id) sheetIdBindByGroupUserId[fmt.Sprintf("%s-%s", groupID, ownerId)] = i.Id } } @@ -347,7 +345,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { for _, i := range newSheetsList { _, err = AttrsNewItem(db, i) if err != nil { - fmt.Printf("用户 %s 的角色卡 %s 无法写入数据库: %s\n", ownerId, i.Name, err.Error()) + fmt.Fprintf(os.Stdout, "用户 %s 的角色卡 %s 无法写入数据库: %s\n", ownerId, i.Name, err.Error()) } } @@ -355,7 +353,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) { rawData, err := ds.NewDictVal(m).V().ToJSON() if err != nil { countFailed += 1 - fmt.Printf("用户 %s 的个人数据无法转换\n", ownerId) + fmt.Fprintf(os.Stdout, "用户 %s 的个人数据无法转换\n", ownerId) continue } @@ -395,110 +393,132 @@ func checkTableExists(db *sqlx.DB, tableName string) (bool, error) { } } -func V150Upgrade() bool { +// Pinenutn: 2024-10-28 我要把这个注释全文背诵,它扰乱了GORM的初始化逻辑 +// -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 +var v150sqls = []string{ + ` +CREATE TABLE IF NOT EXISTS attrs ( + id TEXT PRIMARY KEY, + data BYTEA, + attrs_type TEXT, + binding_sheet_id TEXT default '', + name TEXT default '', + owner_id TEXT default '', + sheet_type TEXT default '', + is_hidden BOOLEAN default FALSE, + created_at INTEGER default 0, + updated_at INTEGER default 0 +); +`, + `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`, + `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`, + `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`, +} + +func V150Upgrade() error { dbDataPath, _ := filepath.Abs("./data/default/data.db") if _, err := os.Stat(dbDataPath); errors.Is(err, os.ErrNotExist) { - return true + log.Error("未找到旧版本数据库,若您启动全新海豹,可安全忽略。") + return nil } db, err := openDB(dbDataPath) if err != nil { - fmt.Println("升级失败,无法打开数据库:", err) - return false + return fmt.Errorf("升级失败,无法打开数据库: %w", err) + } + defer db.Close() + + tx, err := db.Beginx() + if err != nil { + return fmt.Errorf("创建事务失败: %w", err) } defer func() { - _ = db.Close() + if p := recover(); p != nil { + err = tx.Rollback() + if err != nil { + log.Errorf("回滚事务时出错: %v", err) + } + panic(p) // 继续传播 panic + } else if err != nil { + log.Errorf("日志处理时出现异常行为: %v", err) + err = tx.Rollback() + if err != nil { + log.Errorf("回滚事务时出错: %v", err) + return + } + } else { + err = tx.Commit() + if err != nil { + log.Errorf("提交事务时出错: %v", err) + } + } }() exists, err := checkTableExists(db, "attrs") if err != nil { - fmt.Println("V150数据转换未知错误:", err.Error()) - return false + return fmt.Errorf("检查表是否存在时出错: %w", err) } + // 特判146->150的倒霉蛋 + exists146, err := checkTableExists(db, "attrs_group") + if exists { - // 表格已经存在,说明转换完成,退出 - return true + if exists146 { + log.Errorf("1.4.6的数据部分迁移!您可能是150部分版本的受害者,请联系开发者") + return errors.New("150和146的数据库共同存在,请联系开发者") + } + // 表格已经存在,说明转换完成 + return nil } - fmt.Println("1.5 数据迁移") + log.Info("1.5 数据迁移") sheetIdBindByGroupUserId = map[string]string{} - sqls := []string{ - ` -CREATE TABLE IF NOT EXISTS attrs ( - id TEXT PRIMARY KEY, - data BYTEA, - attrs_type TEXT, - - -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 - binding_sheet_id TEXT default '', - - name TEXT default '', - owner_id TEXT default '', - sheet_type TEXT default '', - is_hidden BOOLEAN default FALSE, - - created_at INTEGER default 0, - updated_at INTEGER default 0 -); -`, - `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`, - `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`, - `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`, - } - for _, i := range sqls { - _, _ = db.Exec(i) - } - - tx, err := db.Beginx() - if err != nil { - fmt.Println("V150数据转换创建事务失败:", err.Error()) - return false + for _, singleSql := range v150sqls { + if _, err = tx.Exec(singleSql); err != nil { + return fmt.Errorf("执行 SQL 出错: %w", err) + } } - if exists, _ := checkTableExists(db, "attrs_user"); exists { - count, countSheetsNum, countFailed, err2 := attrsUserMigrate(tx) - fmt.Printf("数据卡转换 - 角色卡,成功人数%d 失败人数 %d 卡数 %d\n", count, countFailed, countSheetsNum) - if err2 != nil { - fmt.Println("异常", err2.Error()) - return false + if exists, _ = checkTableExists(db, "attrs_user"); exists { + count, countSheetsNum, countFailed, err0 := attrsUserMigrate(tx) + log.Infof("数据卡转换 - 角色卡,成功人数%d 失败人数 %d 卡数 %d\n", count, countFailed, countSheetsNum) + if err0 != nil { + return fmt.Errorf("角色卡转换出错: %w", err0) } } - if exists, _ := checkTableExists(db, "attrs_group_user"); exists { - count, countFailed, err2 := attrsGroupUserMigrate(tx) - fmt.Printf("数据卡转换 - 群组个人数据,成功%d 失败 %d\n", count, countFailed) - if err2 != nil { - fmt.Println("异常", err2.Error()) - return false + if exists, _ = checkTableExists(db, "attrs_group_user"); exists { + count, countFailed, err1 := attrsGroupUserMigrate(tx) + log.Infof("数据卡转换 - 群组个人数据,成功%d 失败 %d\n", count, countFailed) + if err1 != nil { + return fmt.Errorf("群组个人数据转换出错: %w", err1) } } - if exists, _ := checkTableExists(db, "attrs_group"); exists { + if exists, _ = checkTableExists(db, "attrs_group"); exists { count, countFailed, err2 := attrsGroupMigrate(tx) - fmt.Printf("数据卡转换 - 群数据,成功%d 失败 %d\n", count, countFailed) + log.Infof("数据卡转换 - 群数据,成功%d 失败 %d\n", count, countFailed) if err2 != nil { - fmt.Println("异常", err2.Error()) - return false + return fmt.Errorf("群数据转换出错: %w", err2) } } - // 删档 - fmt.Println("删除旧版本数据") - _, _ = tx.Exec("drop table attrs_group") - _, _ = tx.Exec("drop table attrs_group_user") - _, _ = tx.Exec("drop table attrs_user") + // 删除旧版本数据 + log.Info("删除旧版本数据") + deleteSQLs := []string{ + "drop table attrs_group", + "drop table attrs_group_user", + "drop table attrs_user", + } + for _, deleteSQL := range deleteSQLs { + if _, err = tx.Exec(deleteSQL); err != nil { + return fmt.Errorf("删除旧数据时出错: %w", err) + } + } + // 放在这里保证能执行 _, _ = db.Exec("PRAGMA wal_checkpoint(TRUNCATE);") - _, _ = tx.Exec("VACUUM;") // 收尾 - + _, _ = db.Exec("VACUUM;") sheetIdBindByGroupUserId = nil - - err = tx.Commit() - if err != nil { - fmt.Println("V150 数据转换失败:", err.Error()) - return false - } - - fmt.Println("V150 数据转换完成") - return true + log.Info("V150 数据转换完成") + return nil } diff --git a/readme.md b/readme.md index bc67977d..26414392 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ ### golang 开发环境 -编译的 golang 版本为 1.20。使用更新版本时需注意不要使用新版本引入的新函数。 +编译的 golang 版本为 1.22。在 [构建](https://github.com/sealdice/sealdice-build) 仓库中采用对 go 1.22 进行修补的方式以支持 Windows 7 等低版本系统。 因部分依赖库的需求,可能需要配置国内镜像,个人使用 镜像。 @@ -45,7 +45,7 @@ 此工具对于代码开发**不是**必要的。但是,本项目的 CI 流程中配置了 linter 检查,不符合规范的代码不能被合入。 -因此,强烈推荐开发者在本地安装此工具,请参考[这份文档](https://golangci-lint.run/usage/install/#local-installation)。分析器的相关配置位于 `.golangci.yml` 文件中。 +因此,强烈推荐开发者在本地安装此工具,请参考[这份文档](https://golangci-lint.run/welcome/install/#local-installation)。分析器的相关配置位于 `.golangci.yml` 文件中。 你可能需要调整编辑器的相关配置,使用 golangci-lint 为默认的分析工具,并开启自动检查。 @@ -57,11 +57,29 @@ > > 以上配置没有写入项目的统一设置,以允许开发者不本地使用 golangci-lint -### 拉取代码并配置数据文件 +### 编译运行 + +#### 使用 `go-task` + +你可以安装 [go-task](https://taskfile.dev/installation) 以执行预置好的任务。安装后可执行: -使用git拉取项目代码 +```bash +# 初次编译运行(包括安装依赖和相关工具) +task install run + +# 后续编译运行 +task run +``` -从已发布的海豹二进制包中,解压 `data`、`gocqhttp` 两个目录到代码目录下。 +#### 手动执行 + +你也可以按照以下步骤手动进行编译运行: + +##### 拉取代码并配置数据文件 + +使用 git 拉取项目代码 + +从已发布的海豹二进制包中,解压 `data`、`lagrange` 两个目录到代码目录下。 同时需要在项目的 `static/frontend` 下放置用于打包进 core 的 ui 静态资源文件,可手动提供,也可通过命令自动从 github 拉取: @@ -82,7 +100,7 @@ static └─assets ``` -### 编译运行 +##### 运行编译命令 打开项目,或使用终端访问项目目录,运行: @@ -104,50 +122,50 @@ go run . ### 从哪开始看 -从 main.go 开始,这里海豹分出了几个线程,一个启动核心并提供服务,另一个提供ui的http服务。 +从 `main.go` 开始,这里海豹分出了几个线程,一个启动核心并提供服务,另一个提供 ui 的 http 服务。 -可以顺藤摸瓜了解海豹如何启动,如何提供服务,如何响应指令。指令响应的部分写在im_session.go中 +可以顺藤摸瓜了解海豹如何启动,如何提供服务,如何响应指令。指令响应的部分写在 `im_session.go` 中 -注意有部分代码还在构思中,实际并未使用,例如 CharacterTemplate,请阅读时先Find Usage加以区分 +注意有部分代码还在构思中,实际并未使用,例如 `CharacterTemplate`,请阅读时先 Find Usage 加以区分 ### 重要数据结构 -dice.go 中的 Dice 结构体存放着各种核心配置,每个Dice实例是一个骰子,而每个骰子下面可以挂靠多个端点(EndPoint)。端点即交互渠道,例如一个QQ账号是一个端点。 +`dice.go` 中的 `Dice` 结构体存放着各种核心配置,每个 `Dice` 实例是一个骰子,而每个骰子下面可以挂靠多个端点 (EndPoint)。端点即交互渠道,例如一个 QQ 账号是一个端点。 -所有的端点由 IMSession 来统一管理,同样的,这个类也负责接收和分发指令。 +所有的端点由 `IMSession` 来统一管理,同样的,这个类也负责接收和分发指令。 -可能你会注意到有 IMSession 和 IMSessionLegacy,只看前一个就行,IMSessionLegacy对应的是0.99.13的上古版本之前的数据结构,仅用于升级配置文件。 +可能你会注意到有 `IMSession` 和 `IMSessionLegacy`,只看前一个就行,`IMSessionLegacy` 对应的是 0.99.13 的上古版本之前的数据结构,仅用于升级配置文件。 -GroupInfo 是群组信息 +`GroupInfo` 是群组信息 -GroupPlayerInfo 是玩家信息 +`GroupPlayerInfo` 是玩家信息 ### 为海豹添加更多平台支持 -海豹使用叫做 PlatformAdapter 的接口来接入平台,只需将接口全部实现,再创建一个 EndPointInfo 塞入当前用户的 IMSession 对象之中即可。 +海豹使用叫做 `PlatformAdapter` 的接口来接入平台,只需将接口全部实现,再创建一个 `EndPointInfo` 塞入当前用户的 `IMSession` 对象之中即可。 -注: 每次在UI上添加QQ账号,其实就是创建了一个EndPointInfo对象,并制定Adapter为PlatformAdapterQQOnebot +注:每次在 UI 上添加 QQ 账号,其实就是创建了一个 `EndPointInfo` 对象,并制定 Adapter 为 `PlatformAdapterQQOnebot` -目前实现的两个adapter,一个对应onebot协议,主要用于QQ,另一个对应http,用于UI后台的测试窗口。 +目前实现的两个 adapter,一个对应 onebot 协议,主要用于 QQ,另一个对应 http,用于 UI 后台的测试窗口。 -观察 PlatformAdapterHttp 如何运作起来是一个很好的切入点,因为他非常简单。 +观察 `PlatformAdapterHttp` 如何运作起来是一个很好的切入点,因为他非常简单。 -### 改动扩展模块,如dnd5e,coc7等 +### 改动扩展模块,如 dnd5e,coc7 等 -对应 dice/ext_xxx.go 系列文件 +对应 `dice/ext_xxx.go` 系列文件 -推荐从 ext_template.go 入手,以 ext_dnd5e.go 为参考,因为这个模块书写时间较晚,相对较为完善。 +推荐从 `ext_template.go` 入手,以 `ext_dnd5e.go` 为参考,因为这个模块书写时间较晚,相对较为完善。 ### 暂不建议修改的地方 #### 表达式解析器 -dice/roll.peg 是海豹的骰点指令文法文件 +`dice/roll.peg` 是海豹的骰点指令文法文件 -dice/rollvm.go 是骰点指令虚拟机 +`dice/rollvm.go` 是骰点指令虚拟机 -1.5后,已经替换使用 dicescript (RollVM V2) 作为表达式解释器,现有版本不宜轻动。 +1.5 后,已经替换使用 dicescript (RollVM V2) 作为表达式解释器,现有版本不宜轻动。 关于 dicescript 的信息,请移步 -而出于兼容性的考虑,V1版本的解释器将继续保留,直到2.0版本。 +而出于兼容性的考虑,V1 版本的解释器将继续保留,直到 2.0 版本。 diff --git a/sealdice-builtins b/sealdice-builtins index cbfc3d36..ae995845 160000 --- a/sealdice-builtins +++ b/sealdice-builtins @@ -1 +1 @@ -Subproject commit cbfc3d368225ded84cd39ffe1ec54563b5c785ba +Subproject commit ae99584518d3de211cee671fb84b175e558cda0b diff --git a/sealdice-ui b/sealdice-ui index e3a53af5..5a6b3b8b 160000 --- a/sealdice-ui +++ b/sealdice-ui @@ -1 +1 @@ -Subproject commit e3a53af5ba8b9702e705040db5a3abc387f31888 +Subproject commit 5a6b3b8bf9798ce09fa00a9765c28a1766c41fe6 diff --git a/static/gen/download-fe.go b/static/gen/download-fe.go index 24e8b771..9984626c 100644 --- a/static/gen/download-fe.go +++ b/static/gen/download-fe.go @@ -2,6 +2,7 @@ package main import ( "archive/zip" + "errors" "fmt" "io" "net/http" @@ -42,7 +43,7 @@ func downloadFrontendZip() error { return err } if resp.StatusCode != http.StatusOK { - return fmt.Errorf(resp.Status) + return errors.New(resp.Status) } defer func() { _ = resp.Body.Close() }() diff --git a/tray_darwin.go b/tray_darwin.go index e6901875..7e06ee73 100644 --- a/tray_darwin.go +++ b/tray_darwin.go @@ -4,7 +4,6 @@ package main import ( - "fmt" "net" "os" "os/exec" @@ -20,6 +19,7 @@ import ( "sealdice-core/dice" "sealdice-core/icon" + log "sealdice-core/utils/kratos" ) var theDm *dice.DiceManager @@ -41,11 +41,11 @@ func TestRunning() bool { } func tempDirWarn() { - fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。") + log.Info("当前工作路径为临时目录,因此拒绝继续执行。") } func showMsgBox(title string, message string) { - fmt.Println(title, message) + log.Info(title, message) } func executeWin(name string, arg ...string) *exec.Cmd { @@ -121,17 +121,16 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) { ln, err := net.Listen("tcp", ":"+portStr) if err != nil { - logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) + log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) runtime.Goexit() } _ = ln.Close() // exec.Command(`cmd`, `/c`, `start`, fmt.Sprintf(`http://localhost:%s`, portStr)).Start() - fmt.Println("如果浏览器没有自动打开,请手动访问:") - fmt.Printf(`http://localhost:%s`, portStr) // 默认:3211 + log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s", portStr) err = e.Start(dm.ServeAddress) if err != nil { - logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) + log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) return } } diff --git a/tray_others.go b/tray_others.go index 75bb354a..e535b080 100644 --- a/tray_others.go +++ b/tray_others.go @@ -4,7 +4,6 @@ package main import ( - "fmt" "net" "os" "os/exec" @@ -15,6 +14,7 @@ import ( "github.com/labstack/echo/v4" "sealdice-core/dice" + log "sealdice-core/utils/kratos" ) func trayInit(dm *dice.DiceManager) { @@ -32,11 +32,11 @@ func TestRunning() bool { } func tempDirWarn() { - fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。") + log.Warn("当前工作路径为临时目录,因此拒绝继续执行。") } func showMsgBox(title string, message string) { - fmt.Println(title, message) + log.Info(title, message) } func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) { @@ -49,16 +49,15 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) { ln, err := net.Listen("tcp", ":"+portStr) if err != nil { - logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) + log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) runtime.Goexit() } _ = ln.Close() - fmt.Println("如果浏览器没有自动打开,请手动访问:") - fmt.Printf(`http://localhost:%s`, portStr) // 默认:3211 + log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s", portStr) err = e.Start(dm.ServeAddress) if err != nil { - logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) + log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) return } } diff --git a/tray_windows.go b/tray_windows.go index 11c482f1..df40c2c6 100644 --- a/tray_windows.go +++ b/tray_windows.go @@ -25,6 +25,7 @@ import ( "sealdice-core/dice" "sealdice-core/icon" + log "sealdice-core/utils/kratos" ) func hideWindow() { @@ -154,7 +155,7 @@ func onReady() { s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误") s2, _ := syscall.UTF16PtrFromString("自启动失败设置失败,原因: " + err.Error()) win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR) - fmt.Println("自启动设置失败: ", err.Error()) + log.Errorf("自启动设置失败: %v", err.Error()) } mAutoBoot.Uncheck() } else { @@ -163,7 +164,7 @@ func onReady() { s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误") s2, _ := syscall.UTF16PtrFromString("自启动失败设置失败,原因: " + err.Error()) win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR) - fmt.Println("自启动设置失败: ", err.Error()) + log.Errorf("自启动设置失败: %v", err.Error()) } mAutoBoot.Check() } @@ -239,12 +240,11 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) { dm.ServeAddress = fmt.Sprintf("0.0.0.0:%d", newPort) continue } else { - logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) + log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress) os.Exit(0) } } else { - fmt.Println("如果浏览器没有自动打开,请手动访问:") - fmt.Printf("http://localhost:%s\n", portStr) // 默认:3211 + log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s\n", portStr) go showUI() break } @@ -255,7 +255,7 @@ func tempDirWarn() { s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误") s2, _ := syscall.UTF16PtrFromString("你正在临时文件目录运行海豹,最可能的情况是没有解压而是直接双击运行!\n请先完整解压后再进行运行操作!\n按确定后将自动退出") win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR) - fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。") + log.Error("当前工作路径为临时目录,因此拒绝继续执行。") } func showMsgBox(title string, message string) { diff --git a/update.go b/update.go index 56eea8c6..83ae2379 100644 --- a/update.go +++ b/update.go @@ -14,15 +14,14 @@ import ( "syscall" "time" - "go.uber.org/zap" - "sealdice-core/dice" "sealdice-core/utils" + log "sealdice-core/utils/kratos" ) var binPrefix = "https://sealdice.coding.net/p/sealdice/d/sealdice-binaries/git/raw/master" -func downloadUpdate(dm *dice.DiceManager, log *zap.SugaredLogger) (string, error) { +func downloadUpdate(dm *dice.DiceManager, log *log.Helper) (string, error) { var packFn string if dm.AppVersionOnline != nil { ver := dm.AppVersionOnline @@ -112,7 +111,7 @@ func doReboot(dm *dice.DiceManager) { binary, err := exec.LookPath(executablePath) if err != nil { - logger.Errorf("Restart Error: %s", err) + log.Errorf("Restart Error: %s", err) return } platform := runtime.GOOS @@ -124,7 +123,7 @@ func doReboot(dm *dice.DiceManager) { cmd := executeWin(binary, "--delay=15") err := cmd.Start() if err != nil { - logger.Errorf("Restart error: %s %v", binary, err) + log.Errorf("Restart error: %s %v", binary, err) } } else { // 手动cleanup @@ -132,7 +131,7 @@ func doReboot(dm *dice.DiceManager) { // os.Args[1:]... execErr := syscall.Exec(binary, []string{os.Args[0], "--delay=25"}, os.Environ()) if execErr != nil { - logger.Errorf("Restart error: %s %v", binary, execErr) + log.Errorf("Restart error: %s %v", binary, execErr) } } os.Exit(0) diff --git a/update_updater.go b/update_updater.go index fcaa5148..46096f35 100644 --- a/update_updater.go +++ b/update_updater.go @@ -13,10 +13,9 @@ import ( "syscall" "time" - "go.uber.org/zap" - "sealdice-core/dice" "sealdice-core/utils" + log "sealdice-core/utils/kratos" ) const updaterVersion = "0.1.1" @@ -73,7 +72,7 @@ func CheckUpdater(dm *dice.DiceManager) error { exists := false fn := getUpdaterFn() if _, err := os.Stat(fn); err == nil { - logger.Info("检测到海豹更新程序") + log.Info("检测到海豹更新程序") exists = true } @@ -82,15 +81,15 @@ func CheckUpdater(dm *dice.DiceManager) error { if exists { err := os.Chmod(fn, 0o755) if err != nil { - logger.Error("设置升级程序执行权限失败", err.Error()) + log.Error("设置升级程序执行权限失败", err.Error()) } cmd := exec.Command(fn, "--version") out, err := cmd.Output() if err != nil { - logger.Error("获取升级程序版本失败") + log.Error("获取升级程序版本失败") } else { ver := strings.TrimSpace(string(out)) - logger.Info("升级程序版本:", ver) + log.Info("升级程序版本:", ver) if ver == "seal-updater "+updaterVersion { isUpdaterOk = true } @@ -99,16 +98,16 @@ func CheckUpdater(dm *dice.DiceManager) error { // 如果升级程序不可用,那么下载一个 if !isUpdaterOk { - logger.Info("未检测到可用更新程序,开始下载") + log.Info("未检测到可用更新程序,开始下载") err := downloadUpdater(dm) if err != nil { - logger.Error("下载更新程序失败") + log.Error("下载更新程序失败") return errors.New("下载更新程序失败,无可用更新程序") } else { - logger.Info("下载更新程序成功") + log.Info("下载更新程序成功") err := os.Chmod(fn, 0o755) if err != nil { - logger.Error("设置升级程序执行权限失败", err.Error()) + log.Error("设置升级程序执行权限失败", err.Error()) } } } @@ -145,11 +144,8 @@ func downloadUpdater(dm *dice.DiceManager) error { return nil } -func UpdateByFile(dm *dice.DiceManager, log *zap.SugaredLogger, packName string, syncMode bool) bool { +func UpdateByFile(dm *dice.DiceManager, packName string, syncMode bool) bool { // 注意: 当执行完就立即退进程的情况下,需要使用 syncMode 为true - if log == nil { - log = logger - } fn := getUpdaterFn() err := os.Chmod(fn, 0o755) if err != nil { diff --git a/utils/convertdb.go b/utils/convertdb.go new file mode 100644 index 00000000..97e5c773 --- /dev/null +++ b/utils/convertdb.go @@ -0,0 +1,23 @@ +package utils + +import ( + "github.com/jmoiron/sqlx" + "gorm.io/gorm" +) + +// GetSQLXDB 将 GORM 的 *gorm.DB 转换为 *sqlx.DB,并自动获取驱动名称,用于需要sqlx的场景 +func GetSQLXDB(db *gorm.DB) (*sqlx.DB, error) { + // 获取底层的 *sql.DB + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // 获取 GORM 使用的驱动名称 + driverName := db.Dialector.Name() + + // 使用 sqlx.NewDb 传递现有的 *sql.DB 和驱动名称 + sqlxDB := sqlx.NewDb(sqlDB, driverName) + + return sqlxDB, nil +} diff --git a/utils/crypto/ecdsa.go b/utils/crypto/ecdsa.go index 551929b9..0a9c234e 100644 --- a/utils/crypto/ecdsa.go +++ b/utils/crypto/ecdsa.go @@ -4,7 +4,7 @@ import ( "crypto/ecdsa" "crypto/rand" "encoding/base64" - "fmt" + "errors" ) // EcdsaSign Ecdsa 签名 @@ -39,7 +39,7 @@ func EcdsaVerify(data []byte, base64Sig, publicKey string) error { if ok := ecdsa.VerifyASN1(key, hashed, sign); ok { return nil } - return fmt.Errorf("verify failed") + return errors.New("verify failed") } func EcdsaVerifyRow(data []byte, sign []byte, publicKey string) error { @@ -48,5 +48,5 @@ func EcdsaVerifyRow(data []byte, sign []byte, publicKey string) error { if ok := ecdsa.VerifyASN1(key, hashed, sign); ok { return nil } - return fmt.Errorf("verify failed") + return errors.New("verify failed") } diff --git a/utils/download.go b/utils/download.go index 9f09c976..aeab7ba6 100644 --- a/utils/download.go +++ b/utils/download.go @@ -3,10 +3,11 @@ package utils import ( "compress/gzip" "errors" - "fmt" "io" "net/http" "os" + + log "sealdice-core/utils/kratos" ) func DownloadFile(filepath string, url string) error { @@ -41,7 +42,7 @@ func DownloadFile(filepath string, url string) error { var reader io.ReadCloser reader, err = gzip.NewReader(resp.Body) if err != nil { - fmt.Println("GZIP解压出错:", err) + log.Errorf("GZIP解压出错: %v", err) return err } defer reader.Close() diff --git a/utils/kratos/const.go b/utils/kratos/const.go new file mode 100644 index 00000000..245435fc --- /dev/null +++ b/utils/kratos/const.go @@ -0,0 +1,9 @@ +package log + +const ( + LOG_SEAL = "SEAL" + LOG_DICE = "DICE" + LOG_WEB = "WEB" + LOG_LAGR = "LAGR" + LOG_HIDE = "HIDE" +) diff --git a/utils/kratos/echologger.go b/utils/kratos/echologger.go new file mode 100644 index 00000000..aa8a95a3 --- /dev/null +++ b/utils/kratos/echologger.go @@ -0,0 +1,52 @@ +// Package middleware provides echo request and response output log +package log + +import ( + "strconv" + "time" + + "github.com/labstack/echo/v4" +) + +// Zlogger returns a middleware that logs HTTP requests. +func EchoMiddleLogger(elogger *Helper) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + req := c.Request() + res := c.Response() + start := time.Now() + + var err error + if err = next(c); err != nil { + c.Error(err) + } + stop := time.Now() + + id := req.Header.Get(echo.HeaderXRequestID) + if id == "" { + id = res.Header().Get(echo.HeaderXRequestID) + } + reqSize := req.Header.Get(echo.HeaderContentLength) + if reqSize == "" { + reqSize = "0" + } + + // 特殊情况下,需要输出DEBUG日志 + elogger.Debugf("%s %s [%v] %s %-7s %s %3d %s %s %13v %s %s", + id, + c.RealIP(), + stop.Format(time.RFC3339), + req.Host, + req.Method, + req.RequestURI, + res.Status, + reqSize, + strconv.FormatInt(res.Size, 10), + stop.Sub(start).String(), + req.Referer(), + req.UserAgent(), + ) + return err + } + } +} diff --git a/utils/kratos/filter.go b/utils/kratos/filter.go new file mode 100644 index 00000000..7380d3dd --- /dev/null +++ b/utils/kratos/filter.go @@ -0,0 +1,95 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +// FilterOption is filter option. +type FilterOption func(*Filter) + +const fuzzyStr = "***" + +// FilterLevel with filter level. +func FilterLevel(level Level) FilterOption { + return func(opts *Filter) { + opts.level = level + } +} + +// FilterKey with filter key. +func FilterKey(key ...string) FilterOption { + return func(o *Filter) { + for _, v := range key { + o.key[v] = struct{}{} + } + } +} + +// FilterValue with filter value. +func FilterValue(value ...string) FilterOption { + return func(o *Filter) { + for _, v := range value { + o.value[v] = struct{}{} + } + } +} + +// FilterFunc with filter func. +func FilterFunc(f func(level Level, keyvals ...interface{}) bool) FilterOption { + return func(o *Filter) { + o.filter = f + } +} + +// Filter is a logger filter. +type Filter struct { + logger Logger + level Level + key map[interface{}]struct{} + value map[interface{}]struct{} + filter func(level Level, keyvals ...interface{}) bool +} + +// NewFilter new a logger filter. +func NewFilter(logger Logger, opts ...FilterOption) *Filter { + options := Filter{ + logger: logger, + key: make(map[interface{}]struct{}), + value: make(map[interface{}]struct{}), + } + for _, o := range opts { + o(&options) + } + return &options +} + +// Log Print log by level and keyvals. +func (f *Filter) Log(level Level, keyvals ...interface{}) error { + if level < f.level { + return nil + } + // prefixkv contains the slice of arguments defined as prefixes during the log initialization + var prefixkv []interface{} + l, ok := f.logger.(*logger) + if ok && len(l.prefix) > 0 { + prefixkv = make([]interface{}, 0, len(l.prefix)) + prefixkv = append(prefixkv, l.prefix...) + } + + if f.filter != nil && (f.filter(level, prefixkv...) || f.filter(level, keyvals...)) { + return nil + } + + if len(f.key) > 0 || len(f.value) > 0 { + for i := 0; i < len(keyvals); i += 2 { + v := i + 1 + if v >= len(keyvals) { + continue + } + if _, ok := f.key[keyvals[i]]; ok { + keyvals[v] = fuzzyStr + } + if _, ok := f.value[keyvals[v]]; ok { + keyvals[v] = fuzzyStr + } + } + } + return f.logger.Log(level, keyvals...) +} diff --git a/utils/kratos/global.go b/utils/kratos/global.go new file mode 100644 index 00000000..cb9446cb --- /dev/null +++ b/utils/kratos/global.go @@ -0,0 +1,134 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +import ( + "context" + "fmt" + "os" + "sync" +) + +// globalLogger is designed as a global logger in current process. +var global = &loggerAppliance{} + +// loggerAppliance is the proxy of `Zlogger` to +// make logger change will affect all sub-logger. +type loggerAppliance struct { + lock sync.Mutex + wx *WriterX + logger Logger +} + +// 似乎不建议使用init(),我自己手动管理吧 +// func init() { +// global.SetLogger(DefaultLogger) +// } + +func (a *loggerAppliance) SetLogger(in Logger) { + a.lock.Lock() + defer a.lock.Unlock() + a.logger = in +} + +// SetLogger should be called before any other log call. +// And it is NOT THREAD SAFE. +func SetLogger(logger Logger) { + global.SetLogger(logger) +} + +// GetLogger returns global logger appliance as logger in current process. +func GetLogger() Logger { + return global.logger +} + +// Log Print log by level and keyvals. +func Log(level Level, keyvals ...interface{}) { + _ = global.logger.Log(level, keyvals...) +} + +// Context with context logger. +func Context(ctx context.Context) *Helper { + return NewHelper(WithContext(ctx, global.logger)) +} + +// Debug logs a message at debug level. +func Debug(a ...interface{}) { + _ = global.logger.Log(LevelDebug, DefaultMessageKey, fmt.Sprint(a...)) +} + +// Debugf logs a message at debug level. +func Debugf(format string, a ...interface{}) { + _ = global.logger.Log(LevelDebug, DefaultMessageKey, fmt.Sprintf(format, a...)) +} + +// Debugw logs a message at debug level. +func Debugw(keyvals ...interface{}) { + _ = global.logger.Log(LevelDebug, keyvals...) +} + +// Info logs a message at info level. +func Info(a ...interface{}) { + _ = global.logger.Log(LevelInfo, DefaultMessageKey, fmt.Sprint(a...)) +} + +// Infof logs a message at info level. +func Infof(format string, a ...interface{}) { + _ = global.logger.Log(LevelInfo, DefaultMessageKey, fmt.Sprintf(format, a...)) +} + +// Infow logs a message at info level. +func Infow(keyvals ...interface{}) { + _ = global.logger.Log(LevelInfo, keyvals...) +} + +// Warn logs a message at warn level. +func Warn(a ...interface{}) { + _ = global.logger.Log(LevelWarn, DefaultMessageKey, fmt.Sprint(a...)) +} + +// Warnf logs a message at warnf level. +func Warnf(format string, a ...interface{}) { + _ = global.logger.Log(LevelWarn, DefaultMessageKey, fmt.Sprintf(format, a...)) +} + +// Warnw logs a message at warnf level. +func Warnw(keyvals ...interface{}) { + _ = global.logger.Log(LevelWarn, keyvals...) +} + +// Error logs a message at error level. +func Error(a ...interface{}) { + _ = global.logger.Log(LevelError, DefaultMessageKey, fmt.Sprint(a...)) +} + +// Errorf logs a message at error level. +func Errorf(format string, a ...interface{}) { + _ = global.logger.Log(LevelError, DefaultMessageKey, fmt.Sprintf(format, a...)) +} + +// Errorw logs a message at error level. +func Errorw(keyvals ...interface{}) { + _ = global.logger.Log(LevelError, keyvals...) +} + +// Fatal logs a message at fatal level. +func Fatal(a ...interface{}) { + _ = global.logger.Log(LevelFatal, DefaultMessageKey, fmt.Sprint(a...)) + os.Exit(1) +} + +// Fatalf logs a message at fatal level. +func Fatalf(format string, a ...interface{}) { + _ = global.logger.Log(LevelFatal, DefaultMessageKey, fmt.Sprintf(format, a...)) + os.Exit(1) +} + +// Fatalw logs a message at fatal level. +func Fatalw(keyvals ...interface{}) { + _ = global.logger.Log(LevelFatal, keyvals...) + os.Exit(1) +} + +func GetWriterX() *WriterX { + return global.wx +} diff --git a/utils/kratos/gormlogger.go b/utils/kratos/gormlogger.go new file mode 100644 index 00000000..ab541dee --- /dev/null +++ b/utils/kratos/gormlogger.go @@ -0,0 +1,113 @@ +package log + +import ( + "context" + "errors" + "fmt" + "time" + + "go.uber.org/zap/zapcore" + gormlogger "gorm.io/gorm/logger" +) + +// gorm的格式化字符串抄过来 +var ( + infoStr = "[info] " + warnStr = "[warn] " + errStr = "[error] " + traceStr = "[%.3fms] [rows:%v] %s" + traceWarnStr = "%s\n[%.3fms] [rows:%v] %s" + traceErrStr = "%s\n[%.3fms] [rows:%v] %s" +) + +type ContextFn func(ctx context.Context) []zapcore.Field + +type GORMLogger struct { + // 要被传入的KartosLogger + ZapLogger *Helper + // 原本的Logger有 + LogLevel gormlogger.LogLevel + // 原本的logger有 + SlowThreshold time.Duration + // 原本的logger有 + IgnoreRecordNotFoundError bool + // logger缺少的 + ParameterizedQueries bool + SkipCallerLookup bool + Context ContextFn +} + +func NewGormLogger(zapLogger *Helper) GORMLogger { + return GORMLogger{ + ZapLogger: zapLogger, + LogLevel: gormlogger.Warn, + SlowThreshold: 100 * time.Millisecond, + IgnoreRecordNotFoundError: false, + Context: nil, + } +} + +func (l GORMLogger) SetAsDefault() { + gormlogger.Default = l +} + +func (l GORMLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { + return GORMLogger{ + ZapLogger: l.ZapLogger, + SlowThreshold: l.SlowThreshold, + LogLevel: level, + SkipCallerLookup: l.SkipCallerLookup, + IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError, + Context: l.Context, + } +} + +func (l GORMLogger) Info(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Info { + l.ZapLogger.Infof(infoStr+msg, args...) + } +} + +func (l GORMLogger) Warn(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Warn { + l.ZapLogger.Warnf(warnStr+msg, args...) + } +} + +func (l GORMLogger) Error(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Error { + l.ZapLogger.Errorf(errStr+msg, args...) + } +} + +func (l GORMLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) { + if l.LogLevel <= gormlogger.Silent { + return + } + + elapsed := time.Since(begin) + switch { + case err != nil && l.LogLevel >= gormlogger.Error && (!errors.Is(err, gormlogger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError): + sql, rows := fc() + if rows == -1 { + l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormlogger.Warn: + sql, rows := fc() + slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold) + if rows == -1 { + l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case l.LogLevel == gormlogger.Info: + sql, rows := fc() + if rows == -1 { + l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + } +} diff --git a/utils/kratos/helper.go b/utils/kratos/helper.go new file mode 100644 index 00000000..e6d6b780 --- /dev/null +++ b/utils/kratos/helper.go @@ -0,0 +1,179 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +import ( + "context" + "fmt" + "os" + + "go.uber.org/zap" +) + +// DefaultMessageKey default message key. +var DefaultMessageKey = "msg" + +// Option is Helper option. +type Option func(*Helper) + +// Helper is a logger helper. +type Helper struct { + logger Logger + msgKey string + sprint func(...interface{}) string + sprintf func(format string, a ...interface{}) string +} + +// WithMessageKey with message key. +func WithMessageKey(k string) Option { + return func(opts *Helper) { + opts.msgKey = k + } +} + +// WithSprint with sprint +func WithSprint(sprint func(...interface{}) string) Option { + return func(opts *Helper) { + opts.sprint = sprint + } +} + +// WithSprintf with sprintf +func WithSprintf(sprintf func(format string, a ...interface{}) string) Option { + return func(opts *Helper) { + opts.sprintf = sprintf + } +} + +// NewHelper new a logger helper. +func NewHelper(logger Logger, opts ...Option) *Helper { + options := &Helper{ + msgKey: DefaultMessageKey, // default message key + logger: logger, + sprint: fmt.Sprint, + sprintf: fmt.Sprintf, + } + for _, o := range opts { + o(options) + } + return options +} + +// ADD 获取自定义Helper +func NewCustomHelper(loggerName string, hideConsole bool, sprintfFunc func(format string, a ...interface{}) string, withOptions ...zap.Option) *Helper { + var zapLogger Logger + var tempZapLogger *zap.Logger + // 添加HIDE的不会被输出到控制台,但会被输出到文件,或者其他位置。 + // Named会保证克隆一个 + if hideConsole { + tempZapLogger = GetLoggerRaw().Named("HIDE").Named(loggerName) + } else { + tempZapLogger = GetLoggerRaw().Named(loggerName) + } + // 添加存在的 + if len(withOptions) > 0 { + tempZapLogger = tempZapLogger.WithOptions(withOptions...) + } + zapLogger = NewZapLogger(tempZapLogger) + // 如果 sprintfFunc 为 nil,则不使用 WithSprintf,直接使用 NewHelper + var helper *Helper + if sprintfFunc != nil { + helper = NewHelper(zapLogger, WithSprintf(sprintfFunc)) + } else { + helper = NewHelper(zapLogger) // 使用默认的 Helper + } + return helper +} + +// WithContext returns a shallow copy of h with its context changed +// to ctx. The provided ctx must be non-nil. +func (h *Helper) WithContext(ctx context.Context) *Helper { + return &Helper{ + msgKey: h.msgKey, + logger: WithContext(ctx, h.logger), + sprint: h.sprint, + sprintf: h.sprintf, + } +} + +// Log Print log by level and keyvals. +func (h *Helper) Log(level Level, keyvals ...interface{}) { + _ = h.logger.Log(level, keyvals...) +} + +// Debug logs a message at debug level. +func (h *Helper) Debug(a ...interface{}) { + _ = h.logger.Log(LevelDebug, h.msgKey, h.sprint(a...)) +} + +// Debugf logs a message at debug level. +func (h *Helper) Debugf(format string, a ...interface{}) { + _ = h.logger.Log(LevelDebug, h.msgKey, h.sprintf(format, a...)) +} + +// Debugw logs a message at debug level. +func (h *Helper) Debugw(keyvals ...interface{}) { + _ = h.logger.Log(LevelDebug, keyvals...) +} + +// Info logs a message at info level. +func (h *Helper) Info(a ...interface{}) { + _ = h.logger.Log(LevelInfo, h.msgKey, h.sprint(a...)) +} + +// Infof logs a message at info level. +func (h *Helper) Infof(format string, a ...interface{}) { + _ = h.logger.Log(LevelInfo, h.msgKey, h.sprintf(format, a...)) +} + +// Infow logs a message at info level. +func (h *Helper) Infow(keyvals ...interface{}) { + _ = h.logger.Log(LevelInfo, keyvals...) +} + +// Warn logs a message at warn level. +func (h *Helper) Warn(a ...interface{}) { + _ = h.logger.Log(LevelWarn, h.msgKey, h.sprint(a...)) +} + +// Warnf logs a message at warnf level. +func (h *Helper) Warnf(format string, a ...interface{}) { + _ = h.logger.Log(LevelWarn, h.msgKey, h.sprintf(format, a...)) +} + +// Warnw logs a message at warnf level. +func (h *Helper) Warnw(keyvals ...interface{}) { + _ = h.logger.Log(LevelWarn, keyvals...) +} + +// Error logs a message at error level. +func (h *Helper) Error(a ...interface{}) { + _ = h.logger.Log(LevelError, h.msgKey, h.sprint(a...)) +} + +// Errorf logs a message at error level. +func (h *Helper) Errorf(format string, a ...interface{}) { + _ = h.logger.Log(LevelError, h.msgKey, h.sprintf(format, a...)) +} + +// Errorw logs a message at error level. +func (h *Helper) Errorw(keyvals ...interface{}) { + _ = h.logger.Log(LevelError, keyvals...) +} + +// Fatal logs a message at fatal level. +func (h *Helper) Fatal(a ...interface{}) { + _ = h.logger.Log(LevelFatal, h.msgKey, h.sprint(a...)) + os.Exit(1) +} + +// Fatalf logs a message at fatal level. +func (h *Helper) Fatalf(format string, a ...interface{}) { + _ = h.logger.Log(LevelFatal, h.msgKey, h.sprintf(format, a...)) + os.Exit(1) +} + +// Fatalw logs a message at fatal level. +func (h *Helper) Fatalw(keyvals ...interface{}) { + _ = h.logger.Log(LevelFatal, keyvals...) + os.Exit(1) +} diff --git a/utils/kratos/level.go b/utils/kratos/level.go new file mode 100644 index 00000000..d4279247 --- /dev/null +++ b/utils/kratos/level.go @@ -0,0 +1,61 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +import "strings" + +// Level is a logger level. +type Level int8 + +// LevelKey is logger level key. +const LevelKey = "level" + +const ( + // LevelDebug is logger debug level. + LevelDebug Level = iota - 1 + // LevelInfo is logger info level. + LevelInfo + // LevelWarn is logger warn level. + LevelWarn + // LevelError is logger error level. + LevelError + // LevelFatal is logger fatal level + LevelFatal +) + +func (l Level) Key() string { + return LevelKey +} + +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + case LevelFatal: + return "FATAL" + default: + return "" + } +} + +// ParseLevel parses a level string into a logger Level value. +func ParseLevel(s string) Level { + switch strings.ToUpper(s) { + case "DEBUG": + return LevelDebug + case "INFO": + return LevelInfo + case "WARN": + return LevelWarn + case "ERROR": + return LevelError + case "FATAL": + return LevelFatal + } + return LevelInfo +} diff --git a/utils/kratos/log.go b/utils/kratos/log.go new file mode 100644 index 00000000..2114ee7f --- /dev/null +++ b/utils/kratos/log.go @@ -0,0 +1,62 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +import ( + "context" +) + +// Zlogger is a logger interface. +type Logger interface { + Log(level Level, keyvals ...interface{}) error +} + +type logger struct { + logger Logger + prefix []interface{} + hasValuer bool + ctx context.Context +} + +func (c *logger) Log(level Level, keyvals ...interface{}) error { + kvs := make([]interface{}, 0, len(c.prefix)+len(keyvals)) + kvs = append(kvs, c.prefix...) + if c.hasValuer { + bindValues(c.ctx, kvs) + } + kvs = append(kvs, keyvals...) + return c.logger.Log(level, kvs...) +} + +// With with logger fields. +func With(l Logger, kv ...interface{}) Logger { + c, ok := l.(*logger) + if !ok { + return &logger{logger: l, prefix: kv, hasValuer: containsValuer(kv), ctx: context.Background()} + } + kvs := make([]interface{}, 0, len(c.prefix)+len(kv)) + kvs = append(kvs, c.prefix...) + kvs = append(kvs, kv...) + return &logger{ + logger: c.logger, + prefix: kvs, + hasValuer: containsValuer(kvs), + ctx: c.ctx, + } +} + +// WithContext returns a shallow copy of l with its context changed +// to ctx. The provided ctx must be non-nil. +func WithContext(ctx context.Context, l Logger) Logger { + switch v := l.(type) { + default: + return &logger{logger: l, ctx: ctx} + case *logger: + lv := *v + lv.ctx = ctx + return &lv + case *Filter: + fv := *v + fv.logger = WithContext(ctx, fv.logger) + return &fv + } +} diff --git a/utils/kratos/value.go b/utils/kratos/value.go new file mode 100644 index 00000000..af88c969 --- /dev/null +++ b/utils/kratos/value.go @@ -0,0 +1,66 @@ +// copied from https://github.com/go-kratos/kratos/tree/main/log +package log + +import ( + "context" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + // DefaultCaller is a Valuer that returns the file and line. + DefaultCaller = Caller(4) + + // DefaultTimestamp is a Valuer that returns the current wallclock time. + DefaultTimestamp = Timestamp(time.RFC3339) +) + +// Valuer is returns a log value. +type Valuer func(ctx context.Context) interface{} + +// Value return the function value. +func Value(ctx context.Context, v interface{}) interface{} { + if v, ok := v.(Valuer); ok { + return v(ctx) + } + return v +} + +// Caller returns a Valuer that returns a pkg/file:line description of the caller. +func Caller(depth int) Valuer { + return func(context.Context) interface{} { + _, file, line, _ := runtime.Caller(depth) + idx := strings.LastIndexByte(file, '/') + if idx == -1 { + return file[idx+1:] + ":" + strconv.Itoa(line) + } + idx = strings.LastIndexByte(file[:idx], '/') + return file[idx+1:] + ":" + strconv.Itoa(line) + } +} + +// Timestamp returns a timestamp Valuer with a custom time format. +func Timestamp(layout string) Valuer { + return func(context.Context) interface{} { + return time.Now().Format(layout) + } +} + +func bindValues(ctx context.Context, keyvals []interface{}) { + for i := 1; i < len(keyvals); i += 2 { + if v, ok := keyvals[i].(Valuer); ok { + keyvals[i] = v(ctx) + } + } +} + +func containsValuer(keyvals []interface{}) bool { + for i := 1; i < len(keyvals); i += 2 { + if _, ok := keyvals[i].(Valuer); ok { + return true + } + } + return false +} diff --git a/utils/kratos/zap.go b/utils/kratos/zap.go new file mode 100644 index 00000000..b3883390 --- /dev/null +++ b/utils/kratos/zap.go @@ -0,0 +1,148 @@ +package log + +import ( + "encoding/json" + "os" + + "github.com/natefinch/lumberjack" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "moul.io/zapfilter" +) + +// TODO:或许有更好的方案,目前只是保证能够使用了 +// 搬运过来WriterX,然后默认初始化,给一个方式获取那个WriterX + +var logLimitDefault int64 = 100 +var originZapLogger *zap.Logger + +// GetLoggerRaw 特殊情况下,获取原生的LOGGER进行处理 +func GetLoggerRaw() *zap.Logger { + return originZapLogger +} + +type LogItem struct { + Level string `json:"level"` + TS float64 `json:"ts"` + Caller string `json:"caller"` + Msg string `json:"msg"` +} + +type WriterX struct { + LogLimit int64 + Items []*LogItem +} + +func (w *WriterX) Write(p []byte) (n int, err error) { + var a LogItem + err2 := json.Unmarshal(p, &a) + if err2 == nil { + w.Items = append(w.Items, &a) + limit := w.LogLimit + if limit == 0 { + w.LogLimit = logLimitDefault + } + if len(w.Items) > int(limit) { + w.Items = w.Items[1:] + } + } + return len(p), nil +} + +var enabledLevel = zap.InfoLevel + +func SetEnableLevel(level zapcore.Level) { + switch level { + case zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel, + zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel: + { + enabledLevel = level + } + default: // no-op + } +} + +// InitZapWithKartosLog 将所有的信息都会输出到main.log,以及输出到控制台 +func InitZapWithKartosLog(level zapcore.Level) { + SetEnableLevel(level) + // 日志文件的路径 + path := "./data/main.log" + + // 使用lumberjack进行日志文件轮转配置 + lumlog := &lumberjack.Logger{ + Filename: path, // 日志文件的名称和路径 + MaxSize: 10, // 每个日志文件最大10MB + MaxBackups: 3, // 最多保留3个旧日志文件 + MaxAge: 7, // 日志文件保存7天 + } + + // 获取日志编码器,定义日志的输出格式 + encoder := getEncoder() + + // 输出到UI的配置部分 + pe := zap.NewProductionEncoderConfig() + global.wx = &WriterX{} + // 输出到文件的配置部分 + mainLogCore := zapcore.NewCore(encoder, zapcore.AddSync(lumlog), zapcore.DebugLevel) + // 创建控制台的日志编码器 + consoleCoreRaw := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), enabledLevel) + // 适配隐藏控制台输出的部分,重新设置日志级别,并输出除了HIDE以外的所有情况。这里ByNamespaces注意要先定义”全部选择“,然后定义”HIDE的不要“。 + consoleCore := zapfilter.NewFilteringCore(consoleCoreRaw, zapfilter.All(zapfilter.MinimumLevel(enabledLevel), zapfilter.ByNamespaces("*,-HIDE.*"))) + + // 创建日志核心,将日志写入lumberjack的文件中,并设置日志级别为Debug + cores := []zapcore.Core{ + // 默认输出到main.log的,全量日志文件 + mainLogCore, + // 默认输出到UI的,只输出Info级别 + // This outputs to WebUI, DO NOT apply enabledLevel + zapcore.NewCore(zapcore.NewJSONEncoder(pe), zapcore.AddSync(global.wx), zapcore.InfoLevel), + consoleCore, + } + + // 将多个日志核心组合到一起,以同时记录到文件和控制台 + core := zapcore.NewTee(cores...) + + // 创建带有调用者信息的日志记录器,注意跳过两层,这样就能正常提供给log + originZapLogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2)) + // 设置全局日志记录器,默认全局记录器为SEAL命名空间 + global.SetLogger(NewZapLogger(originZapLogger.Named(LOG_SEAL))) + // GORM部分 + SetDefaultGoRMLogger() +} + +func SetDefaultGoRMLogger() { + gormpath := "./data/database.log" + gormpathlumlog := &lumberjack.Logger{ + Filename: gormpath, // 日志文件的名称和路径 + MaxSize: 10, // 每个日志文件最大10MB + MaxBackups: 3, // 最多保留3个旧日志文件 + MaxAge: 7, // 日志文件保存7天 + } + gormCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(gormpathlumlog), zapcore.DebugLevel) + // 层层进行包装 + gormZapLogger := NewHelper(NewZapLogger(zap.New(gormCore).Named("GORM").WithOptions(zap.WithCaller(true), zap.AddCallerSkip(6)))) + + NewGormLogger(gormZapLogger).SetAsDefault() +} + +func GetWebLogger() *Helper { + webpath := "./data/web.log" + // WEB的可以少一点点~ + weblumlog := &lumberjack.Logger{ + Filename: webpath, // 日志文件的名称和路径 + MaxSize: 5, // 每个日志文件最大5MB + MaxBackups: 3, // 最多保留1个旧日志文件 + MaxAge: 3, // 日志文件保存3天 + } + webCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(weblumlog), zapcore.DebugLevel) + webZapLogger := zap.New(webCore, zap.WithCaller(false)) + return NewHelper(NewZapLogger(webZapLogger.Named("WEB"))) +} + +func getEncoder() zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + + return zapcore.NewConsoleEncoder(encoderConfig) +} diff --git a/utils/kratos/zaplogger.go b/utils/kratos/zaplogger.go new file mode 100644 index 00000000..9fd05636 --- /dev/null +++ b/utils/kratos/zaplogger.go @@ -0,0 +1,75 @@ +// copied from github.com/go-kratos/kratos/contrib/log/zap/v2 +package log + +import ( + "fmt" + + "go.uber.org/zap" +) + +var _ Logger = (*ZapLogger)(nil) + +type ZapLogger struct { + log *zap.Logger + msgKey string +} + +func NewZapLogger(zlog *zap.Logger) *ZapLogger { + return &ZapLogger{ + log: zlog, + msgKey: DefaultMessageKey, + } +} + +// 保留给Helper使用 + +type ZapOption func(*ZapLogger) + +// WithZapMessageKey with message key. +func WithZapMessageKey(key string) ZapOption { + return func(l *ZapLogger) { + l.msgKey = key + } +} + +func (l *ZapLogger) Log(level Level, keyvals ...interface{}) error { + var ( + msg = "" + keylen = len(keyvals) + ) + if keylen == 0 || keylen%2 != 0 { + l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals)) + return nil + } + + data := make([]zap.Field, 0, (keylen/2)+1) + for i := 0; i < keylen; i += 2 { + if keyvals[i].(string) == l.msgKey { + msg, _ = keyvals[i+1].(string) + continue + } + data = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1])) + } + + switch level { + case LevelDebug: + l.log.Debug(msg, data...) + case LevelInfo: + l.log.Info(msg, data...) + case LevelWarn: + l.log.Warn(msg, data...) + case LevelError: + l.log.Error(msg, data...) + case LevelFatal: + l.log.Fatal(msg, data...) + } + return nil +} + +func (l *ZapLogger) Sync() error { + return l.log.Sync() +} + +func (l *ZapLogger) Close() error { + return l.Sync() +} diff --git a/utils/oschecker/checker_other.go b/utils/oschecker/checker_other.go new file mode 100644 index 00000000..9e2d65f0 --- /dev/null +++ b/utils/oschecker/checker_other.go @@ -0,0 +1,9 @@ +//go:build !windows +// +build !windows + +package oschecker + +// 我们没有对其他的系统进行筛查的打算。 +func OldVersionCheck() (bool, string) { + return false, "NOTHING" +} diff --git a/utils/oschecker/checker_windows.go b/utils/oschecker/checker_windows.go new file mode 100644 index 00000000..67b55097 --- /dev/null +++ b/utils/oschecker/checker_windows.go @@ -0,0 +1,206 @@ +//go:build windows +// +build windows + +package oschecker + +// copied from https://github.com/jfjallid/go-secdump/blob/e307524e114f9abb39e2cd2b13ae421aae02d2de/utils.go with some changes + +import ( + "fmt" + "strconv" + "strings" + "syscall" + + "github.com/lxn/win" + "golang.org/x/sys/windows/registry" + + log "sealdice-core/utils/kratos" +) + +const ( + WIN_UNKNOWN = iota + WINXP + WIN_SERVER_2003 + WIN_VISTA + WIN_SERVER_2008 + WIN7 + WIN_SERVER_2008_R2 + WIN8 + WIN_SERVER_2012 + WIN81 + WIN_SERVER_2012_R2 + WIN10 + WIN_SERVER_2016 + WIN_SERVER_2019 + WIN_SERVER_2022 + WIN11 +) + +var osNameMap = map[byte]string{ + WIN_UNKNOWN: "Windows Unknown", + WINXP: "Windows XP", + WIN_VISTA: "Windows Vista", + WIN7: "Windows 7", + WIN8: "Windows 8", + WIN81: "Windows 8.1", + WIN10: "Windows 10", + WIN11: "Windows 11", + WIN_SERVER_2003: "Windows Server 2003", + WIN_SERVER_2008: "Windows Server 2008", + WIN_SERVER_2008_R2: "Windows Server 2008 R2", + WIN_SERVER_2012: "Windows Server 2012", + WIN_SERVER_2012_R2: "Windows Server 2012 R2", + WIN_SERVER_2016: "Windows Server 2016", + WIN_SERVER_2019: "Windows Server 2019", + WIN_SERVER_2022: "Windows Server 2022", +} + +// OldVersionCheck 只获取最低版本 +func OldVersionCheck() (bool, string) { + build, f, b, err := getOSVersionBuild() + if err != nil { + // 不知道的版本,就认为是支持的 + showNoticeBox("版本确认提示", "海豹无法获取您的操作系统版本,请确认正在使用 Windows 10/Windows Server 2016 或更高版本的 Windows。") + return true, osNameMap[WIN_UNKNOWN] + } + os := GetOSVersion(build, f, b) + // 这里用WinXP打底的原因是,WinXP下面是未知系统,我们默认放行未知系统 + if (WINXP <= os) && (os < WIN10) { + // 展示提示弹窗,提示用户升级 + showMsgBox("版本升级提示", fmt.Sprintf("您的操作系统版本「%s」过旧,海豹未来将不再支持,请尽快升级系统至 Windows 10/Windows Server 2016 或更高版本。", osNameMap[os])) + return true, osNameMap[os] + } else { + return false, osNameMap[os] + } +} + +func showMsgBox(title string, message string) { + s1, _ := syscall.UTF16PtrFromString(title) + s2, _ := syscall.UTF16PtrFromString(message) + win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR) +} + +func showNoticeBox(title string, message string) { + s1, _ := syscall.UTF16PtrFromString(title) + s2, _ := syscall.UTF16PtrFromString(message) + win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONWARNING) +} + +func GetOSVersion(currentBuild int, currentVersion float64, server bool) (os byte) { + currentVersionStr := strconv.FormatFloat(currentVersion, 'f', 1, 64) + if server { + switch { + case currentBuild >= 3790 && currentBuild < 6001: + os = WIN_SERVER_2003 + case currentBuild >= 6001 && currentBuild < 7601: + os = WIN_SERVER_2008 + case currentBuild >= 7601 && currentBuild < 9200: + os = WIN_SERVER_2008_R2 + case currentBuild >= 9200 && currentBuild < 9600: + os = WIN_SERVER_2012 + case currentBuild >= 9200 && currentBuild < 14393: + os = WIN_SERVER_2012_R2 + case currentBuild >= 14393 && currentBuild < 17763: + os = WIN_SERVER_2016 + case currentBuild >= 17763 && currentBuild < 20348: + os = WIN_SERVER_2019 + case currentBuild >= 20348: + os = WIN_SERVER_2022 + default: + log.Debugf("Unknown server version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) + os = WIN_UNKNOWN + } + } else { + switch currentVersionStr { + case "5.1": + os = WINXP + case "6.0": + // Windows Vista but it shares CurrentVersion and CurrentBuild with Windows Server 2008 + os = WIN_VISTA + case "6.1": + // Windows 7 but it shares CurrentVersion and CurrentBuild with Windows Server 2008 R2 + os = WIN7 + case "6.2": + // Windows 8 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 + os = WIN8 + case "6.3": + // Windows 8.1 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 R2 + os = WIN81 + case "10.0": + if currentBuild < 22000 { + os = WIN10 + } else { + os = WIN11 + } + default: + log.Debugf("Unknown version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion) + os = WIN_UNKNOWN + } + } + + log.Debugf("OS Version: %s\n", osNameMap[os]) + return +} + +func getOSVersionBuild() (build int, version float64, server bool, err error) { + hSubKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + log.Errorf("Failed to open registry key CurrentVersion with error: %v\n", err) + return + } + defer func(hSubKey registry.Key) { + err = hSubKey.Close() + if err != nil { + log.Fatalf("Failed to close hSubkey with error: %v\n", err) + } + }(hSubKey) + + buildStr, _, err := hSubKey.GetStringValue("CurrentBuild") + if err != nil { + log.Error(err) + return + } + build, err = strconv.Atoi(buildStr) + if err != nil { + log.Error(err) + return + } + versionStr, _, err := hSubKey.GetStringValue("CurrentVersion") + if err != nil { + log.Error(err) + return + } + + version, err = strconv.ParseFloat(versionStr, 32) + if err != nil { + log.Errorf("Failed to get CurrentVersion with error: %v\n", err) + return + } + // 二次判断:由于有Win8升级成Win10的情况,这个参数不准确。这个参数只有Win10往上有,所以下面 + majorVersionStr, _, err := hSubKey.GetIntegerValue("CurrentMajorVersionNumber") + if err != nil { + log.Debug("非Win8以上系统,不包含CurrentMajorVersionNumber参数。") + } + // TODO: 据说,当前Win11和Win10的大版本号还相同,没有Win11,难以测试 + if majorVersionStr == 10 { + version = 10.0 + } + + hSubKey, err = registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\ProductOptions`, registry.QUERY_VALUE) + if err != nil { + log.Errorf("Failed to open registry key ProductOptions with error: %v\n", err) + return + } + + serverFlag, _, err := hSubKey.GetStringValue("ProductType") + if err != nil { + log.Error(err) + return + } + + if strings.Compare(serverFlag, "ServerNT") == 0 { + server = true + } + + return +} diff --git a/utils/paniclog/paniclog.go b/utils/paniclog/paniclog.go new file mode 100644 index 00000000..9dc835d5 --- /dev/null +++ b/utils/paniclog/paniclog.go @@ -0,0 +1,35 @@ +package paniclog + +import ( + "fmt" + "io" + "os" + "time" + + log "sealdice-core/utils/kratos" +) + +func InitPanicLog() { + // TODO: 全局写死写入在data目录,这东西几乎没有任何值得配置的 + if err := os.MkdirAll("./data", 0755); err != nil { + log.Fatalf("未发现data文件夹,且未能创建data文件夹,请检查写入权限: %v", err) + } + f, err := os.OpenFile("./data/panic.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + log.Fatalf("未能创建panic日志文件,请检查写入权限: %v", err) + } + // Copied from https://github.com/rclone/rclone/tree/master/fs/log + // 这里GPT说,因为使用了APPEND,所以保证了不需要使用SEEK。但是rclone既然这么用了,我决定相信rclone的处理。 + _, err = f.Seek(0, io.SeekEnd) + if err != nil { + log.Errorf("移动写入位置到末尾失败,请检查写入权限: %v", err) + } + currentTime := time.Now().Format("2006-01-02 15:04:05") + separator := fmt.Sprintf("\n-------- %s --------\n", currentTime) + // 将分割线写入文件 + _, err = f.WriteString(separator) + if err != nil { + log.Fatalf("写入Panic日志分割线失败,请检查写入权限: %v", err) + } + redirectStderr(f) +} diff --git a/utils/paniclog/redirect_stderr.go b/utils/paniclog/redirect_stderr.go new file mode 100644 index 00000000..7b64dac4 --- /dev/null +++ b/utils/paniclog/redirect_stderr.go @@ -0,0 +1,18 @@ +// Copied from https://github.com/rclone/rclone/tree/master/fs/log +// Log the panic to the log file - for oses which can't do this + +//go:build !windows && !darwin && !dragonfly && !freebsd && !linux && !nacl && !netbsd && !openbsd + +package paniclog + +import ( + "os" + + log "sealdice-core/utils/kratos" +) + +// redirectStderr to the file passed in +func redirectStderr(f *os.File) { + // 安卓当前还暂时没有什么头绪,看上去rclone也没头绪。 + log.Error("Can't redirect stderr to file") +} diff --git a/utils/paniclog/redirect_stderr_unix.go b/utils/paniclog/redirect_stderr_unix.go new file mode 100644 index 00000000..e078b9cd --- /dev/null +++ b/utils/paniclog/redirect_stderr_unix.go @@ -0,0 +1,22 @@ +// Copied from https://github.com/rclone/rclone/tree/master/fs/log +// Log the panic under unix to the log file + +//go:build !windows && !solaris && !plan9 && !js + +package paniclog + +import ( + "os" + + "golang.org/x/sys/unix" + + log "sealdice-core/utils/kratos" +) + +// redirectStderr to the file passed in +func redirectStderr(f *os.File) { + err := unix.Dup2(int(f.Fd()), int(os.Stderr.Fd())) + if err != nil { + log.Fatalf("Failed to redirect stderr to file: %v", err) + } +} diff --git a/utils/paniclog/redirect_stderr_windows.go b/utils/paniclog/redirect_stderr_windows.go new file mode 100644 index 00000000..8a6d7a82 --- /dev/null +++ b/utils/paniclog/redirect_stderr_windows.go @@ -0,0 +1,45 @@ +// Copied from https://github.com/rclone/rclone/tree/master/fs/log +// Log the panic under windows to the log file +// +// Code from minix, via +// +// https://play.golang.org/p/kLtct7lSUg + +//go:build windows + +package paniclog + +import ( + "os" + "syscall" + + log "sealdice-core/utils/kratos" +) + +var ( + kernel32 = syscall.MustLoadDLL("kernel32.dll") + procSetStdHandle = kernel32.MustFindProc("SetStdHandle") +) + +func setStdHandle(stdhandle int32, handle syscall.Handle) error { + r0, _, e1 := syscall.SyscallN(procSetStdHandle.Addr(), uintptr(stdhandle), uintptr(handle)) + if r0 == 0 { + if e1 != 0 { + return error(e1) + } + return syscall.EINVAL + } + return nil +} + +// redirectStderr to the file passed in +func redirectStderr(f *os.File) { + err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd())) + if err != nil { + log.Fatalf("Failed to redirect stderr to file: %v", err) + } + // https://stackoverflow.com/questions/34772012/capturing-panic-in-golang rclone can't get some + // I did some more experimenting and on window's you must also do os.Stderr = f since SetStdHandle does not affect the prior reference to stderr. + // On unix it is not necessary since the Dup2 does affect the prior reference to stderr. ( Tim Lewis Commented) + os.Stderr = f +} diff --git a/utils/procs/procs.go b/utils/procs/procs.go index 0baae797..a65b9913 100644 --- a/utils/procs/procs.go +++ b/utils/procs/procs.go @@ -3,11 +3,12 @@ package procs import ( "bufio" "bytes" - "fmt" "io" "os/exec" "strings" + log "sealdice-core/utils/kratos" + "github.com/fyrchik/go-shlex" ) @@ -87,7 +88,7 @@ func (p *Process) Start() error { go func() { defer func() { if r := recover(); r != nil { - fmt.Println("Recovered from panic:", r) + log.Errorf("Recovered from panic: %v", r) } }() @@ -107,7 +108,7 @@ func (p *Process) Start() error { go func() { defer func() { if r := recover(); r != nil { - fmt.Println("Recovered from panic:", r) + log.Errorf("Recovered from panic: %v", r) } }() diff --git a/utils/public_dice/sdk.go b/utils/public_dice/sdk.go new file mode 100644 index 00000000..55e9e741 --- /dev/null +++ b/utils/public_dice/sdk.go @@ -0,0 +1,165 @@ +package public_dice + +import ( + "github.com/monaco-io/request" +) + +// PublicDiceClient SDK客户端 +type PublicDiceClient struct { + baseURL string + token string +} + +// NewClient 创建新的SDK客户端 +func NewClient(baseURL string, token string) *PublicDiceClient { + return &PublicDiceClient{ + baseURL: baseURL, + token: token, + } +} + +// doReq 发送HTTP请求 +func doReq[T any](c *PublicDiceClient, method string, path string, data any, params map[string]string) (*T, int) { + req := request.Client{ + URL: c.baseURL + path, + Method: method, + JSON: data, + Query: params, + } + + var result T + resp := req.Send() + resp.Scan(&result) + + return &result, resp.Code() +} + +// Endpoint 公骰终端信息 +type Endpoint struct { + Platform string `json:"platform" msgpack:",omitempty"` + UID string `json:"uid" msgpack:",omitempty"` + InviteURL string `json:"inviteUrl" msgpack:",omitempty"` + IsOnline bool `json:"isOnline" msgpack:",omitempty"` + + ID string `json:"id" msgpack:",omitempty"` + CreatedAt string `json:"createdAt" msgpack:",omitempty"` + UpdatedAt string `json:"updatedAt" msgpack:",omitempty"` + LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"` +} + +// DiceInfo 公骰信息 +type DiceInfo struct { + ID string `json:"id" msgpack:",omitempty"` + CreatedAt string `json:"createdAt" msgpack:",omitempty"` + UpdatedAt string `json:"updatedAt" msgpack:",omitempty"` + Name string `json:"name" msgpack:",omitempty"` + Brief string `json:"brief" msgpack:",omitempty"` + Note string `json:"note" msgpack:",omitempty"` + Avatar string `json:"avatar" msgpack:",omitempty"` + Version string `json:"version" msgpack:",omitempty"` + IsOfficialVersion bool `json:"isOfficialVersion" msgpack:",omitempty"` + UpdateTickCount int `json:"updateTickCount" msgpack:",omitempty"` + LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"` + Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"` +} + +// ListResponse 公骰列表响应 +type ListResponse struct { + Items []*DiceInfo `json:"items"` +} + +// ListGet 获取公骰列表 +func (c *PublicDiceClient) ListGet(keyFunc func(data any) string) (*ListResponse, int) { + if keyFunc != nil { + data := keyFunc(nil) + return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", data, nil) + } + return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", nil, nil) +} + +// RegisterRequest 注册公骰请求 +type RegisterRequest struct { + ID string `json:"ID,omitempty" msgpack:",omitempty"` + Name string `json:"name,omitempty" msgpack:",omitempty"` // 15字 + Brief string `json:"brief,omitempty" msgpack:",omitempty"` + Note string `json:"note,omitempty" msgpack:",omitempty"` + Avatar string `json:"avatar,omitempty" msgpack:",omitempty"` // 头像?还是用另一个api进行注册比较好?可以省略 + Key string `json:"key,omitempty" msgpack:",omitempty"` +} + +// RegisterResponse 注册公骰响应 +type RegisterResponse struct { + Item DiceInfo `json:"item"` +} + +// Register 注册公骰 +func (c *PublicDiceClient) Register(req *RegisterRequest, keyFunc func(data any) string) (*RegisterResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[RegisterResponse](c, "POST", "/dice/api/public-dice/register", req, nil) +} + +// DiceUpdateRequest 更新公骰请求 +type DiceUpdateRequest struct { + ID string `json:"id" msgpack:",omitempty"` + Name string `json:"name" msgpack:",omitempty"` + Brief string `json:"brief" msgpack:",omitempty"` + Note string `json:"note" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` +} + +// DiceUpdateResponse 更新公骰响应 +type DiceUpdateResponse struct { + Updated int `json:"updated"` +} + +// DiceUpdate 更新公骰 +func (c *PublicDiceClient) DiceUpdate(req *DiceUpdateRequest, keyFunc func(data any) string) (*DiceUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[DiceUpdateResponse](c, "POST", "/dice/api/public-dice/register?update=1", req, nil) +} + +// EndpointUpdateRequest 更新公骰SNS账号信息请求 +type EndpointUpdateRequest struct { + DiceID string `json:"diceId" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` + Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"` +} + +// EndpointUpdateResponse 更新公骰SNS账号信息响应 +type EndpointUpdateResponse struct{} + +// EndpointUpdate 更新公骰SNS账号信息 +func (c *PublicDiceClient) EndpointUpdate(req *EndpointUpdateRequest, keyFunc func(data any) string) (*EndpointUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[EndpointUpdateResponse](c, "POST", "/dice/api/public-dice/endpoint-update", req, nil) +} + +// TickUpdateRequest 更新公骰心跳请求 +type TickUpdateRequest struct { + ID string `json:"ID" msgpack:",omitempty"` + Key string `json:"key" msgpack:",omitempty"` + Endpoints []*TickEndpoint `json:"Endpoints" msgpack:",omitempty"` +} + +// TickEndpoint 公骰心跳端点信息 +type TickEndpoint struct { + UID string `json:"uid" msgpack:",omitempty"` + IsOnline bool `json:"isOnline" msgpack:",omitempty"` +} + +// TickUpdateResponse 更新公骰心跳响应 +type TickUpdateResponse struct{} + +// TickUpdate 更新公骰心跳 +func (c *PublicDiceClient) TickUpdate(req *TickUpdateRequest, keyFunc func(data any) string) (*TickUpdateResponse, int) { + if keyFunc != nil { + req.Key = keyFunc(req) + } + return doReq[TickUpdateResponse](c, "POST", "/dice/api/public-dice/tick-update", req, nil) +}