From 5551a8bc8ddcdd784e6fd804f72d9a38359948df Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Fri, 1 Aug 2025 16:44:59 +0200 Subject: [PATCH 01/10] feat: add /xmppbridge unmap command for channel unmapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DeleteChannelRoomMapping method to Bridge interface - Implement channel unmapping logic in XMPP bridge (cache + KVStore removal) - Add /xmppbridge unmap command handler with validation - Bridge user automatically leaves XMPP room when unmapping - Update command help text and autocomplete 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- server/bridge/xmpp/bridge.go | 45 +++++++++++++++++++++++++++++ server/command/command.go | 56 ++++++++++++++++++++++++++++++++++-- server/model/bridge.go | 3 ++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index 4c6392a..d180d80 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -441,3 +441,48 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) { return roomJID, nil } + +// DeleteChannelRoomMapping removes a mapping between a Mattermost channel and XMPP room +func (b *xmppBridge) DeleteChannelRoomMapping(channelID string) error { + if b.kvstore == nil { + return fmt.Errorf("KV store not initialized") + } + + // Get the room JID from the mapping before deleting + roomJID, err := b.GetChannelRoomMapping(channelID) + if err != nil { + return fmt.Errorf("failed to get channel mapping: %w", err) + } + if roomJID == "" { + return fmt.Errorf("channel is not mapped to any room") + } + + // Delete forward and reverse mappings from KV store + err = b.kvstore.Delete(kvstore.BuildChannelMapKey("mattermost", channelID)) + if err != nil { + return fmt.Errorf("failed to delete channel room mapping: %w", err) + } + + err = b.kvstore.Delete(kvstore.BuildChannelMapKey("xmpp", roomJID)) + if err != nil { + return fmt.Errorf("failed to delete reverse room mapping: %w", err) + } + + // Remove from local cache + b.mappingsMu.Lock() + delete(b.channelMappings, channelID) + b.mappingsMu.Unlock() + + // Leave the room if connected + if b.connected.Load() && b.xmppClient != nil { + if err := b.xmppClient.LeaveRoom(roomJID); err != nil { + b.logger.LogWarn("Failed to leave unmapped room", "channel_id", channelID, "room_jid", roomJID, "error", err) + // Don't fail the entire operation if leaving the room fails + } else { + b.logger.LogInfo("Left XMPP room after unmapping", "channel_id", channelID, "room_jid", roomJID) + } + } + + b.logger.LogInfo("Deleted channel room mapping", "channel_id", channelID, "room_jid", roomJID) + return nil +} diff --git a/server/command/command.go b/server/command/command.go index 1feed2d..77d78b7 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -29,6 +29,9 @@ func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.Bridg mapSubcommand.AddTextArgument("XMPP room JID (e.g., room@conference.example.com)", "[room_jid]", "") xmppBridgeData.AddCommand(mapSubcommand) + unmapSubcommand := model.NewAutocompleteData("unmap", "", "Unmap current channel from XMPP room") + xmppBridgeData.AddCommand(unmapSubcommand) + statusSubcommand := model.NewAutocompleteData("status", "", "Show bridge connection status") xmppBridgeData.AddCommand(statusSubcommand) @@ -36,7 +39,7 @@ func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.Bridg Trigger: xmppBridgeCommandTrigger, AutoComplete: true, AutoCompleteDesc: "Manage XMPP bridge mappings", - AutoCompleteHint: "[map|status]", + AutoCompleteHint: "[map|unmap|status]", AutocompleteData: xmppBridgeData, }) if err != nil { @@ -72,6 +75,7 @@ func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.Comma **Available commands:** - ` + "`/xmppbridge map `" + ` - Map current channel to XMPP room +- ` + "`/xmppbridge unmap`" + ` - Unmap current channel from XMPP room - ` + "`/xmppbridge status`" + ` - Show bridge connection status **Example:** @@ -83,6 +87,8 @@ func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.Comma switch subcommand { case "map": return c.executeMapCommand(args, fields) + case "unmap": + return c.executeUnmapCommand(args) case "status": return c.executeStatusCommand(args) default: @@ -155,11 +161,54 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeInChannel, + ResponseType: model.CommandResponseTypeEphemeral, Text: fmt.Sprintf("✅ Successfully mapped this channel to XMPP room: `%s`", roomJID), } } +func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandResponse { + channelID := args.ChannelId + + // Get the XMPP bridge + bridge, err := c.bridgeManager.GetBridge("xmpp") + if err != nil { + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: "❌ XMPP bridge is not available. Please check the plugin configuration.", + } + } + + // Check if channel is mapped + roomJID, err := bridge.GetChannelRoomMapping(channelID) + if err != nil { + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf("Error checking existing mapping: %v", err), + } + } + + if roomJID == "" { + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: "❌ This channel is not mapped to any XMPP room.", + } + } + + // Delete the mapping + err = bridge.DeleteChannelRoomMapping(channelID) + if err != nil { + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf("❌ Failed to unmap channel: %v", err), + } + } + + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf("✅ Successfully unmapped this channel from XMPP room: `%s`", roomJID), + } +} + func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandResponse { // Get the XMPP bridge bridge, err := c.bridgeManager.GetBridge("xmpp") @@ -201,6 +250,7 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe %s **Commands:** -- Use `+"`/xmppbridge map `"+` to map this channel to an XMPP room`, statusText, mappingText), +- Use `+"`/xmppbridge map `"+` to map this channel to an XMPP room +- Use `+"`/xmppbridge unmap`"+` to unmap this channel from an XMPP room`, statusText, mappingText), } } diff --git a/server/model/bridge.go b/server/model/bridge.go index c9c7a3d..2a01ac6 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -57,6 +57,9 @@ type Bridge interface { // GetChannelRoomMapping retrieves the bridge room ID for a given Mattermost channel ID. GetChannelRoomMapping(channelID string) (string, error) + // DeleteChannelRoomMapping removes a mapping between a Mattermost channel ID and a bridge room ID. + DeleteChannelRoomMapping(channelID string) error + // IsConnected checks if the bridge is connected to the remote service. IsConnected() bool } From 5d143808a3871d516036d4f3ce8e56018d4a0080 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Fri, 1 Aug 2025 16:58:02 +0200 Subject: [PATCH 02/10] feat: add direct message testing to XMPP doctor command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add reusable MessageBody and XMPPMessage structs to xmpp client - Refactor SendMessage to use shared structs instead of inline definitions - Add SendDirectMessage method for direct user messaging (type="chat") - Enhance doctor command with --test-dm flag (enabled by default) - Add testDirectMessage function that sends test message to admin@localhost - Update help text, examples, and timing measurements for direct messages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/xmpp-client-doctor/main.go | 58 ++- go.mod | 173 ++++++- go.sum | 837 ++++++++++++++++++++++++++++++++- server/xmpp/client.go | 66 ++- 4 files changed, 1110 insertions(+), 24 deletions(-) diff --git a/cmd/xmpp-client-doctor/main.go b/cmd/xmpp-client-doctor/main.go index 020e5da..f1ccaf0 100644 --- a/cmd/xmpp-client-doctor/main.go +++ b/cmd/xmpp-client-doctor/main.go @@ -27,6 +27,7 @@ type Config struct { Resource string TestRoom string TestMUC bool + TestDirectMessage bool Verbose bool InsecureSkipVerify bool } @@ -41,20 +42,22 @@ func main() { flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource") flag.StringVar(&config.TestRoom, "test-room", defaultTestRoom, "MUC room JID for testing") flag.BoolVar(&config.TestMUC, "test-muc", true, "Enable MUC room testing (join/wait/leave)") + flag.BoolVar(&config.TestDirectMessage, "test-dm", true, "Enable direct message testing (send message to admin user)") flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging") flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity and MUC operations\n\n") fmt.Fprintf(os.Stderr, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n") - fmt.Fprintf(os.Stderr, "performing connection tests, optionally testing MUC room operations,\n") + fmt.Fprintf(os.Stderr, "performing connection tests, optionally testing MUC room operations and direct messages,\n") fmt.Fprintf(os.Stderr, "and then disconnecting gracefully.\n\n") fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Examples:\n") fmt.Fprintf(os.Stderr, " %s # Test basic connectivity\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s --test-muc # Test connectivity and MUC operations\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " %s --test-muc=false # Test connectivity only\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s --test-dm # Test connectivity and direct messages\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s --test-muc=false --test-dm=false # Test connectivity only\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n") @@ -75,6 +78,9 @@ func main() { if config.TestMUC { log.Printf(" Test Room: %s", config.TestRoom) } + if config.TestDirectMessage { + log.Printf(" Test Direct Messages: enabled") + } } // Test the XMPP client @@ -89,6 +95,9 @@ func main() { if config.TestMUC { fmt.Println("✅ XMPP MUC operations test passed!") } + if config.TestDirectMessage { + fmt.Println("✅ XMPP direct message test passed!") + } } } @@ -154,6 +163,7 @@ func testXMPPClient(config *Config) error { } var mucDuration time.Duration + var dmDuration time.Duration // Test MUC operations if requested if config.TestMUC { @@ -165,6 +175,16 @@ func testXMPPClient(config *Config) error { mucDuration = time.Since(start) } + // Test direct message if requested + if config.TestDirectMessage { + start = time.Now() + err = testDirectMessage(client, config) + if err != nil { + return fmt.Errorf("direct message test failed: %w", err) + } + dmDuration = time.Since(start) + } + if config.Verbose { log.Printf("Disconnecting from XMPP server...") } @@ -185,11 +205,17 @@ func testXMPPClient(config *Config) error { if config.TestMUC { log.Printf(" MUC operations time: %v", mucDuration) } + if config.TestDirectMessage { + log.Printf(" Direct message time: %v", dmDuration) + } log.Printf(" Disconnect time: %v", disconnectDuration) totalTime := connectDuration + pingDuration + disconnectDuration if config.TestMUC { totalTime += mucDuration } + if config.TestDirectMessage { + totalTime += dmDuration + } log.Printf(" Total time: %v", totalTime) } @@ -265,6 +291,34 @@ func testMUCOperations(client *xmpp.Client, config *Config) error { return nil } +func testDirectMessage(client *xmpp.Client, config *Config) error { + if config.Verbose { + log.Printf("Testing direct message functionality...") + log.Printf("Sending test message to admin user...") + } + + // Send a test message to the admin user + testMessage := fmt.Sprintf("Test direct message from XMPP doctor at %s", time.Now().Format("15:04:05")) + adminJID := "admin@localhost" // Default admin user for development server + + start := time.Now() + err := client.SendDirectMessage(adminJID, testMessage) + if err != nil { + return fmt.Errorf("failed to send direct message to %s: %w", adminJID, err) + } + sendDuration := time.Since(start) + + if config.Verbose { + log.Printf("✅ Successfully sent direct message in %v", sendDuration) + log.Printf("Message: %s", testMessage) + log.Printf("Recipient: %s", adminJID) + log.Printf("Direct message test summary:") + log.Printf(" Send message time: %v", sendDuration) + } + + return nil +} + func maskPassword(password string) string { if len(password) <= 2 { return "****" diff --git a/go.mod b/go.mod index 0dd6bd5..efd6373 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/mattermost/mattermost-plugin-bridge-xmpp go 1.24.3 require ( - github.com/golang/mock v1.6.0 github.com/gorilla/mux v1.8.1 github.com/mattermost/mattermost/server/public v0.1.10 github.com/pkg/errors v0.9.1 @@ -13,53 +12,225 @@ require ( ) require ( + 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect + 4d63.com/gochecknoglobals v0.2.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/4meepo/tagalign v1.3.4 // indirect + github.com/Abirdcfly/dupword v0.1.1 // indirect + github.com/Antonboom/errname v0.1.13 // indirect + github.com/Antonboom/nilnil v0.1.9 // indirect + github.com/Antonboom/testifylint v1.4.3 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/Crocmagnon/fatcontext v0.5.2 // indirect + github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect + github.com/alecthomas/go-check-sumtype v0.1.4 // indirect + github.com/alexkohler/nakedret/v2 v2.0.4 // indirect + github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alingse/asasalint v0.0.11 // indirect + github.com/ashanbrown/forbidigo v1.6.0 // indirect + github.com/ashanbrown/makezero v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bkielbasa/cyclop v1.2.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/blizzy78/varnamelen v0.8.0 // indirect + github.com/bombsimon/wsl/v4 v4.4.1 // indirect + github.com/breml/bidichk v0.2.7 // indirect + github.com/breml/errchkjson v0.3.6 // indirect + github.com/butuzov/ireturn v0.3.0 // indirect + github.com/butuzov/mirror v1.2.0 // indirect + github.com/catenacyber/perfsprint v0.7.1 // indirect + github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charithe/durationcheck v0.0.10 // indirect + github.com/chavacava/garif v0.1.0 // indirect + github.com/ckaznocha/intrange v0.2.0 // indirect + github.com/curioswitch/go-reassign v0.2.0 // indirect + github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/dnephin/pflag v1.0.7 // indirect github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/ghostiam/protogetter v0.3.6 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-critic/go-critic v0.11.4 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-toolsmith/astcast v1.1.0 // indirect + github.com/go-toolsmith/astcopy v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.2.0 // indirect + github.com/go-toolsmith/astfmt v1.1.0 // indirect + github.com/go-toolsmith/astp v1.1.0 // indirect + github.com/go-toolsmith/strparse v1.1.0 // indirect + github.com/go-toolsmith/typep v1.1.0 // indirect + github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect + github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect + github.com/golangci/golangci-lint v1.61.0 // indirect + github.com/golangci/misspell v0.6.0 // indirect + github.com/golangci/modinfo v0.3.4 // indirect + github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/revgrep v0.5.3 // indirect + github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/gostaticanalysis/analysisutil v0.7.1 // indirect + github.com/gostaticanalysis/comment v1.4.2 // indirect + github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect + github.com/gostaticanalysis/nilerr v0.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jgautheron/goconst v1.7.1 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect + github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect + github.com/jjti/go-spancheck v0.6.2 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/julz/importas v0.1.0 // indirect + github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect + github.com/kisielk/errcheck v1.7.0 // indirect + github.com/kkHAIKE/contextcheck v1.1.5 // indirect + github.com/kulti/thelper v0.6.3 // indirect + github.com/kunwardeep/paralleltest v1.0.10 // indirect + github.com/kyoh86/exportloopref v0.1.11 // indirect + github.com/lasiar/canonicalheader v1.1.1 // indirect + github.com/ldez/gomoddirectives v0.2.4 // indirect + github.com/ldez/tagliatelle v0.5.0 // indirect + github.com/leonklingele/grouper v1.1.2 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/lufeee/execinquery v1.2.1 // indirect + github.com/macabu/inamedparam v0.1.3 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/maratori/testableexamples v1.0.0 // indirect + github.com/maratori/testpackage v1.1.1 // indirect + github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect github.com/mattermost/logr/v2 v2.0.21 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mgechev/revive v1.3.9 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moricho/tparallel v0.3.2 // indirect + github.com/nakabonne/nestif v0.3.1 // indirect + github.com/nishanths/exhaustive v0.12.0 // indirect + github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nunnatsa/ginkgolinter v0.16.2 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/polyfloyd/go-errorlint v1.6.0 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect + github.com/quasilyte/gogrep v0.5.0 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect + github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect + github.com/ryancurrah/gomodguard v1.3.5 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect + github.com/sashamelentyev/interfacebloat v1.1.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect + github.com/securego/gosec/v2 v2.21.2 // indirect + github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sivchari/containedctx v1.0.3 // indirect + github.com/sivchari/tenv v1.10.0 // indirect + github.com/sonatard/noctx v0.0.2 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect + github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/tdakkota/asciicheck v0.2.0 // indirect + github.com/tetafro/godot v1.4.17 // indirect + github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect + github.com/timonwong/loggercheck v0.9.4 // indirect github.com/tinylib/msgp v1.2.5 // indirect + github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect + github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect + github.com/ultraware/funlen v0.1.0 // indirect + github.com/ultraware/whitespace v0.1.1 // indirect + github.com/uudashr/gocognit v1.1.3 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect + github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/yagipy/maintidx v1.0.0 // indirect + github.com/yeya24/promlinter v0.3.0 // indirect + github.com/ykadowak/zerologlint v0.1.5 // indirect + gitlab.com/bosi/decorder v0.4.2 // indirect + go-simpler.org/musttag v0.12.2 // indirect + go-simpler.org/sloglint v0.7.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/automaxprocs v1.5.3 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect + golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect google.golang.org/grpc v1.70.0 // indirect google.golang.org/protobuf v1.36.4 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/gotestsum v1.7.0 // indirect + honnef.co/go/tools v0.5.1 // indirect mellium.im/reader v0.1.0 // indirect mellium.im/xmlstream v0.15.4 // indirect + mvdan.cc/gofumpt v0.7.0 // indirect + mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect +) + +tool ( + github.com/golangci/golangci-lint/cmd/golangci-lint + gotest.tools/gotestsum ) diff --git a/go.sum b/go.sum index 4e30588..9a27f5c 100644 --- a/go.sum +++ b/go.sum @@ -1,79 +1,345 @@ +4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= +4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= +4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= +4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= +github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= +github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY= +github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM= +github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= +github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= +github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= +github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= +github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck= +github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= +github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= +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/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= +github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= +github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= +github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= +github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= +github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= +github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= +github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= +github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= +github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= +github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= +github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= +github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= +github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= +github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= +github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= +github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/ckaznocha/intrange v0.2.0 h1:FykcZuJ8BD7oX93YbO1UY9oZtkRbp+1/kJcDjkefYLs= +github.com/ckaznocha/intrange v0.2.0/go.mod h1:r5I7nUlAAG56xmkOpw4XVr16BXhwYTUdcuRFeevn1oE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= +github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= +github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= +github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= +github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= +github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= +github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= +github.com/golangci/golangci-lint v1.61.0 h1:VvbOLaRVWmyxCnUIMTbf1kDsaJbTzH20FAMXTAlQGu8= +github.com/golangci/golangci-lint v1.61.0/go.mod h1:e4lztIrJJgLPhWvFPDkhiMwEFRrWlmFbrZea3FsJyN8= +github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= +github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= +github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= +github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs= +github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.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= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= +github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= +github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= +github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -85,25 +351,94 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= +github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= +github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= +github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= +github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= +github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= +github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= +github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= +github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= +github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= +github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= +github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= +github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= +github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= +github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= +github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= +github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= +github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= @@ -112,6 +447,7 @@ github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy5 github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= github.com/mattermost/mattermost/server/public v0.1.10 h1:gp3XHxqj5KDkz3venimqqNc62rqyF15uusQuBr8k7J4= github.com/mattermost/mattermost/server/public v0.1.10/go.mod h1:hu2sIyXm024PGIGhACqmCxvp3atrwRzXGgAzCvs6zJs= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -120,36 +456,129 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= +github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= +github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +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/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= +github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= +github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= +github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/securego/gosec/v2 v2.21.2 h1:deZp5zmYf3TWwU7A7cR2+SolbTpZ3HQiwFqnzQyEl3M= +github.com/securego/gosec/v2 v2.21.2/go.mod h1:au33kg78rNseF5PwPnTWhuYBFf534bvJRvOrgZ/bFzU= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -172,22 +601,83 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/tenv v1.10.0 h1:g/hzMA+dBCKqGXgW8AV/1xIWhAvDrx0zFKNR48NFMg0= +github.com/sivchari/tenv v1.10.0/go.mod h1:tdY24masnVoZFxYrHv/nD6Tc8FbkEtAQEEziXpyMgqY= +github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= +github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= +github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= +github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.4.17 h1:pGzu+Ye7ZUEFx7LHU0dAKmCOXWsPjl7qA6iMGndsjPs= +github.com/tetafro/godot v1.4.17/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= +github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= +github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= +github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= +github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= +github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= +github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= +github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -198,8 +688,35 @@ github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= +go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= +go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= +go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= +go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= +go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= +go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= @@ -210,19 +727,75 @@ go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiy go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -230,109 +803,356 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-20210603081109-ebe580a85c40/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I= google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.5/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/gotestsum v1.7.0 h1:RwpqwwFKBAa2h+F6pMEGpE707Edld0etUD3GhqqhDNc= +gotest.tools/gotestsum v1.7.0/go.mod h1:V1m4Jw3eBerhI/A6qCxUE07RnCg7ACkKj9BYcAm09V8= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww= mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI= mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= @@ -341,5 +1161,12 @@ mellium.im/xmlstream v0.15.4 h1:gLKxcWl4rLMUpKgtzrTBvr4OexPeO/edYus+uK3F6ZI= mellium.im/xmlstream v0.15.4/go.mod h1:yXaCW2++fmVO4L9piKVkyLDqnCmictVYF7FDQW8prb4= mellium.im/xmpp v0.22.0 h1:UthQVSwEAr7SNrmyc90c2ykGpVHxjn/3yw8Ey4+Im8s= mellium.im/xmpp v0.22.0/go.mod h1:WSjq12nhREFD88Vy/0WD6Q8inE8t6a8w7QjzwivWitw= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/server/xmpp/client.go b/server/xmpp/client.go index 4a49ac8..2ed8681 100644 --- a/server/xmpp/client.go +++ b/server/xmpp/client.go @@ -52,6 +52,21 @@ type SendMessageResponse struct { StanzaID string `json:"stanza_id"` } +// MessageBody represents the body element of an XMPP message +type MessageBody struct { + XMLName xml.Name `xml:"body"` + Text string `xml:",chardata"` +} + +// XMPPMessage represents a complete XMPP message stanza +type XMPPMessage struct { + XMLName xml.Name `xml:"jabber:client message"` + Type string `xml:"type,attr"` + To string `xml:"to,attr"` + From string `xml:"from,attr"` + Body MessageBody `xml:"body"` +} + // GhostUser represents an XMPP ghost user type GhostUser struct { JID string `json:"jid"` @@ -324,26 +339,12 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) { sendCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second) defer cancel() - // Create the message body structure - type messageBody struct { - XMLName xml.Name `xml:"body"` - Text string `xml:",chardata"` - } - // Create complete message with body - type message struct { - XMLName xml.Name `xml:"jabber:client message"` - Type string `xml:"type,attr"` - To string `xml:"to,attr"` - From string `xml:"from,attr"` - Body messageBody `xml:"body"` - } - - fullMsg := message{ + fullMsg := XMPPMessage{ Type: "groupchat", To: to.String(), From: c.jidAddr.String(), - Body: messageBody{Text: req.Message}, + Body: MessageBody{Text: req.Message}, } // Send the message using the session encoder @@ -359,6 +360,39 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) { return response, nil } +// SendDirectMessage sends a direct message to a specific user +func (c *Client) SendDirectMessage(userJID, message string) error { + if c.session == nil { + if err := c.Connect(); err != nil { + return err + } + } + + to, err := jid.Parse(userJID) + if err != nil { + return fmt.Errorf("failed to parse user JID: %w", err) + } + + // Create a context with timeout for the send operation + sendCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second) + defer cancel() + + // Create direct message using reusable structs + msg := XMPPMessage{ + Type: "chat", + To: to.String(), + From: c.jidAddr.String(), + Body: MessageBody{Text: message}, + } + + // Send the message using the session encoder + if err := c.session.Encode(sendCtx, msg); err != nil { + return fmt.Errorf("failed to send direct message: %w", err) + } + + return nil +} + // ResolveRoomAlias resolves a room alias to room JID func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) { // For XMPP, return the alias as-is if it's already a valid JID From 2e13d96dce44d9dd4bf7809eaed6927a04ef7edc Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Fri, 1 Aug 2025 18:18:10 +0200 Subject: [PATCH 03/10] feat: implement centralized channel mapping management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds OnChannelMappingDeleted method to BridgeManager for centralized cleanup of channel mappings across all bridge types. Updates slash commands to use centralized management and fixes method naming inconsistencies. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- plugin.json | 4 +- server/bridge/manager.go | 98 ++++++++++- server/bridge/mattermost/bridge.go | 257 +++++++++++++++++++++++++++++ server/bridge/xmpp/bridge.go | 33 +--- server/command/command.go | 16 +- server/model/bridge.go | 52 +++++- server/plugin.go | 63 ++++++- 7 files changed, 480 insertions(+), 43 deletions(-) create mode 100644 server/bridge/mattermost/bridge.go diff --git a/plugin.json b/plugin.json index fbf764e..d99213d 100644 --- a/plugin.json +++ b/plugin.json @@ -6,7 +6,7 @@ "support_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp/issues", "icon_path": "assets/logo.png", "version": "", - "min_server_version": "6.2.1", + "min_server_version": "9.5.0", "server": { "executables": { "darwin-amd64": "server/dist/plugin-darwin-amd64", @@ -82,4 +82,4 @@ "version": "v0.1.1" } } -} \ No newline at end of file +} diff --git a/server/bridge/manager.go b/server/bridge/manager.go index 588aa81..3c90d2c 100644 --- a/server/bridge/manager.go +++ b/server/bridge/manager.go @@ -213,4 +213,100 @@ func (m *Manager) OnPluginConfigurationChange(config any) error { m.logger.LogInfo("Configuration changes propagated to all bridges") return nil -} \ No newline at end of file +} + +// OnChannelMappingCreated handles the creation of a channel mapping by calling the appropriate bridge +func (m *Manager) OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID string) error { + // Input validation + if channelID == "" { + return fmt.Errorf("channelID cannot be empty") + } + if bridgeName == "" { + return fmt.Errorf("bridgeName cannot be empty") + } + if bridgeRoomID == "" { + return fmt.Errorf("bridgeRoomID cannot be empty") + } + + m.logger.LogDebug("Creating channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID) + + // Get the specific bridge + bridge, err := m.GetBridge(bridgeName) + if err != nil { + m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err) + return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err) + } + + // Check if bridge is connected + if !bridge.IsConnected() { + return fmt.Errorf("bridge '%s' is not connected", bridgeName) + } + + // Create the channel mapping on the receiving bridge + if err = bridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil { + m.logger.LogError("Failed to create channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err) + return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", bridgeName, err) + } + + mattermostBridge, err := m.GetBridge("mattermost") + if err != nil { + m.logger.LogError("Failed to get Mattermost bridge", "error", err) + return fmt.Errorf("failed to get Mattermost bridge: %w", err) + } + + // Create the channel mapping in the Mattermost bridge + if err = mattermostBridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil { + m.logger.LogError("Failed to create channel mapping in Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err) + return fmt.Errorf("failed to create channel mapping in Mattermost bridge: %w", err) + } + + m.logger.LogInfo("Successfully created channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID) + return nil +} + +// OnChannelMappingDeleted handles the deletion of a channel mapping by calling the appropriate bridges +func (m *Manager) OnChannelMappingDeleted(channelID, bridgeName string) error { + // Input validation + if channelID == "" { + return fmt.Errorf("channelID cannot be empty") + } + if bridgeName == "" { + return fmt.Errorf("bridgeName cannot be empty") + } + + m.logger.LogDebug("Deleting channel mapping", "channel_id", channelID, "bridge_name", bridgeName) + + // Get the specific bridge + bridge, err := m.GetBridge(bridgeName) + if err != nil { + m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err) + return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err) + } + + // Check if bridge is connected + if !bridge.IsConnected() { + return fmt.Errorf("bridge '%s' is not connected", bridgeName) + } + + // Delete the channel mapping from the specific bridge + if err = bridge.DeleteChannelMapping(channelID); err != nil { + m.logger.LogError("Failed to delete channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "error", err) + return fmt.Errorf("failed to delete channel mapping for bridge '%s': %w", bridgeName, err) + } + + // Also delete from Mattermost bridge to clean up reverse mappings + mattermostBridge, err := m.GetBridge("mattermost") + if err != nil { + m.logger.LogError("Failed to get Mattermost bridge", "error", err) + return fmt.Errorf("failed to get Mattermost bridge: %w", err) + } + + // Delete the channel mapping from the Mattermost bridge + if err = mattermostBridge.DeleteChannelMapping(channelID); err != nil { + m.logger.LogError("Failed to delete channel mapping from Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "error", err) + return fmt.Errorf("failed to delete channel mapping from Mattermost bridge: %w", err) + } + + m.logger.LogInfo("Successfully deleted channel mapping", "channel_id", channelID, "bridge_name", bridgeName) + return nil +} diff --git a/server/bridge/mattermost/bridge.go b/server/bridge/mattermost/bridge.go new file mode 100644 index 0000000..ccd69e6 --- /dev/null +++ b/server/bridge/mattermost/bridge.go @@ -0,0 +1,257 @@ +package mattermost + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" + pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// mattermostBridge handles syncing messages between Mattermost instances +type mattermostBridge struct { + logger logger.Logger + api plugin.API + kvstore kvstore.KVStore + + // Connection management + connected atomic.Bool + ctx context.Context + cancel context.CancelFunc + + // Current configuration + config *config.Configuration + configMu sync.RWMutex + + // Channel mappings cache + channelMappings map[string]string + mappingsMu sync.RWMutex +} + +// NewBridge creates a new Mattermost bridge +func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge { + ctx, cancel := context.WithCancel(context.Background()) + bridge := &mattermostBridge{ + logger: log, + api: api, + kvstore: kvstore, + ctx: ctx, + cancel: cancel, + channelMappings: make(map[string]string), + config: cfg, + } + + return bridge +} + +// UpdateConfiguration updates the bridge configuration +func (b *mattermostBridge) UpdateConfiguration(newConfig any) error { + cfg, ok := newConfig.(*config.Configuration) + if !ok { + return fmt.Errorf("invalid configuration type") + } + + b.configMu.Lock() + oldConfig := b.config + b.config = cfg + b.configMu.Unlock() + + // Log the configuration change + b.logger.LogInfo("Mattermost bridge configuration updated", "old_config", oldConfig, "new_config", cfg) + + return nil +} + +// Start initializes the bridge +func (b *mattermostBridge) Start() error { + b.logger.LogDebug("Starting Mattermost bridge") + + b.configMu.RLock() + config := b.config + b.configMu.RUnlock() + + if config == nil { + return fmt.Errorf("bridge configuration not set") + } + + // For Mattermost bridge, we're always "connected" since we're running within Mattermost + b.connected.Store(true) + + // Load existing channel mappings + if err := b.loadChannelMappings(); err != nil { + b.logger.LogWarn("Failed to load some channel mappings", "error", err) + } + + b.logger.LogInfo("Mattermost bridge started successfully") + return nil +} + +// Stop shuts down the bridge +func (b *mattermostBridge) Stop() error { + b.logger.LogInfo("Stopping Mattermost bridge") + + if b.cancel != nil { + b.cancel() + } + + b.connected.Store(false) + b.logger.LogInfo("Mattermost bridge stopped") + return nil +} + +// loadChannelMappings loads existing channel mappings from KV store +func (b *mattermostBridge) loadChannelMappings() error { + b.logger.LogDebug("Loading channel mappings for Mattermost bridge") + + // Get all channel mappings from KV store for Mattermost bridge + mappings, err := b.getAllChannelMappings() + if err != nil { + return fmt.Errorf("failed to load channel mappings: %w", err) + } + + if len(mappings) == 0 { + b.logger.LogInfo("No channel mappings found for Mattermost bridge") + return nil + } + + b.logger.LogInfo("Found channel mappings for Mattermost bridge", "count", len(mappings)) + + // Update local cache + b.mappingsMu.Lock() + for channelID, roomID := range mappings { + b.channelMappings[channelID] = roomID + } + b.mappingsMu.Unlock() + + return nil +} + +// getAllChannelMappings retrieves all channel mappings from KV store for Mattermost bridge +func (b *mattermostBridge) getAllChannelMappings() (map[string]string, error) { + if b.kvstore == nil { + return nil, fmt.Errorf("KV store not initialized") + } + + mappings := make(map[string]string) + + // Get all keys with the Mattermost bridge mapping prefix + mattermostPrefix := kvstore.KeyPrefixChannelMap + "mattermost_" + keys, err := b.kvstore.ListKeysWithPrefix(0, 1000, mattermostPrefix) + if err != nil { + return nil, fmt.Errorf("failed to list Mattermost bridge mapping keys: %w", err) + } + + // Load each mapping + for _, key := range keys { + channelIDBytes, err := b.kvstore.Get(key) + if err != nil { + b.logger.LogWarn("Failed to load mapping for key", "key", key, "error", err) + continue + } + + // Extract room ID from the key + roomID := kvstore.ExtractIdentifierFromChannelMapKey(key, "mattermost") + if roomID == "" { + b.logger.LogWarn("Failed to extract room ID from key", "key", key) + continue + } + + channelID := string(channelIDBytes) + mappings[channelID] = roomID + } + + return mappings, nil +} + +// IsConnected returns whether the bridge is connected +func (b *mattermostBridge) IsConnected() bool { + // Mattermost bridge is always "connected" since it runs within Mattermost + return b.connected.Load() +} + +// CreateChannelMapping creates a mapping between a Mattermost channel and another Mattermost room/channel +func (b *mattermostBridge) CreateChannelMapping(channelID, roomID string) error { + if b.kvstore == nil { + return fmt.Errorf("KV store not initialized") + } + + // Store forward and reverse mappings using bridge-agnostic keys + err := b.kvstore.Set(kvstore.BuildChannelMapKey("mattermost", channelID), []byte(roomID)) + if err != nil { + return fmt.Errorf("failed to store channel room mapping: %w", err) + } + + // Update local cache + b.mappingsMu.Lock() + b.channelMappings[channelID] = roomID + b.mappingsMu.Unlock() + + b.logger.LogInfo("Created Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID) + return nil +} + +// GetChannelMapping gets the room ID for a Mattermost channel +func (b *mattermostBridge) GetChannelMapping(channelID string) (string, error) { + // Check cache first + b.mappingsMu.RLock() + roomID, exists := b.channelMappings[channelID] + b.mappingsMu.RUnlock() + + if exists { + return roomID, nil + } + + if b.kvstore == nil { + return "", fmt.Errorf("KV store not initialized") + } + + // Check if we have a mapping in the KV store for this channel ID + roomIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", channelID)) + if err != nil { + return "", nil // Unmapped channels are expected + } + + roomID = string(roomIDBytes) + + // Update cache + b.mappingsMu.Lock() + b.channelMappings[channelID] = roomID + b.mappingsMu.Unlock() + + return roomID, nil +} + +// DeleteChannelMapping removes a mapping between a Mattermost channel and room +func (b *mattermostBridge) DeleteChannelMapping(channelID string) error { + if b.kvstore == nil { + return fmt.Errorf("KV store not initialized") + } + + // Get the room ID from the mapping before deleting + roomID, err := b.GetChannelMapping(channelID) + if err != nil { + return fmt.Errorf("failed to get channel mapping: %w", err) + } + if roomID == "" { + return fmt.Errorf("channel is not mapped to any room") + } + + // Delete forward and reverse mappings from KV store + err = b.kvstore.Delete(kvstore.BuildChannelMapKey("mattermost", channelID)) + if err != nil { + return fmt.Errorf("failed to delete channel room mapping: %w", err) + } + + // Remove from local cache + b.mappingsMu.Lock() + delete(b.channelMappings, channelID) + b.mappingsMu.Unlock() + + b.logger.LogInfo("Deleted Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID) + return nil +} diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index d180d80..a2697e3 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -142,9 +142,6 @@ func (b *xmppBridge) Start() error { return fmt.Errorf("bridge configuration not set") } - // Print the configuration for debugging - b.logger.LogDebug("Bridge configuration", "config", config) - if !config.EnableSync { b.logger.LogInfo("XMPP sync is disabled, bridge will not start") return nil @@ -378,19 +375,13 @@ func (b *xmppBridge) IsConnected() bool { return b.connected.Load() } -// CreateChannelRoomMapping creates a mapping between a Mattermost channel and XMPP room -func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error { +// CreateChannelMapping creates a mapping between a Mattermost channel and XMPP room +func (b *xmppBridge) CreateChannelMapping(channelID, roomJID string) error { if b.kvstore == nil { return fmt.Errorf("KV store not initialized") } - // Store forward and reverse mappings using bridge-agnostic keys - err := b.kvstore.Set(kvstore.BuildChannelMapKey("mattermost", channelID), []byte(roomJID)) - if err != nil { - return fmt.Errorf("failed to store channel room mapping: %w", err) - } - - err = b.kvstore.Set(kvstore.BuildChannelMapKey("xmpp", roomJID), []byte(channelID)) + err := b.kvstore.Set(kvstore.BuildChannelMapKey("xmpp", roomJID), []byte(channelID)) if err != nil { return fmt.Errorf("failed to store reverse room mapping: %w", err) } @@ -411,8 +402,8 @@ func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error { return nil } -// GetChannelRoomMapping gets the XMPP room JID for a Mattermost channel -func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) { +// GetChannelMapping gets the XMPP room JID for a Mattermost channel +func (b *xmppBridge) GetChannelMapping(channelID string) (string, error) { // Check cache first b.mappingsMu.RLock() roomJID, exists := b.channelMappings[channelID] @@ -427,7 +418,7 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) { } // Check if we have a mapping in the KV store for this channel ID - roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", channelID)) + roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("xmpp", channelID)) if err != nil { return "", nil // Unmapped channels are expected } @@ -442,14 +433,14 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) { return roomJID, nil } -// DeleteChannelRoomMapping removes a mapping between a Mattermost channel and XMPP room -func (b *xmppBridge) DeleteChannelRoomMapping(channelID string) error { +// DeleteChannelMapping removes a mapping between a Mattermost channel and XMPP room +func (b *xmppBridge) DeleteChannelMapping(channelID string) error { if b.kvstore == nil { return fmt.Errorf("KV store not initialized") } // Get the room JID from the mapping before deleting - roomJID, err := b.GetChannelRoomMapping(channelID) + roomJID, err := b.GetChannelMapping(channelID) if err != nil { return fmt.Errorf("failed to get channel mapping: %w", err) } @@ -457,12 +448,6 @@ func (b *xmppBridge) DeleteChannelRoomMapping(channelID string) error { return fmt.Errorf("channel is not mapped to any room") } - // Delete forward and reverse mappings from KV store - err = b.kvstore.Delete(kvstore.BuildChannelMapKey("mattermost", channelID)) - if err != nil { - return fmt.Errorf("failed to delete channel room mapping: %w", err) - } - err = b.kvstore.Delete(kvstore.BuildChannelMapKey("xmpp", roomJID)) if err != nil { return fmt.Errorf("failed to delete reverse room mapping: %w", err) diff --git a/server/command/command.go b/server/command/command.go index 77d78b7..5c40f20 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -118,7 +118,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } } - // Get the XMPP bridge + // Get the XMPP bridge to check existing mappings bridge, err := c.bridgeManager.GetBridge("xmpp") if err != nil { return &model.CommandResponse{ @@ -136,7 +136,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } // Check if channel is already mapped - existingMapping, err := bridge.GetChannelRoomMapping(channelID) + existingMapping, err := bridge.GetChannelMapping(channelID) if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -151,8 +151,8 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } } - // Create the mapping - err = bridge.CreateChannelRoomMapping(channelID, roomJID) + // Create the mapping using BridgeManager + err = c.bridgeManager.OnChannelMappingCreated(channelID, "xmpp", roomJID) if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -169,7 +169,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandResponse { channelID := args.ChannelId - // Get the XMPP bridge + // Get the XMPP bridge to check existing mappings bridge, err := c.bridgeManager.GetBridge("xmpp") if err != nil { return &model.CommandResponse{ @@ -179,7 +179,7 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes } // Check if channel is mapped - roomJID, err := bridge.GetChannelRoomMapping(channelID) + roomJID, err := bridge.GetChannelMapping(channelID) if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -195,7 +195,7 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes } // Delete the mapping - err = bridge.DeleteChannelRoomMapping(channelID) + err = c.bridgeManager.OnChannelMappingDeleted(channelID, "xmpp") if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -230,7 +230,7 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe // Check if current channel is mapped channelID := args.ChannelId - roomJID, err := bridge.GetChannelRoomMapping(channelID) + roomJID, err := bridge.GetChannelMapping(channelID) var mappingText string if err != nil { diff --git a/server/model/bridge.go b/server/model/bridge.go index 2a01ac6..b7f5e6a 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -1,5 +1,16 @@ package model +type BridgeID string + +type UserState int + +const ( + UserStateOnline UserState = iota + UserStateAway + UserStateBusy + UserStateOffline +) + type BridgeManager interface { // RegisterBridge registers a bridge with the given name. Returns an error if the name is empty, // the bridge is nil, or a bridge with the same name is already registered. @@ -39,6 +50,12 @@ type BridgeManager interface { // Returns an error if any bridge fails to update its configuration, but continues to // attempt updating all bridges. OnPluginConfigurationChange(config any) error + + // OnChannelMappingCreated is called when a channel mapping is created. + OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID string) error + + // OnChannelMappingDeleted is called when a channel mapping is deleted. + OnChannelMappingDeleted(channelID, bridgeName string) error } type Bridge interface { @@ -51,15 +68,38 @@ type Bridge interface { // Stop stops the bridge Stop() error - // CreateChannelRoomMapping creates a mapping between a Mattermost channel ID and an bridge room ID. - CreateChannelRoomMapping(channelID, roomJID string) error + // CreateChannelMapping creates a mapping between a Mattermost channel ID and an bridge room ID. + CreateChannelMapping(channelID, roomJID string) error - // GetChannelRoomMapping retrieves the bridge room ID for a given Mattermost channel ID. - GetChannelRoomMapping(channelID string) (string, error) + // GetChannelMapping retrieves the bridge room ID for a given Mattermost channel ID. + GetChannelMapping(channelID string) (string, error) - // DeleteChannelRoomMapping removes a mapping between a Mattermost channel ID and a bridge room ID. - DeleteChannelRoomMapping(channelID string) error + // DeleteChannelMapping removes a mapping between a Mattermost channel ID and a bridge room ID. + DeleteChannelMapping(channelID string) error // IsConnected checks if the bridge is connected to the remote service. IsConnected() bool } + +type BridgeUserManager interface { + // CreateUser creates a new user in the bridge system. + CreateUser(userID string, userData any) error + + // GetUser retrieves user data for a given user ID. + GetUser(userID string) (any, error) + + // UpdateUser updates user data for a given user ID. + UpdateUser(userID string, userData any) error + + // DeleteUser removes a user from the bridge system. + DeleteUser(userID string) error + + // ListUsers returns a list of all users in the bridge system. + ListUsers() ([]string, error) + + // HasUser checks if a user exists in the bridge system. + HasUser(userID string) bool + + // OnUserStateChange is called when a user's state changes (e.g., online, away, offline). + OnUserStateChange(userID string, state UserState) error +} diff --git a/server/plugin.go b/server/plugin.go index 216dab4..ded24bb 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -7,6 +7,7 @@ import ( "time" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge" + mattermostbridge "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge/mattermost" xmppbridge "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge/xmpp" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" @@ -43,6 +44,10 @@ type Plugin struct { // remoteID is the identifier returned by RegisterPluginForSharedChannels remoteID string + // botUserID is the ID of the bot user created for this plugin + botUserID string + + // backgroundJob is the scheduled job that runs periodically to perform background tasks. backgroundJob *cluster.Job // configurationLock synchronizes access to the configuration. @@ -71,6 +76,12 @@ func (p *Plugin) OnActivate() error { cfg := p.getConfiguration() p.logger.LogDebug("Loaded configuration in OnActivate", "config", cfg) + // Register the plugin for shared channels + if err := p.registerForSharedChannels(); err != nil { + p.logger.LogError("Failed to register for shared channels", "error", err) + return fmt.Errorf("failed to register for shared channels: %w", err) + } + // Initialize bridge manager p.bridgeManager = bridge.NewManager(p.logger) @@ -118,6 +129,10 @@ func (p *Plugin) OnDeactivate() error { } } + if err := p.API.UnregisterPluginForSharedChannels(manifest.Id); err != nil { + p.API.LogError("Failed to unregister plugin for shared channels", "err", err) + } + return nil } @@ -143,19 +158,63 @@ func (p *Plugin) initXMPPClient() { func (p *Plugin) initBridges(cfg config.Configuration) error { // Create and register XMPP bridge - bridge := xmppbridge.NewBridge( + xmppBridge := xmppbridge.NewBridge( p.logger, p.API, p.kvstore, &cfg, ) - if err := p.bridgeManager.RegisterBridge("xmpp", bridge); err != nil { + if err := p.bridgeManager.RegisterBridge("xmpp", xmppBridge); err != nil { return fmt.Errorf("failed to register XMPP bridge: %w", err) } + // Create and register Mattermost bridge + mattermostBridge := mattermostbridge.NewBridge( + p.logger, + p.API, + p.kvstore, + &cfg, + ) + + if err := p.bridgeManager.RegisterBridge("mattermost", mattermostBridge); err != nil { + return fmt.Errorf("failed to register Mattermost bridge: %w", err) + } + p.logger.LogInfo("Bridge instances created and registered successfully") return nil } +func (p *Plugin) registerForSharedChannels() error { + botUserID, err := p.API.EnsureBotUser(&model.Bot{ + Username: "mattermost-bridge", + DisplayName: "Mattermost Bridge", + Description: "Mattermost Bridge Bot", + }) + if err != nil { + return fmt.Errorf("failed to ensure bot user: %w", err) + } + + p.botUserID = botUserID + + opts := model.RegisterPluginOpts{ + Displayname: "XMPP-Bridge", + PluginID: manifest.Id, + CreatorID: botUserID, + AutoShareDMs: false, + AutoInvited: false, + } + + remoteID, appErr := p.API.RegisterPluginForSharedChannels(opts) + if appErr != nil { + return fmt.Errorf("failed to register plugin for shared channels: %w", appErr) + } + + // Store the remote ID for use in sync operations + p.remoteID = remoteID + + p.logger.LogInfo("Successfully registered plugin for shared channels", "remote_id", remoteID) + return nil +} + // See https://developers.mattermost.com/extend/plugins/server/reference/ From a5eb80817c85f35ddc3b7aa7cbf6861a3f24554b Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Fri, 1 Aug 2025 18:30:27 +0200 Subject: [PATCH 04/10] fix: correct plugin ID in manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates plugin ID to match proper naming convention from com.mattermost.bridge-xmpp to com.mattermost.plugin-bridge-xmpp. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index d99213d..a7f0e11 100644 --- a/plugin.json +++ b/plugin.json @@ -1,5 +1,5 @@ { - "id": "com.mattermost.bridge-xmpp", + "id": "com.mattermost.plugin-bridge-xmpp", "name": "Mattermost Bridge for XMPP", "description": "This plugin provides a bridge connecting Mattermost and XMPP servers.", "homepage_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp", From 1f45197aa85fda47fe0555bb04908fe232898d1c Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Fri, 1 Aug 2025 19:10:40 +0200 Subject: [PATCH 05/10] feat: refactor channel mapping with structured parameters and shared channel integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ChannelMappingRequest and ChannelMappingDeleteRequest structs with validation - Update BridgeManager interface to accept structured parameters instead of individual strings - Implement proper user ID and team ID propagation to shared channels - Add shared channel creation/deletion integration with Mattermost API - Update command handlers to provide user and team context - Enhance logging with comprehensive parameter tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- server/bridge/manager.go | 151 ++++++++++++++++++++++++++------------ server/command/command.go | 19 ++++- server/model/bridge.go | 60 ++++++++++++++- server/model/strings.go | 40 ++++++++++ server/plugin.go | 4 +- 5 files changed, 223 insertions(+), 51 deletions(-) create mode 100644 server/model/strings.go diff --git a/server/bridge/manager.go b/server/bridge/manager.go index 3c90d2c..c873377 100644 --- a/server/bridge/manager.go +++ b/server/bridge/manager.go @@ -6,24 +6,33 @@ import ( "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" + mmModel "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" ) // Manager manages multiple bridge instances type Manager struct { - bridges map[string]model.Bridge - mu sync.RWMutex - logger logger.Logger + bridges map[string]model.Bridge + mu sync.RWMutex + logger logger.Logger + api plugin.API + remoteID string } // NewManager creates a new bridge manager -func NewManager(logger logger.Logger) model.BridgeManager { +func NewManager(logger logger.Logger, api plugin.API, remoteID string) model.BridgeManager { if logger == nil { panic("logger cannot be nil") } + if api == nil { + panic("plugin API cannot be nil") + } return &Manager{ - bridges: make(map[string]model.Bridge), - logger: logger, + bridges: make(map[string]model.Bridge), + logger: logger, + api: api, + remoteID: remoteID, } } @@ -216,36 +225,30 @@ func (m *Manager) OnPluginConfigurationChange(config any) error { } // OnChannelMappingCreated handles the creation of a channel mapping by calling the appropriate bridge -func (m *Manager) OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID string) error { - // Input validation - if channelID == "" { - return fmt.Errorf("channelID cannot be empty") - } - if bridgeName == "" { - return fmt.Errorf("bridgeName cannot be empty") - } - if bridgeRoomID == "" { - return fmt.Errorf("bridgeRoomID cannot be empty") +func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error { + // Validate request + if err := req.Validate(); err != nil { + return fmt.Errorf("invalid mapping request: %w", err) } - m.logger.LogDebug("Creating channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID) + m.logger.LogDebug("Creating channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "user_id", req.UserID, "team_id", req.TeamID) // Get the specific bridge - bridge, err := m.GetBridge(bridgeName) + bridge, err := m.GetBridge(req.BridgeName) if err != nil { - m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err) - return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err) + m.logger.LogError("Failed to get bridge", "bridge_name", req.BridgeName, "error", err) + return fmt.Errorf("failed to get bridge '%s': %w", req.BridgeName, err) } // Check if bridge is connected if !bridge.IsConnected() { - return fmt.Errorf("bridge '%s' is not connected", bridgeName) + return fmt.Errorf("bridge '%s' is not connected", req.BridgeName) } // Create the channel mapping on the receiving bridge - if err = bridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil { - m.logger.LogError("Failed to create channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err) - return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", bridgeName, err) + if err = bridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil { + m.logger.LogError("Failed to create channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err) + return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", req.BridgeName, err) } mattermostBridge, err := m.GetBridge("mattermost") @@ -255,43 +258,47 @@ func (m *Manager) OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID st } // Create the channel mapping in the Mattermost bridge - if err = mattermostBridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil { - m.logger.LogError("Failed to create channel mapping in Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err) + if err = mattermostBridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil { + m.logger.LogError("Failed to create channel mapping in Mattermost bridge", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err) return fmt.Errorf("failed to create channel mapping in Mattermost bridge: %w", err) } - m.logger.LogInfo("Successfully created channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID) + // Share the channel using Mattermost's shared channels API + if err = m.shareChannel(req); err != nil { + m.logger.LogError("Failed to share channel", "channel_id", req.ChannelID, "bridge_room_id", req.BridgeRoomID, "error", err) + // Don't fail the entire operation if sharing fails, but log the error + m.logger.LogWarn("Channel mapping created but sharing failed - channel may not sync properly") + } + + m.logger.LogInfo("Successfully created channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID) return nil } // OnChannelMappingDeleted handles the deletion of a channel mapping by calling the appropriate bridges -func (m *Manager) OnChannelMappingDeleted(channelID, bridgeName string) error { - // Input validation - if channelID == "" { - return fmt.Errorf("channelID cannot be empty") - } - if bridgeName == "" { - return fmt.Errorf("bridgeName cannot be empty") +func (m *Manager) OnChannelMappingDeleted(req model.ChannelMappingDeleteRequest) error { + // Validate request + if err := req.Validate(); err != nil { + return fmt.Errorf("invalid delete request: %w", err) } - m.logger.LogDebug("Deleting channel mapping", "channel_id", channelID, "bridge_name", bridgeName) + m.logger.LogDebug("Deleting channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "user_id", req.UserID, "team_id", req.TeamID) // Get the specific bridge - bridge, err := m.GetBridge(bridgeName) + bridge, err := m.GetBridge(req.BridgeName) if err != nil { - m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err) - return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err) + m.logger.LogError("Failed to get bridge", "bridge_name", req.BridgeName, "error", err) + return fmt.Errorf("failed to get bridge '%s': %w", req.BridgeName, err) } // Check if bridge is connected if !bridge.IsConnected() { - return fmt.Errorf("bridge '%s' is not connected", bridgeName) + return fmt.Errorf("bridge '%s' is not connected", req.BridgeName) } // Delete the channel mapping from the specific bridge - if err = bridge.DeleteChannelMapping(channelID); err != nil { - m.logger.LogError("Failed to delete channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "error", err) - return fmt.Errorf("failed to delete channel mapping for bridge '%s': %w", bridgeName, err) + if err = bridge.DeleteChannelMapping(req.ChannelID); err != nil { + m.logger.LogError("Failed to delete channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "error", err) + return fmt.Errorf("failed to delete channel mapping for bridge '%s': %w", req.BridgeName, err) } // Also delete from Mattermost bridge to clean up reverse mappings @@ -302,11 +309,65 @@ func (m *Manager) OnChannelMappingDeleted(channelID, bridgeName string) error { } // Delete the channel mapping from the Mattermost bridge - if err = mattermostBridge.DeleteChannelMapping(channelID); err != nil { - m.logger.LogError("Failed to delete channel mapping from Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "error", err) + if err = mattermostBridge.DeleteChannelMapping(req.ChannelID); err != nil { + m.logger.LogError("Failed to delete channel mapping from Mattermost bridge", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "error", err) return fmt.Errorf("failed to delete channel mapping from Mattermost bridge: %w", err) } - m.logger.LogInfo("Successfully deleted channel mapping", "channel_id", channelID, "bridge_name", bridgeName) + // Unshare the channel using Mattermost's shared channels API + if err = m.unshareChannel(req.ChannelID); err != nil { + m.logger.LogError("Failed to unshare channel", "channel_id", req.ChannelID, "error", err) + // Don't fail the entire operation if unsharing fails, but log the error + m.logger.LogWarn("Channel mapping deleted but unsharing failed - channel may still appear as shared") + } + + m.logger.LogInfo("Successfully deleted channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName) + return nil +} + +// shareChannel creates a shared channel configuration using the Mattermost API +func (m *Manager) shareChannel(req model.ChannelMappingRequest) error { + if m.remoteID == "" { + return fmt.Errorf("remote ID not set - plugin not registered for shared channels") + } + + // Create SharedChannel configuration + sharedChannel := &mmModel.SharedChannel{ + ChannelId: req.ChannelID, + TeamId: req.TeamID, + Home: true, + ReadOnly: false, + ShareName: model.SanitizeShareName(fmt.Sprintf("bridge-%s", req.BridgeRoomID)), + ShareDisplayName: fmt.Sprintf("Bridge: %s", req.BridgeRoomID), + SharePurpose: fmt.Sprintf("Shared channel bridged to %s", req.BridgeRoomID), + ShareHeader: "test header", + CreatorId: req.UserID, + RemoteId: m.remoteID, + } + + // Share the channel + sharedChannel, err := m.api.ShareChannel(sharedChannel) + if err != nil { + return fmt.Errorf("failed to share channel via API: %w", err) + } + + m.logger.LogInfo("Successfully shared channel", "channel_id", req.ChannelID, "shared_channel_id", sharedChannel.ChannelId) + return nil +} + +// unshareChannel removes shared channel configuration using the Mattermost API +func (m *Manager) unshareChannel(channelID string) error { + // Unshare the channel + unshared, err := m.api.UnshareChannel(channelID) + if err != nil { + return fmt.Errorf("failed to unshare channel via API: %w", err) + } + + if !unshared { + m.logger.LogWarn("Channel was not shared or already unshared", "channel_id", channelID) + } else { + m.logger.LogInfo("Successfully unshared channel", "channel_id", channelID) + } + return nil } diff --git a/server/command/command.go b/server/command/command.go index 5c40f20..931287e 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -152,7 +152,15 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } // Create the mapping using BridgeManager - err = c.bridgeManager.OnChannelMappingCreated(channelID, "xmpp", roomJID) + mappingReq := pluginModel.ChannelMappingRequest{ + ChannelID: channelID, + BridgeName: "xmpp", + BridgeRoomID: roomJID, + UserID: args.UserId, + TeamID: args.TeamId, + } + + err = c.bridgeManager.OnChannelMappingCreated(mappingReq) if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -195,7 +203,14 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes } // Delete the mapping - err = c.bridgeManager.OnChannelMappingDeleted(channelID, "xmpp") + deleteReq := pluginModel.ChannelMappingDeleteRequest{ + ChannelID: channelID, + BridgeName: "xmpp", + UserID: args.UserId, + TeamID: args.TeamId, + } + + err = c.bridgeManager.OnChannelMappingDeleted(deleteReq) if err != nil { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, diff --git a/server/model/bridge.go b/server/model/bridge.go index b7f5e6a..799188f 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -1,5 +1,7 @@ package model +import "fmt" + type BridgeID string type UserState int @@ -11,6 +13,60 @@ const ( UserStateOffline ) +// ChannelMappingRequest contains information needed to create a channel mapping +type ChannelMappingRequest struct { + ChannelID string // Mattermost channel ID + BridgeName string // Name of the bridge (e.g., "xmpp") + BridgeRoomID string // Remote room/channel ID (e.g., JID for XMPP) + UserID string // ID of user who triggered the mapping creation + TeamID string // Team ID where the channel belongs +} + +// Validate checks if all required fields are present and valid +func (r ChannelMappingRequest) Validate() error { + if r.ChannelID == "" { + return fmt.Errorf("channelID cannot be empty") + } + if r.BridgeName == "" { + return fmt.Errorf("bridgeName cannot be empty") + } + if r.BridgeRoomID == "" { + return fmt.Errorf("bridgeRoomID cannot be empty") + } + if r.UserID == "" { + return fmt.Errorf("userID cannot be empty") + } + if r.TeamID == "" { + return fmt.Errorf("teamID cannot be empty") + } + return nil +} + +// ChannelMappingDeleteRequest contains information needed to delete a channel mapping +type ChannelMappingDeleteRequest struct { + ChannelID string // Mattermost channel ID + BridgeName string // Name of the bridge (e.g., "xmpp") + UserID string // ID of user who triggered the mapping deletion + TeamID string // Team ID where the channel belongs +} + +// Validate checks if all required fields are present and valid +func (r ChannelMappingDeleteRequest) Validate() error { + if r.ChannelID == "" { + return fmt.Errorf("channelID cannot be empty") + } + if r.BridgeName == "" { + return fmt.Errorf("bridgeName cannot be empty") + } + if r.UserID == "" { + return fmt.Errorf("userID cannot be empty") + } + if r.TeamID == "" { + return fmt.Errorf("teamID cannot be empty") + } + return nil +} + type BridgeManager interface { // RegisterBridge registers a bridge with the given name. Returns an error if the name is empty, // the bridge is nil, or a bridge with the same name is already registered. @@ -52,10 +108,10 @@ type BridgeManager interface { OnPluginConfigurationChange(config any) error // OnChannelMappingCreated is called when a channel mapping is created. - OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID string) error + OnChannelMappingCreated(req ChannelMappingRequest) error // OnChannelMappingDeleted is called when a channel mapping is deleted. - OnChannelMappingDeleted(channelID, bridgeName string) error + OnChannelMappingDeleted(req ChannelMappingDeleteRequest) error } type Bridge interface { diff --git a/server/model/strings.go b/server/model/strings.go new file mode 100644 index 0000000..1e559d9 --- /dev/null +++ b/server/model/strings.go @@ -0,0 +1,40 @@ +package model + +import "strings" + +// sanitizeShareName creates a valid ShareName matching the regex: ^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]*$ +func SanitizeShareName(name string) string { + // Convert to lowercase and replace spaces with hyphens + shareName := strings.ToLower(name) + shareName = strings.ReplaceAll(shareName, " ", "-") + + // Remove any characters that aren't lowercase letters, numbers, hyphens, or underscores + var validShareName strings.Builder + for _, r := range shareName { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { + validShareName.WriteRune(r) + } + } + + result := validShareName.String() + if result == "" { + return "matrixbridge" // fallback if no valid characters + } + + // Ensure it starts with alphanumeric + for len(result) > 0 && (result[0] == '-' || result[0] == '_') { + result = result[1:] + } + + // Ensure it ends with alphanumeric + for len(result) > 0 && (result[len(result)-1] == '-' || result[len(result)-1] == '_') { + result = result[:len(result)-1] + } + + // Final fallback check + if result == "" { + return "matrixbridge" + } + + return result +} diff --git a/server/plugin.go b/server/plugin.go index ded24bb..760d200 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -83,7 +83,7 @@ func (p *Plugin) OnActivate() error { } // Initialize bridge manager - p.bridgeManager = bridge.NewManager(p.logger) + p.bridgeManager = bridge.NewManager(p.logger, p.API, p.remoteID) // Initialize and register bridges with current configuration if err := p.initBridges(*cfg); err != nil { @@ -202,7 +202,7 @@ func (p *Plugin) registerForSharedChannels() error { PluginID: manifest.Id, CreatorID: botUserID, AutoShareDMs: false, - AutoInvited: false, + AutoInvited: true, } remoteID, appErr := p.API.RegisterPluginForSharedChannels(opts) From a95ca8fb76ff0a6db847157663ddeb96e2de1a34 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 4 Aug 2025 11:29:35 +0200 Subject: [PATCH 06/10] feat: implement comprehensive room validation and admin-only command access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RoomExists and GetRoomMapping methods to Bridge interface - Implement XMPP room existence checking using disco#info queries (XEP-0030) - Add room validation in BridgeManager to prevent duplicate mappings and invalid rooms - Enhance XMPP client with CheckRoomExists method and comprehensive logging - Implement admin-only access control for all bridge commands - Add user-friendly error messages with actionable troubleshooting steps - Update doctor command with room existence testing and pre-join validation - Add SimpleLogger implementation for standalone command usage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/xmpp-client-doctor/main.go | 135 +++++++++++++++++++++++++++-- server/bridge/manager.go | 33 +++++++ server/bridge/mattermost/bridge.go | 50 +++++++++++ server/bridge/xmpp/bridge.go | 46 ++++++++++ server/command/command.go | 100 +++++++++++++++++++-- server/model/bridge.go | 6 ++ server/plugin.go | 1 + server/xmpp/client.go | 100 ++++++++++++++++++++- 8 files changed, 454 insertions(+), 17 deletions(-) diff --git a/cmd/xmpp-client-doctor/main.go b/cmd/xmpp-client-doctor/main.go index f1ccaf0..0de41fd 100644 --- a/cmd/xmpp-client-doctor/main.go +++ b/cmd/xmpp-client-doctor/main.go @@ -28,6 +28,7 @@ type Config struct { TestRoom string TestMUC bool TestDirectMessage bool + TestRoomExists bool Verbose bool InsecureSkipVerify bool } @@ -43,14 +44,15 @@ func main() { flag.StringVar(&config.TestRoom, "test-room", defaultTestRoom, "MUC room JID for testing") flag.BoolVar(&config.TestMUC, "test-muc", true, "Enable MUC room testing (join/wait/leave)") flag.BoolVar(&config.TestDirectMessage, "test-dm", true, "Enable direct message testing (send message to admin user)") + flag.BoolVar(&config.TestRoomExists, "test-room-exists", true, "Enable room existence testing using disco#info") flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging") flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity and MUC operations\n\n") fmt.Fprintf(os.Stderr, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n") - fmt.Fprintf(os.Stderr, "performing connection tests, optionally testing MUC room operations and direct messages,\n") - fmt.Fprintf(os.Stderr, "and then disconnecting gracefully.\n\n") + fmt.Fprintf(os.Stderr, "performing connection tests, room existence checks, optionally testing MUC room operations\n") + fmt.Fprintf(os.Stderr, "and direct messages, and then disconnecting gracefully.\n\n") fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Examples:\n") @@ -81,6 +83,9 @@ func main() { if config.TestDirectMessage { log.Printf(" Test Direct Messages: enabled") } + if config.TestRoomExists { + log.Printf(" Test Room Existence: enabled") + } } // Test the XMPP client @@ -98,6 +103,9 @@ func main() { if config.TestDirectMessage { fmt.Println("✅ XMPP direct message test passed!") } + if config.TestRoomExists { + fmt.Println("✅ XMPP room existence test passed!") + } } } @@ -106,6 +114,9 @@ func testXMPPClient(config *Config) error { log.Printf("Creating XMPP client...") } + // Create a simple logger for the XMPP client + doctorLogger := &SimpleLogger{verbose: config.Verbose} + // Create XMPP client with optional TLS configuration var client *xmpp.Client if config.InsecureSkipVerify { @@ -122,6 +133,7 @@ func testXMPPClient(config *Config) error { config.Resource, "doctor-remote-id", tlsConfig, + doctorLogger, ) } else { client = xmpp.NewClient( @@ -130,6 +142,7 @@ func testXMPPClient(config *Config) error { config.Password, config.Resource, "doctor-remote-id", + doctorLogger, ) } @@ -164,6 +177,7 @@ func testXMPPClient(config *Config) error { var mucDuration time.Duration var dmDuration time.Duration + var roomExistsDuration time.Duration // Test MUC operations if requested if config.TestMUC { @@ -185,6 +199,16 @@ func testXMPPClient(config *Config) error { dmDuration = time.Since(start) } + // Test room existence if requested + if config.TestRoomExists { + start = time.Now() + err = testRoomExists(client, config) + if err != nil { + return fmt.Errorf("room existence test failed: %w", err) + } + roomExistsDuration = time.Since(start) + } + if config.Verbose { log.Printf("Disconnecting from XMPP server...") } @@ -208,6 +232,9 @@ func testXMPPClient(config *Config) error { if config.TestDirectMessage { log.Printf(" Direct message time: %v", dmDuration) } + if config.TestRoomExists { + log.Printf(" Room existence check time: %v", roomExistsDuration) + } log.Printf(" Disconnect time: %v", disconnectDuration) totalTime := connectDuration + pingDuration + disconnectDuration if config.TestMUC { @@ -216,6 +243,9 @@ func testXMPPClient(config *Config) error { if config.TestDirectMessage { totalTime += dmDuration } + if config.TestRoomExists { + totalTime += roomExistsDuration + } log.Printf(" Total time: %v", totalTime) } @@ -225,12 +255,33 @@ func testXMPPClient(config *Config) error { func testMUCOperations(client *xmpp.Client, config *Config) error { if config.Verbose { log.Printf("Testing MUC operations with room: %s", config.TestRoom) - log.Printf("Attempting to join MUC room...") + log.Printf("First checking if room exists...") + } + + // Check if room exists before attempting to join + start := time.Now() + exists, err := client.CheckRoomExists(config.TestRoom) + if err != nil { + return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err) + } + checkDuration := time.Since(start) + + if config.Verbose { + log.Printf("✅ Room existence check completed in %v", checkDuration) + log.Printf("Room %s exists: %t", config.TestRoom, exists) + } + + if !exists { + return fmt.Errorf("cannot test MUC operations: room %s does not exist or is not accessible", config.TestRoom) + } + + if config.Verbose { + log.Printf("Room exists, proceeding to join...") } // Test joining the room - start := time.Now() - err := client.JoinRoom(config.TestRoom) + start = time.Now() + err = client.JoinRoom(config.TestRoom) if err != nil { return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err) } @@ -281,11 +332,12 @@ func testMUCOperations(client *xmpp.Client, config *Config) error { if config.Verbose { log.Printf("✅ Successfully left MUC room in %v", leaveDuration) log.Printf("MUC operations summary:") + log.Printf(" Room existence check time: %v", checkDuration) log.Printf(" Join time: %v", joinDuration) log.Printf(" Send message time: %v", sendDuration) log.Printf(" Wait time: 5s") log.Printf(" Leave time: %v", leaveDuration) - log.Printf(" Total MUC time: %v", joinDuration+sendDuration+5*time.Second+leaveDuration) + log.Printf(" Total MUC time: %v", checkDuration+joinDuration+sendDuration+5*time.Second+leaveDuration) } return nil @@ -319,9 +371,80 @@ func testDirectMessage(client *xmpp.Client, config *Config) error { return nil } +func testRoomExists(client *xmpp.Client, config *Config) error { + if config.Verbose { + log.Printf("Testing room existence functionality...") + log.Printf("Checking if test room exists: %s", config.TestRoom) + } + + // Test room existence check + start := time.Now() + exists, err := client.CheckRoomExists(config.TestRoom) + if err != nil { + return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err) + } + checkDuration := time.Since(start) + + if config.Verbose { + log.Printf("✅ Room existence check completed in %v", checkDuration) + log.Printf("Room %s exists: %t", config.TestRoom, exists) + } + + // Test with a non-existent room to verify negative case + nonExistentRoom := "nonexistent-room-12345@conference.localhost" + if config.Verbose { + log.Printf("Testing negative case with non-existent room: %s", nonExistentRoom) + } + + start = time.Now() + existsNegative, err := client.CheckRoomExists(nonExistentRoom) + if err != nil { + return fmt.Errorf("failed to check non-existent room %s: %w", nonExistentRoom, err) + } + checkNegativeDuration := time.Since(start) + + if config.Verbose { + log.Printf("✅ Negative room existence check completed in %v", checkNegativeDuration) + log.Printf("Non-existent room %s exists: %t (should be false)", nonExistentRoom, existsNegative) + log.Printf("Room existence test summary:") + log.Printf(" Test room check time: %v", checkDuration) + log.Printf(" Negative case check time: %v", checkNegativeDuration) + log.Printf(" Total room existence test time: %v", checkDuration+checkNegativeDuration) + } + + return nil +} + func maskPassword(password string) string { if len(password) <= 2 { return "****" } return password[:2] + "****" +} + +// SimpleLogger provides basic logging functionality for the doctor command +type SimpleLogger struct { + verbose bool +} + +// LogDebug logs debug messages if verbose mode is enabled +func (l *SimpleLogger) LogDebug(msg string, args ...interface{}) { + if l.verbose { + log.Printf("[DEBUG] "+msg, args...) + } +} + +// LogInfo logs info messages +func (l *SimpleLogger) LogInfo(msg string, args ...interface{}) { + log.Printf("[INFO] "+msg, args...) +} + +// LogWarn logs warning messages +func (l *SimpleLogger) LogWarn(msg string, args ...interface{}) { + log.Printf("[WARN] "+msg, args...) +} + +// LogError logs error messages +func (l *SimpleLogger) LogError(msg string, args ...interface{}) { + log.Printf("[ERROR] "+msg, args...) } \ No newline at end of file diff --git a/server/bridge/manager.go b/server/bridge/manager.go index c873377..6e17483 100644 --- a/server/bridge/manager.go +++ b/server/bridge/manager.go @@ -245,6 +245,39 @@ func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error return fmt.Errorf("bridge '%s' is not connected", req.BridgeName) } + // NEW: Check if room already mapped to another channel + existingChannelID, err := bridge.GetRoomMapping(req.BridgeRoomID) + if err != nil { + m.logger.LogError("Failed to check room mapping", "bridge_room_id", req.BridgeRoomID, "error", err) + return fmt.Errorf("failed to check room mapping: %w", err) + } + if existingChannelID != "" { + m.logger.LogWarn("Room already mapped to another channel", + "bridge_room_id", req.BridgeRoomID, + "existing_channel_id", existingChannelID, + "requested_channel_id", req.ChannelID) + return fmt.Errorf("room '%s' is already mapped to channel '%s'", req.BridgeRoomID, existingChannelID) + } + + // NEW: Check if room exists on target bridge + roomExists, err := bridge.RoomExists(req.BridgeRoomID) + if err != nil { + m.logger.LogError("Failed to check room existence", "bridge_room_id", req.BridgeRoomID, "error", err) + return fmt.Errorf("failed to check room existence: %w", err) + } + if !roomExists { + m.logger.LogWarn("Room does not exist on bridge", + "bridge_room_id", req.BridgeRoomID, + "bridge_name", req.BridgeName) + return fmt.Errorf("room '%s' does not exist on %s bridge", req.BridgeRoomID, req.BridgeName) + } + + m.logger.LogDebug("Room validation passed", + "bridge_room_id", req.BridgeRoomID, + "bridge_name", req.BridgeName, + "room_exists", roomExists, + "already_mapped", false) + // Create the channel mapping on the receiving bridge if err = bridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil { m.logger.LogError("Failed to create channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err) diff --git a/server/bridge/mattermost/bridge.go b/server/bridge/mattermost/bridge.go index ccd69e6..4c1fd6c 100644 --- a/server/bridge/mattermost/bridge.go +++ b/server/bridge/mattermost/bridge.go @@ -255,3 +255,53 @@ func (b *mattermostBridge) DeleteChannelMapping(channelID string) error { b.logger.LogInfo("Deleted Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID) return nil } + +// RoomExists checks if a Mattermost channel exists on the server +func (b *mattermostBridge) RoomExists(roomID string) (bool, error) { + if b.api == nil { + return false, fmt.Errorf("Mattermost API not initialized") + } + + b.logger.LogDebug("Checking if Mattermost channel exists", "channel_id", roomID) + + // Use the Mattermost API to check if the channel exists + channel, appErr := b.api.GetChannel(roomID) + if appErr != nil { + if appErr.StatusCode == 404 { + b.logger.LogDebug("Mattermost channel does not exist", "channel_id", roomID) + return false, nil + } + b.logger.LogError("Failed to check channel existence", "channel_id", roomID, "error", appErr) + return false, fmt.Errorf("failed to check channel existence: %w", appErr) + } + + if channel == nil { + b.logger.LogDebug("Mattermost channel does not exist (nil response)", "channel_id", roomID) + return false, nil + } + + b.logger.LogDebug("Mattermost channel exists", "channel_id", roomID, "channel_name", channel.Name) + return true, nil +} + +// GetRoomMapping retrieves the Mattermost channel ID for a given room ID (reverse lookup) +func (b *mattermostBridge) GetRoomMapping(roomID string) (string, error) { + if b.kvstore == nil { + return "", fmt.Errorf("KV store not initialized") + } + + b.logger.LogDebug("Getting channel mapping for Mattermost room", "room_id", roomID) + + // Look up the channel ID using the room ID as the key + channelIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", roomID)) + if err != nil { + // No mapping found is not an error, just return empty string + b.logger.LogDebug("No channel mapping found for room", "room_id", roomID) + return "", nil + } + + channelID := string(channelIDBytes) + b.logger.LogDebug("Found channel mapping for room", "room_id", roomID, "channel_id", channelID) + + return channelID, nil +} diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index a2697e3..4a9f0f8 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -73,6 +73,7 @@ func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Cli cfg.GetXMPPResource(), "", // remoteID not needed for bridge user tlsConfig, + b.logger, ) } @@ -471,3 +472,48 @@ func (b *xmppBridge) DeleteChannelMapping(channelID string) error { b.logger.LogInfo("Deleted channel room mapping", "channel_id", channelID, "room_jid", roomJID) return nil } + +// RoomExists checks if an XMPP room exists on the remote service +func (b *xmppBridge) RoomExists(roomID string) (bool, error) { + if !b.connected.Load() { + return false, fmt.Errorf("not connected to XMPP server") + } + + if b.xmppClient == nil { + return false, fmt.Errorf("XMPP client not initialized") + } + + b.logger.LogDebug("Checking if XMPP room exists", "room_jid", roomID) + + // Use the XMPP client to check room existence + exists, err := b.xmppClient.CheckRoomExists(roomID) + if err != nil { + b.logger.LogError("Failed to check room existence", "room_jid", roomID, "error", err) + return false, fmt.Errorf("failed to check room existence: %w", err) + } + + b.logger.LogDebug("Room existence check completed", "room_jid", roomID, "exists", exists) + return exists, nil +} + +// GetRoomMapping retrieves the Mattermost channel ID for a given XMPP room JID (reverse lookup) +func (b *xmppBridge) GetRoomMapping(roomID string) (string, error) { + if b.kvstore == nil { + return "", fmt.Errorf("KV store not initialized") + } + + b.logger.LogDebug("Getting channel mapping for XMPP room", "room_jid", roomID) + + // Look up the channel ID using the room JID as the key + channelIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("xmpp", roomID)) + if err != nil { + // No mapping found is not an error, just return empty string + b.logger.LogDebug("No channel mapping found for room", "room_jid", roomID) + return "", nil + } + + channelID := string(channelIDBytes) + b.logger.LogDebug("Found channel mapping for room", "room_jid", roomID, "channel_id", channelID) + + return channelID, nil +} diff --git a/server/command/command.go b/server/command/command.go index 931287e..40bf026 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -54,6 +54,14 @@ func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.Bridg // ExecuteCommand hook calls this method to execute the commands that were registered in the NewCommandHandler function. func (c *Handler) Handle(args *model.CommandArgs) (*model.CommandResponse, error) { + // Check if user is system admin for all plugin commands + if !c.isSystemAdmin(args.UserId) { + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: "❌ Only system administrators can use XMPP bridge commands.", + }, nil + } + trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/") switch trigger { case xmppBridgeCommandTrigger: @@ -162,10 +170,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m err = c.bridgeManager.OnChannelMappingCreated(mappingReq) if err != nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: fmt.Sprintf("❌ Failed to create channel mapping: %v", err), - } + return c.formatMappingError("create", roomJID, err) } return &model.CommandResponse{ @@ -212,10 +217,7 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes err = c.bridgeManager.OnChannelMappingDeleted(deleteReq) if err != nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: fmt.Sprintf("❌ Failed to unmap channel: %v", err), - } + return c.formatMappingError("delete", roomJID, err) } return &model.CommandResponse{ @@ -269,3 +271,85 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe - Use `+"`/xmppbridge unmap`"+` to unmap this channel from an XMPP room`, statusText, mappingText), } } + +// isSystemAdmin checks if the user is a system administrator +func (c *Handler) isSystemAdmin(userID string) bool { + user, err := c.client.User.Get(userID) + if err != nil { + c.client.Log.Warn("Failed to get user for admin check", "user_id", userID, "error", err) + return false + } + + return user.IsSystemAdmin() +} + +// formatMappingError provides user-friendly error messages for mapping operations +func (c *Handler) formatMappingError(operation, roomJID string, err error) *model.CommandResponse { + errorMsg := err.Error() + + // Handle specific error cases with user-friendly messages + switch { + case strings.Contains(errorMsg, "already mapped to channel"): + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf(`❌ **Room Already Mapped** + +The XMPP room **%s** is already connected to another channel. + +**What you can do:** +- Choose a different XMPP room that isn't already in use +- Unmap the room from the other channel first using ` + "`/xmppbridge unmap`" + ` +- Use ` + "`/xmppbridge status`" + ` to check current mappings`, roomJID), + } + + case strings.Contains(errorMsg, "does not exist"): + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf(`❌ **Room Not Found** + +The XMPP room **%s** doesn't exist or isn't accessible. + +**What you can do:** +- Check that the room JID is spelled correctly +- Make sure the room exists on the XMPP server +- Verify you have permission to access the room +- Contact your XMPP administrator if needed + +**Example format:** room@conference.example.com`, roomJID), + } + + case strings.Contains(errorMsg, "not connected"): + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: `❌ **Bridge Not Connected** + +The XMPP bridge is currently disconnected. + +**What you can do:** +- Wait a moment and try again (the bridge may be reconnecting) +- Contact your system administrator +- Use ` + "`/xmppbridge status`" + ` to check the connection status`, + } + + default: + // Generic error message for unknown cases + action := "create the mapping" + if operation == "delete" { + action = "remove the mapping" + } + + return &model.CommandResponse{ + ResponseType: model.CommandResponseTypeEphemeral, + Text: fmt.Sprintf(`❌ **Operation Failed** + +Unable to %s for room **%s**. + +**What you can do:** +- Try the command again in a few moments +- Use ` + "`/xmppbridge status`" + ` to check the bridge status +- Contact your system administrator if the problem persists + +**Error details:** %s`, action, roomJID, errorMsg), + } + } +} diff --git a/server/model/bridge.go b/server/model/bridge.go index 799188f..eab186e 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -133,6 +133,12 @@ type Bridge interface { // DeleteChannelMapping removes a mapping between a Mattermost channel ID and a bridge room ID. DeleteChannelMapping(channelID string) error + // RoomExists checks if a room/channel exists on the remote service. + RoomExists(roomID string) (bool, error) + + // GetRoomMapping retrieves the Mattermost channel ID for a given room ID (reverse lookup). + GetRoomMapping(roomID string) (string, error) + // IsConnected checks if the bridge is connected to the remote service. IsConnected() bool } diff --git a/server/plugin.go b/server/plugin.go index 760d200..439c04c 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -153,6 +153,7 @@ func (p *Plugin) initXMPPClient() { cfg.XMPPPassword, cfg.GetXMPPResource(), p.remoteID, + p.logger, ) } diff --git a/server/xmpp/client.go b/server/xmpp/client.go index 2ed8681..4cb4440 100644 --- a/server/xmpp/client.go +++ b/server/xmpp/client.go @@ -8,8 +8,10 @@ import ( "fmt" "time" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" "mellium.im/sasl" "mellium.im/xmpp" + "mellium.im/xmpp/disco" "mellium.im/xmpp/jid" "mellium.im/xmpp/muc" "mellium.im/xmpp/mux" @@ -25,6 +27,7 @@ type Client struct { remoteID string // Plugin remote ID for metadata serverDomain string // explicit server domain for testing tlsConfig *tls.Config // custom TLS configuration + logger logger.Logger // Logger for debugging // XMPP connection session *xmpp.Session @@ -80,7 +83,7 @@ type UserProfile struct { } // NewClient creates a new XMPP client. -func NewClient(serverURL, username, password, resource, remoteID string) *Client { +func NewClient(serverURL, username, password, resource, remoteID string, logger logger.Logger) *Client { ctx, cancel := context.WithCancel(context.Background()) mucClient := &muc.Client{} mux := mux.New("jabber:client", muc.HandleClient(mucClient)) @@ -91,6 +94,7 @@ func NewClient(serverURL, username, password, resource, remoteID string) *Client password: password, resource: resource, remoteID: remoteID, + logger: logger, ctx: ctx, cancel: cancel, mucClient: mucClient, @@ -100,8 +104,8 @@ func NewClient(serverURL, username, password, resource, remoteID string) *Client } // NewClientWithTLS creates a new XMPP client with custom TLS configuration. -func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config) *Client { - client := NewClient(serverURL, username, password, resource, remoteID) +func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config, logger logger.Logger) *Client { + client := NewClient(serverURL, username, password, resource, remoteID, logger) client.tlsConfig = tlsConfig return client } @@ -430,3 +434,93 @@ func (c *Client) SetOnlinePresence() error { return nil } + +// CheckRoomExists verifies if an XMPP room exists and is accessible using disco#info +func (c *Client) CheckRoomExists(roomJID string) (bool, error) { + if c.session == nil { + return false, fmt.Errorf("XMPP session not established") + } + + c.logger.LogDebug("Checking room existence using disco#info", "room_jid", roomJID) + + // Parse and validate the room JID + roomAddr, err := jid.Parse(roomJID) + if err != nil { + c.logger.LogError("Invalid room JID", "room_jid", roomJID, "error", err) + return false, fmt.Errorf("invalid room JID: %w", err) + } + + // Set timeout for the disco query + ctx, cancel := context.WithTimeout(c.ctx, 10*time.Second) + defer cancel() + + // Perform disco#info query to the room + info, err := disco.GetInfo(ctx, "", roomAddr, c.session) + if err != nil { + // Check if it's a service-unavailable or item-not-found error + if stanzaErr, ok := err.(stanza.Error); ok { + c.logger.LogDebug("Received stanza error during disco#info query", + "room_jid", roomJID, + "error_condition", string(stanzaErr.Condition), + "error_type", string(stanzaErr.Type)) + + switch stanzaErr.Condition { + case stanza.ServiceUnavailable, stanza.ItemNotFound: + c.logger.LogDebug("Room does not exist", "room_jid", roomJID, "condition", string(stanzaErr.Condition)) + return false, nil // Room doesn't exist + case stanza.Forbidden: + c.logger.LogWarn("Access denied to room (room exists but not accessible)", "room_jid", roomJID) + return false, fmt.Errorf("access denied to room %s", roomJID) + case stanza.NotAuthorized: + c.logger.LogWarn("Not authorized to query room (room exists but not queryable)", "room_jid", roomJID) + return false, fmt.Errorf("not authorized to query room %s", roomJID) + default: + c.logger.LogError("Unexpected disco query error", "room_jid", roomJID, "condition", string(stanzaErr.Condition), "error", err) + return false, fmt.Errorf("disco query failed: %w", err) + } + } + c.logger.LogError("Disco query error", "room_jid", roomJID, "error", err) + return false, fmt.Errorf("disco query error: %w", err) + } + + c.logger.LogDebug("Received disco#info response, checking for MUC features", + "room_jid", roomJID, + "features_count", len(info.Features), + "identities_count", len(info.Identity)) + + // Verify it's actually a MUC room by checking features + for _, feature := range info.Features { + if feature.Var == muc.NS { // "http://jabber.org/protocol/muc" + c.logger.LogDebug("Room exists and has MUC feature", "room_jid", roomJID) + return true, nil + } + } + + // Check for conference identity as backup verification + for _, identity := range info.Identity { + if identity.Category == "conference" { + c.logger.LogDebug("Room exists and has conference identity", "room_jid", roomJID, "identity_type", identity.Type) + return true, nil + } + } + + // Log all features and identities for debugging + c.logger.LogDebug("Room exists but doesn't appear to be a MUC room", + "room_jid", roomJID, + "features", func() []string { + var features []string + for _, f := range info.Features { + features = append(features, f.Var) + } + return features + }(), + "identities", func() []string { + var identities []string + for _, i := range info.Identity { + identities = append(identities, fmt.Sprintf("%s/%s", i.Category, i.Type)) + } + return identities + }()) + + return false, nil +} From 35174c61a2597d8cae2d5d5efdd9f83e9c163a94 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 4 Aug 2025 13:49:25 +0200 Subject: [PATCH 07/10] pluginctl: update to v0.1.2 --- .golangci.yml | 102 +++++++++++++++++++++++++++++++++++--------------- build/test.mk | 14 ++++--- plugin.json | 42 +++++++++++++++------ 3 files changed, 110 insertions(+), 48 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2b4c948..8c2d951 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,48 +1,88 @@ -run: - timeout: 5m - modules-download-mode: readonly - -linters-settings: - gofmt: - simplify: true - goimports: - local-prefixes: github.com/mattermost/mattermost-plugin-bridge-xmpp - govet: - check-shadowing: true - enable-all: true - disable: - - fieldalignment - misspell: - locale: US +version: "2" linters: - disable-all: true enable: - bodyclose - errcheck - gocritic - - gofmt - - goimports - gosec - - gosimple - - govet - ineffassign - misspell - nakedret - revive - - staticcheck - - stylecheck + - staticcheck # Now includes gosimple and stylecheck - typecheck - unconvert - unused - whitespace + - govet # Ensure this is included + + settings: + errcheck: + # Add any errcheck settings here + exclude-functions: + - io.Copy(*bytes.Buffer) + + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + + gosec: + # Add gosec settings + excludes: + - G104 # Errors unhandled + + staticcheck: + # Configure staticcheck (includes gosimple/stylecheck checks) + checks: ["all"] + + revive: + # Add revive rules + rules: + - name: exported + disabled: false + + exclusions: + presets: + - comments + - std-error-handling + - common-false-positives + + rules: + - path: '_test\.go' + linters: + - errcheck + - gosec + +formatters: + enable: + - gofmt + - goimports + + settings: + gofmt: + simplify: true + + goimports: + local-prefixes: + - github.com/mattermost/mattermost-plugin-bridge-xmpp + +output: + formats: + text: + path: stdout + colors: true + print-linter-name: true + +run: + timeout: 5m + tests: true issues: - exclude-rules: - - path: server/configuration.go - linters: - - unused - - path: _test\.go - linters: - - bodyclose - - scopelint # https://github.com/kyoh86/scopelint/issues/4 + max-issues-per-linter: 0 + max-same-issues: 0 + fix: false diff --git a/build/test.mk b/build/test.mk index e7691a2..16261da 100644 --- a/build/test.mk +++ b/build/test.mk @@ -2,11 +2,13 @@ # Testing and Quality Assurance # ==================================================================================== +GOLANGCI_LINT_BINARY = ./build/bin/golangci-lint +GOTESTSUM_BINARY = ./build/bin/gotestsum + ## Install go tools install-go-tools: - @echo Installing go tools - $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 - $(GO) install gotest.tools/gotestsum@v1.7.0 + @echo "Installing development tools..." + @pluginctl tools install --bin-dir ./build/bin ## Runs eslint and golangci-lint .PHONY: check-style @@ -24,14 +26,14 @@ endif ifneq ($(HAS_SERVER),) @echo Running golangci-lint $(GO) vet ./... - $(GOBIN)/golangci-lint run ./... + $(GOLANGCI_LINT_BINARY) run ./... endif ## Runs any lints and unit tests defined for the server and webapp, if they exist. .PHONY: test test: apply webapp/node_modules install-go-tools ifneq ($(HAS_SERVER),) - $(GOBIN)/gotestsum -- -v ./... + $(GOTESTSUM_BINARY) -- -v ./... endif ifneq ($(HAS_WEBAPP),) cd webapp && $(NPM) run test; @@ -42,7 +44,7 @@ endif .PHONY: test-ci test-ci: apply webapp/node_modules install-go-tools ifneq ($(HAS_SERVER),) - $(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./... + $(GOTESTSUM_BINARY) --format standard-verbose --junitfile report.xml -- ./... endif ifneq ($(HAS_WEBAPP),) cd webapp && $(NPM) run test; diff --git a/plugin.json b/plugin.json index a7f0e11..4a1f690 100644 --- a/plugin.json +++ b/plugin.json @@ -29,28 +29,40 @@ "display_name": "XMPP Server URL", "type": "text", "help_text": "The URL of the XMPP server to connect to (e.g., xmpp.example.com:5222)", - "placeholder": "xmpp.example.com:5222" + "placeholder": "xmpp.example.com:5222", + "default": null, + "hosting": "", + "secret": false }, { "key": "XMPPUsername", "display_name": "XMPP Username", "type": "text", "help_text": "The username for authenticating with the XMPP server", - "placeholder": "bridge@xmpp.example.com" + "placeholder": "bridge@xmpp.example.com", + "default": null, + "hosting": "", + "secret": false }, { "key": "XMPPPassword", "display_name": "XMPP Password", "type": "text", - "secret": true, - "help_text": "The password for authenticating with the XMPP server" + "help_text": "The password for authenticating with the XMPP server", + "placeholder": "", + "default": null, + "hosting": "", + "secret": true }, { "key": "EnableSync", "display_name": "Enable Message Synchronization", "type": "bool", "help_text": "When enabled, messages will be synchronized between Mattermost and XMPP", - "default": false + "placeholder": "", + "default": false, + "hosting": "", + "secret": false }, { "key": "XMPPUsernamePrefix", @@ -58,7 +70,9 @@ "type": "text", "help_text": "Prefix for XMPP users in Mattermost (e.g., 'xmpp' creates usernames like 'xmpp:user@domain')", "placeholder": "xmpp", - "default": "xmpp" + "default": "xmpp", + "hosting": "", + "secret": false }, { "key": "XMPPResource", @@ -66,20 +80,26 @@ "type": "text", "help_text": "XMPP resource identifier for the bridge client", "placeholder": "mattermost-bridge", - "default": "mattermost-bridge" + "default": "mattermost-bridge", + "hosting": "", + "secret": false }, { "key": "XMPPInsecureSkipVerify", "display_name": "Skip TLS Certificate Verification", "type": "bool", "help_text": "Skip TLS certificate verification for XMPP connections (use only for testing/development)", - "default": false + "placeholder": "", + "default": false, + "hosting": "", + "secret": false } - ] + ], + "sections": null }, "props": { "pluginctl": { - "version": "v0.1.1" + "version": "v0.1.2" } } -} +} \ No newline at end of file From ea1711e94cdbe8b0a1881ecdc6ce291c0891cc90 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 4 Aug 2025 16:42:59 +0200 Subject: [PATCH 08/10] feat: implement OnSharedChannelsPing hook with active bridge health checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Ping() method to Bridge interface for active connectivity testing - Implement XMPP ping using disco#info query to server domain (fast & reliable) - Implement Mattermost bridge ping using GetServerVersion API call - Add comprehensive OnSharedChannelsPing hook with proper error handling - Replace timeout-prone IQ ping with proven disco#info approach - Add detailed logging for monitoring and debugging ping operations - Fix doctor command to use new Ping method instead of TestConnection - Performance: XMPP ping now completes in ~4ms vs previous 5s timeout 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/xmpp-client-doctor/main.go | 2 +- server/bridge/manager.go | 24 ++++----- server/bridge/mattermost/bridge.go | 27 +++++++++- server/bridge/xmpp/bridge.go | 32 ++++++++++-- server/command/command.go | 30 ++++++------ server/hooks_sharedchannels.go | 46 +++++++++++++++++ server/model/bridge.go | 23 +++++---- server/xmpp/client.go | 79 +++++++++++++++++------------- 8 files changed, 184 insertions(+), 79 deletions(-) create mode 100644 server/hooks_sharedchannels.go diff --git a/cmd/xmpp-client-doctor/main.go b/cmd/xmpp-client-doctor/main.go index 0de41fd..10c0ed0 100644 --- a/cmd/xmpp-client-doctor/main.go +++ b/cmd/xmpp-client-doctor/main.go @@ -165,7 +165,7 @@ func testXMPPClient(config *Config) error { // Test connection health start = time.Now() - err = client.TestConnection() + err = client.Ping() if err != nil { return fmt.Errorf("connection health test failed: %w", err) } diff --git a/server/bridge/manager.go b/server/bridge/manager.go index 6e17483..2cb4da3 100644 --- a/server/bridge/manager.go +++ b/server/bridge/manager.go @@ -224,8 +224,8 @@ func (m *Manager) OnPluginConfigurationChange(config any) error { return nil } -// OnChannelMappingCreated handles the creation of a channel mapping by calling the appropriate bridge -func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error { +// CreateChannelMapping handles the creation of a channel mapping by calling the appropriate bridge +func (m *Manager) CreateChannelMapping(req model.CreateChannelMappingRequest) error { // Validate request if err := req.Validate(); err != nil { return fmt.Errorf("invalid mapping request: %w", err) @@ -252,9 +252,9 @@ func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error return fmt.Errorf("failed to check room mapping: %w", err) } if existingChannelID != "" { - m.logger.LogWarn("Room already mapped to another channel", - "bridge_room_id", req.BridgeRoomID, - "existing_channel_id", existingChannelID, + m.logger.LogWarn("Room already mapped to another channel", + "bridge_room_id", req.BridgeRoomID, + "existing_channel_id", existingChannelID, "requested_channel_id", req.ChannelID) return fmt.Errorf("room '%s' is already mapped to channel '%s'", req.BridgeRoomID, existingChannelID) } @@ -266,14 +266,14 @@ func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error return fmt.Errorf("failed to check room existence: %w", err) } if !roomExists { - m.logger.LogWarn("Room does not exist on bridge", - "bridge_room_id", req.BridgeRoomID, + m.logger.LogWarn("Room does not exist on bridge", + "bridge_room_id", req.BridgeRoomID, "bridge_name", req.BridgeName) return fmt.Errorf("room '%s' does not exist on %s bridge", req.BridgeRoomID, req.BridgeName) } - m.logger.LogDebug("Room validation passed", - "bridge_room_id", req.BridgeRoomID, + m.logger.LogDebug("Room validation passed", + "bridge_room_id", req.BridgeRoomID, "bridge_name", req.BridgeName, "room_exists", roomExists, "already_mapped", false) @@ -307,8 +307,8 @@ func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error return nil } -// OnChannelMappingDeleted handles the deletion of a channel mapping by calling the appropriate bridges -func (m *Manager) OnChannelMappingDeleted(req model.ChannelMappingDeleteRequest) error { +// DeleteChannepMapping handles the deletion of a channel mapping by calling the appropriate bridges +func (m *Manager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) error { // Validate request if err := req.Validate(); err != nil { return fmt.Errorf("invalid delete request: %w", err) @@ -359,7 +359,7 @@ func (m *Manager) OnChannelMappingDeleted(req model.ChannelMappingDeleteRequest) } // shareChannel creates a shared channel configuration using the Mattermost API -func (m *Manager) shareChannel(req model.ChannelMappingRequest) error { +func (m *Manager) shareChannel(req model.CreateChannelMappingRequest) error { if m.remoteID == "" { return fmt.Errorf("remote ID not set - plugin not registered for shared channels") } diff --git a/server/bridge/mattermost/bridge.go b/server/bridge/mattermost/bridge.go index 4c1fd6c..da3728d 100644 --- a/server/bridge/mattermost/bridge.go +++ b/server/bridge/mattermost/bridge.go @@ -57,12 +57,11 @@ func (b *mattermostBridge) UpdateConfiguration(newConfig any) error { } b.configMu.Lock() - oldConfig := b.config b.config = cfg b.configMu.Unlock() // Log the configuration change - b.logger.LogInfo("Mattermost bridge configuration updated", "old_config", oldConfig, "new_config", cfg) + b.logger.LogInfo("Mattermost bridge configuration updated") return nil } @@ -174,6 +173,30 @@ func (b *mattermostBridge) IsConnected() bool { return b.connected.Load() } +// Ping actively tests the Mattermost API connectivity +func (b *mattermostBridge) Ping() error { + if !b.connected.Load() { + return fmt.Errorf("Mattermost bridge is not connected") + } + + if b.api == nil { + return fmt.Errorf("Mattermost API not initialized") + } + + b.logger.LogDebug("Testing Mattermost bridge connectivity with API ping") + + // Test API connectivity with a lightweight call + // Using GetServerVersion as it's a simple, read-only operation + version := b.api.GetServerVersion() + if version == "" { + b.logger.LogWarn("Mattermost bridge ping returned empty version") + return fmt.Errorf("Mattermost API ping returned empty server version") + } + + b.logger.LogDebug("Mattermost bridge ping successful", "server_version", version) + return nil +} + // CreateChannelMapping creates a mapping between a Mattermost channel and another Mattermost room/channel func (b *mattermostBridge) CreateChannelMapping(channelID, roomID string) error { if b.kvstore == nil { diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index 4a9f0f8..fb15df8 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -87,11 +87,13 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error { b.configMu.Lock() oldConfig := b.config b.config = cfg + defer b.configMu.Unlock() + + b.logger.LogInfo("XMPP bridge configuration updated") // Initialize or update XMPP client with new configuration if cfg.EnableSync { if cfg.XMPPServerURL == "" || cfg.XMPPUsername == "" || cfg.XMPPPassword == "" { - b.configMu.Unlock() return fmt.Errorf("XMPP server URL, username, and password are required when sync is enabled") } @@ -100,8 +102,6 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error { b.xmppClient = nil } - b.configMu.Unlock() - // Check if we need to restart the bridge due to configuration changes wasConnected := b.connected.Load() needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected @@ -322,7 +322,7 @@ func (b *xmppBridge) checkConnection() error { if !b.connected.Load() { return fmt.Errorf("not connected") } - return b.xmppClient.TestConnection() + return b.xmppClient.Ping() } // handleReconnection attempts to reconnect to XMPP and rejoin rooms @@ -376,6 +376,28 @@ func (b *xmppBridge) IsConnected() bool { return b.connected.Load() } +// Ping actively tests the XMPP connection health +func (b *xmppBridge) Ping() error { + if !b.connected.Load() { + return fmt.Errorf("XMPP bridge is not connected") + } + + if b.xmppClient == nil { + return fmt.Errorf("XMPP client not initialized") + } + + b.logger.LogDebug("Testing XMPP bridge connectivity with ping") + + // Use the XMPP client's ping method + if err := b.xmppClient.Ping(); err != nil { + b.logger.LogWarn("XMPP bridge ping failed", "error", err) + return fmt.Errorf("XMPP bridge ping failed: %w", err) + } + + b.logger.LogDebug("XMPP bridge ping successful") + return nil +} + // CreateChannelMapping creates a mapping between a Mattermost channel and XMPP room func (b *xmppBridge) CreateChannelMapping(channelID, roomJID string) error { if b.kvstore == nil { @@ -514,6 +536,6 @@ func (b *xmppBridge) GetRoomMapping(roomID string) (string, error) { channelID := string(channelIDBytes) b.logger.LogDebug("Found channel mapping for room", "room_jid", roomID, "channel_id", channelID) - + return channelID, nil } diff --git a/server/command/command.go b/server/command/command.go index 40bf026..d15b9f5 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -160,15 +160,15 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m } // Create the mapping using BridgeManager - mappingReq := pluginModel.ChannelMappingRequest{ + mappingReq := pluginModel.CreateChannelMappingRequest{ ChannelID: channelID, BridgeName: "xmpp", BridgeRoomID: roomJID, UserID: args.UserId, TeamID: args.TeamId, } - - err = c.bridgeManager.OnChannelMappingCreated(mappingReq) + + err = c.bridgeManager.CreateChannelMapping(mappingReq) if err != nil { return c.formatMappingError("create", roomJID, err) } @@ -208,14 +208,14 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes } // Delete the mapping - deleteReq := pluginModel.ChannelMappingDeleteRequest{ + deleteReq := pluginModel.DeleteChannelMappingRequest{ ChannelID: channelID, BridgeName: "xmpp", UserID: args.UserId, TeamID: args.TeamId, } - - err = c.bridgeManager.OnChannelMappingDeleted(deleteReq) + + err = c.bridgeManager.DeleteChannepMapping(deleteReq) if err != nil { return c.formatMappingError("delete", roomJID, err) } @@ -279,14 +279,14 @@ func (c *Handler) isSystemAdmin(userID string) bool { c.client.Log.Warn("Failed to get user for admin check", "user_id", userID, "error", err) return false } - + return user.IsSystemAdmin() } // formatMappingError provides user-friendly error messages for mapping operations func (c *Handler) formatMappingError(operation, roomJID string, err error) *model.CommandResponse { errorMsg := err.Error() - + // Handle specific error cases with user-friendly messages switch { case strings.Contains(errorMsg, "already mapped to channel"): @@ -298,10 +298,10 @@ The XMPP room **%s** is already connected to another channel. **What you can do:** - Choose a different XMPP room that isn't already in use -- Unmap the room from the other channel first using ` + "`/xmppbridge unmap`" + ` -- Use ` + "`/xmppbridge status`" + ` to check current mappings`, roomJID), +- Unmap the room from the other channel first using `+"`/xmppbridge unmap`"+` +- Use `+"`/xmppbridge status`"+` to check current mappings`, roomJID), } - + case strings.Contains(errorMsg, "does not exist"): return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -317,7 +317,7 @@ The XMPP room **%s** doesn't exist or isn't accessible. **Example format:** room@conference.example.com`, roomJID), } - + case strings.Contains(errorMsg, "not connected"): return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, @@ -330,14 +330,14 @@ The XMPP bridge is currently disconnected. - Contact your system administrator - Use ` + "`/xmppbridge status`" + ` to check the connection status`, } - + default: // Generic error message for unknown cases action := "create the mapping" if operation == "delete" { action = "remove the mapping" } - + return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: fmt.Sprintf(`❌ **Operation Failed** @@ -346,7 +346,7 @@ Unable to %s for room **%s**. **What you can do:** - Try the command again in a few moments -- Use ` + "`/xmppbridge status`" + ` to check the bridge status +- Use `+"`/xmppbridge status`"+` to check the bridge status - Contact your system administrator if the problem persists **Error details:** %s`, action, roomJID, errorMsg), diff --git a/server/hooks_sharedchannels.go b/server/hooks_sharedchannels.go new file mode 100644 index 0000000..798ba0f --- /dev/null +++ b/server/hooks_sharedchannels.go @@ -0,0 +1,46 @@ +package main + +import "github.com/mattermost/mattermost/server/public/model" + +// OnSharedChannelsPing is called to check if the bridge is healthy and ready to process messages +func (p *Plugin) OnSharedChannelsPing(remoteCluster *model.RemoteCluster) bool { + config := p.getConfiguration() + + p.logger.LogDebug("OnSharedChannelsPing called", "remote_cluster_id", remoteCluster.RemoteId) + + var remoteClusterID string + if remoteCluster != nil { + remoteClusterID = remoteCluster.RemoteId + } + + p.logger.LogDebug("Received shared channels ping", "remote_cluster_id", remoteClusterID) + + // If sync is disabled, we're still "healthy" but not actively processing + if !config.EnableSync { + p.logger.LogDebug("Ping received but sync is disabled", "remote_cluster_id", remoteClusterID) + return true + } + + // Check if bridge manager is available + if p.bridgeManager == nil { + p.logger.LogError("Bridge manager not initialized during ping", "remote_cluster_id", remoteClusterID) + return false + } + + // Get the XMPP bridge for active connectivity testing + bridge, err := p.bridgeManager.GetBridge("xmpp") + if err != nil { + p.logger.LogWarn("XMPP bridge not available during ping", "error", err, "remote_cluster_id", remoteClusterID) + // Return true if bridge is not registered - this might be expected during startup/shutdown + return false + } + + // Perform active ping test on the XMPP bridge + if err := bridge.Ping(); err != nil { + p.logger.LogError("XMPP bridge ping failed", "error", err, "remote_cluster_id", remoteClusterID) + return false + } + + p.logger.LogDebug("Shared channels ping successful - XMPP bridge is healthy", "remote_cluster_id", remoteClusterID) + return true +} diff --git a/server/model/bridge.go b/server/model/bridge.go index eab186e..3a0559c 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -13,8 +13,8 @@ const ( UserStateOffline ) -// ChannelMappingRequest contains information needed to create a channel mapping -type ChannelMappingRequest struct { +// CreateChannelMappingRequest contains information needed to create a channel mapping +type CreateChannelMappingRequest struct { ChannelID string // Mattermost channel ID BridgeName string // Name of the bridge (e.g., "xmpp") BridgeRoomID string // Remote room/channel ID (e.g., JID for XMPP) @@ -23,7 +23,7 @@ type ChannelMappingRequest struct { } // Validate checks if all required fields are present and valid -func (r ChannelMappingRequest) Validate() error { +func (r CreateChannelMappingRequest) Validate() error { if r.ChannelID == "" { return fmt.Errorf("channelID cannot be empty") } @@ -42,8 +42,8 @@ func (r ChannelMappingRequest) Validate() error { return nil } -// ChannelMappingDeleteRequest contains information needed to delete a channel mapping -type ChannelMappingDeleteRequest struct { +// DeleteChannelMappingRequest contains information needed to delete a channel mapping +type DeleteChannelMappingRequest struct { ChannelID string // Mattermost channel ID BridgeName string // Name of the bridge (e.g., "xmpp") UserID string // ID of user who triggered the mapping deletion @@ -51,7 +51,7 @@ type ChannelMappingDeleteRequest struct { } // Validate checks if all required fields are present and valid -func (r ChannelMappingDeleteRequest) Validate() error { +func (r DeleteChannelMappingRequest) Validate() error { if r.ChannelID == "" { return fmt.Errorf("channelID cannot be empty") } @@ -107,11 +107,11 @@ type BridgeManager interface { // attempt updating all bridges. OnPluginConfigurationChange(config any) error - // OnChannelMappingCreated is called when a channel mapping is created. - OnChannelMappingCreated(req ChannelMappingRequest) error + // CreateChannelMapping is called when a channel mapping is created. + CreateChannelMapping(req CreateChannelMappingRequest) error - // OnChannelMappingDeleted is called when a channel mapping is deleted. - OnChannelMappingDeleted(req ChannelMappingDeleteRequest) error + // DeleteChannepMapping is called when a channel mapping is deleted. + DeleteChannepMapping(req DeleteChannelMappingRequest) error } type Bridge interface { @@ -141,6 +141,9 @@ type Bridge interface { // IsConnected checks if the bridge is connected to the remote service. IsConnected() bool + + // Ping actively tests the bridge connection health by sending a lightweight request. + Ping() error } type BridgeUserManager interface { diff --git a/server/xmpp/client.go b/server/xmpp/client.go index 4cb4440..aad5e24 100644 --- a/server/xmpp/client.go +++ b/server/xmpp/client.go @@ -24,19 +24,19 @@ type Client struct { username string password string resource string - remoteID string // Plugin remote ID for metadata - serverDomain string // explicit server domain for testing - tlsConfig *tls.Config // custom TLS configuration + remoteID string // Plugin remote ID for metadata + serverDomain string // explicit server domain for testing + tlsConfig *tls.Config // custom TLS configuration logger logger.Logger // Logger for debugging // XMPP connection - session *xmpp.Session - jidAddr jid.JID - ctx context.Context - cancel context.CancelFunc - mucClient *muc.Client - mux *mux.ServeMux - sessionReady chan struct{} + session *xmpp.Session + jidAddr jid.JID + ctx context.Context + cancel context.CancelFunc + mucClient *muc.Client + mux *mux.ServeMux + sessionReady chan struct{} sessionServing bool } @@ -87,7 +87,7 @@ func NewClient(serverURL, username, password, resource, remoteID string, logger ctx, cancel := context.WithCancel(context.Background()) mucClient := &muc.Client{} mux := mux.New("jabber:client", muc.HandleClient(mucClient)) - + return &Client{ serverURL: serverURL, username: username, @@ -183,11 +183,11 @@ func (c *Client) serveSession() { close(c.sessionReady) // Signal failure return } - + // Signal that the session is ready to serve c.sessionServing = true close(c.sessionReady) - + err := c.session.Serve(c.mux) if err != nil { c.sessionServing = false @@ -221,23 +221,6 @@ func (c *Client) Disconnect() error { return nil } -// TestConnection tests the XMPP connection -func (c *Client) TestConnection() error { - if c.session == nil { - if err := c.Connect(); err != nil { - return err - } - } - - // For now, just check if session exists and is not closed - // A proper ping implementation would require more complex IQ handling - if c.session == nil { - return fmt.Errorf("XMPP session is not established") - } - - return nil -} - // JoinRoom joins an XMPP Multi-User Chat room func (c *Client) JoinRoom(roomJID string) error { if c.session == nil { @@ -270,7 +253,7 @@ func (c *Client) JoinRoom(roomJID string) error { opts := []muc.Option{ muc.MaxBytes(0), // Don't limit message history } - + // Run the join operation in a goroutine to avoid blocking errChan := make(chan error, 1) go func() { @@ -459,7 +442,7 @@ func (c *Client) CheckRoomExists(roomJID string) (bool, error) { if err != nil { // Check if it's a service-unavailable or item-not-found error if stanzaErr, ok := err.(stanza.Error); ok { - c.logger.LogDebug("Received stanza error during disco#info query", + c.logger.LogDebug("Received stanza error during disco#info query", "room_jid", roomJID, "error_condition", string(stanzaErr.Condition), "error_type", string(stanzaErr.Type)) @@ -483,7 +466,7 @@ func (c *Client) CheckRoomExists(roomJID string) (bool, error) { return false, fmt.Errorf("disco query error: %w", err) } - c.logger.LogDebug("Received disco#info response, checking for MUC features", + c.logger.LogDebug("Received disco#info response, checking for MUC features", "room_jid", roomJID, "features_count", len(info.Features), "identities_count", len(info.Identity)) @@ -505,7 +488,7 @@ func (c *Client) CheckRoomExists(roomJID string) (bool, error) { } // Log all features and identities for debugging - c.logger.LogDebug("Room exists but doesn't appear to be a MUC room", + c.logger.LogDebug("Room exists but doesn't appear to be a MUC room", "room_jid", roomJID, "features", func() []string { var features []string @@ -524,3 +507,31 @@ func (c *Client) CheckRoomExists(roomJID string) (bool, error) { return false, nil } + +// Ping sends a lightweight ping to the XMPP server to test connectivity +func (c *Client) Ping() error { + if c.session == nil { + return fmt.Errorf("XMPP session not established") + } + + c.logger.LogDebug("Sending XMPP ping to test connectivity") + + // Create a context with timeout for the ping + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + start := time.Now() + + // Use disco#info query to server domain as a connectivity test + // This is a standard, lightweight XMPP operation that all servers support + _, err := disco.GetInfo(ctx, "", c.jidAddr.Domain(), c.session) + if err != nil { + duration := time.Since(start) + c.logger.LogDebug("XMPP ping failed", "error", err, "duration", duration) + return fmt.Errorf("XMPP server ping failed: %w", err) + } + + duration := time.Since(start) + c.logger.LogDebug("XMPP ping successful", "duration", duration) + return nil +} From db8037ffbf389a2a5ecc061a2b1ec6d9b5f0d8c5 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 4 Aug 2025 17:50:44 +0200 Subject: [PATCH 09/10] feat: implement comprehensive bridge-agnostic user management system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a complete multi-user bridge management system that allows bridges to control multiple users with async goroutine management and convenience methods for channel operations. Key features: - Bridge-agnostic BridgeUser interface with validation, identity, state management, channel operations, connection lifecycle, and goroutine lifecycle methods - BridgeUserManager interface for user lifecycle management with bridge type identification - XMPPUser implementation for XMPP bridge with XMPP client integration, connection monitoring, and room operations - MattermostUser implementation for Mattermost bridge with API integration and channel management - Updated Bridge interface to include GetUserManager() method - Base UserManager implementation with generic user management logic - Added Ping() and CheckChannelExists() methods to BridgeUser interface for health checking and room validation - Updated bridge manager naming from Manager to BridgeManager for clarity The system enables bridges to manage multiple users (like "Mattermost Bridge" user in XMPP) with proper state management, connection monitoring, and channel operations abstracted across different bridge protocols. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- server/bridge/manager.go | 38 ++-- server/bridge/mattermost/bridge.go | 14 +- server/bridge/mattermost/user.go | 300 ++++++++++++++++++++++++++ server/bridge/user.go | 188 ++++++++++++++++ server/bridge/xmpp/bridge.go | 92 ++++---- server/bridge/xmpp/user.go | 336 +++++++++++++++++++++++++++++ server/model/bridge.go | 73 +++++-- server/plugin.go | 2 +- 8 files changed, 949 insertions(+), 94 deletions(-) create mode 100644 server/bridge/mattermost/user.go create mode 100644 server/bridge/user.go create mode 100644 server/bridge/xmpp/user.go diff --git a/server/bridge/manager.go b/server/bridge/manager.go index 2cb4da3..1137368 100644 --- a/server/bridge/manager.go +++ b/server/bridge/manager.go @@ -10,8 +10,8 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" ) -// Manager manages multiple bridge instances -type Manager struct { +// BridgeManager manages multiple bridge instances +type BridgeManager struct { bridges map[string]model.Bridge mu sync.RWMutex logger logger.Logger @@ -19,8 +19,8 @@ type Manager struct { remoteID string } -// NewManager creates a new bridge manager -func NewManager(logger logger.Logger, api plugin.API, remoteID string) model.BridgeManager { +// NewBridgeManager creates a new bridge manager +func NewBridgeManager(logger logger.Logger, api plugin.API, remoteID string) model.BridgeManager { if logger == nil { panic("logger cannot be nil") } @@ -28,7 +28,7 @@ func NewManager(logger logger.Logger, api plugin.API, remoteID string) model.Bri panic("plugin API cannot be nil") } - return &Manager{ + return &BridgeManager{ bridges: make(map[string]model.Bridge), logger: logger, api: api, @@ -37,7 +37,7 @@ func NewManager(logger logger.Logger, api plugin.API, remoteID string) model.Bri } // RegisterBridge registers a bridge with the manager -func (m *Manager) RegisterBridge(name string, bridge model.Bridge) error { +func (m *BridgeManager) RegisterBridge(name string, bridge model.Bridge) error { if name == "" { return fmt.Errorf("bridge name cannot be empty") } @@ -59,7 +59,7 @@ func (m *Manager) RegisterBridge(name string, bridge model.Bridge) error { } // StartBridge starts a specific bridge -func (m *Manager) StartBridge(name string) error { +func (m *BridgeManager) StartBridge(name string) error { m.mu.RLock() bridge, exists := m.bridges[name] m.mu.RUnlock() @@ -80,7 +80,7 @@ func (m *Manager) StartBridge(name string) error { } // StopBridge stops a specific bridge -func (m *Manager) StopBridge(name string) error { +func (m *BridgeManager) StopBridge(name string) error { m.mu.RLock() bridge, exists := m.bridges[name] m.mu.RUnlock() @@ -101,7 +101,7 @@ func (m *Manager) StopBridge(name string) error { } // UnregisterBridge removes a bridge from the manager -func (m *Manager) UnregisterBridge(name string) error { +func (m *BridgeManager) UnregisterBridge(name string) error { m.mu.Lock() defer m.mu.Unlock() @@ -124,7 +124,7 @@ func (m *Manager) UnregisterBridge(name string) error { } // GetBridge retrieves a bridge by name -func (m *Manager) GetBridge(name string) (model.Bridge, error) { +func (m *BridgeManager) GetBridge(name string) (model.Bridge, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -137,7 +137,7 @@ func (m *Manager) GetBridge(name string) (model.Bridge, error) { } // ListBridges returns a list of all registered bridge names -func (m *Manager) ListBridges() []string { +func (m *BridgeManager) ListBridges() []string { m.mu.RLock() defer m.mu.RUnlock() @@ -150,7 +150,7 @@ func (m *Manager) ListBridges() []string { } // HasBridge checks if a bridge with the given name is registered -func (m *Manager) HasBridge(name string) bool { +func (m *BridgeManager) HasBridge(name string) bool { m.mu.RLock() defer m.mu.RUnlock() @@ -159,7 +159,7 @@ func (m *Manager) HasBridge(name string) bool { } // HasBridges checks if any bridges are registered -func (m *Manager) HasBridges() bool { +func (m *BridgeManager) HasBridges() bool { m.mu.RLock() defer m.mu.RUnlock() @@ -167,7 +167,7 @@ func (m *Manager) HasBridges() bool { } // Shutdown stops and unregisters all bridges -func (m *Manager) Shutdown() error { +func (m *BridgeManager) Shutdown() error { m.mu.Lock() defer m.mu.Unlock() @@ -196,7 +196,7 @@ func (m *Manager) Shutdown() error { } // OnPluginConfigurationChange propagates configuration changes to all registered bridges -func (m *Manager) OnPluginConfigurationChange(config any) error { +func (m *BridgeManager) OnPluginConfigurationChange(config any) error { m.mu.RLock() defer m.mu.RUnlock() @@ -225,7 +225,7 @@ func (m *Manager) OnPluginConfigurationChange(config any) error { } // CreateChannelMapping handles the creation of a channel mapping by calling the appropriate bridge -func (m *Manager) CreateChannelMapping(req model.CreateChannelMappingRequest) error { +func (m *BridgeManager) CreateChannelMapping(req model.CreateChannelMappingRequest) error { // Validate request if err := req.Validate(); err != nil { return fmt.Errorf("invalid mapping request: %w", err) @@ -308,7 +308,7 @@ func (m *Manager) CreateChannelMapping(req model.CreateChannelMappingRequest) er } // DeleteChannepMapping handles the deletion of a channel mapping by calling the appropriate bridges -func (m *Manager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) error { +func (m *BridgeManager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) error { // Validate request if err := req.Validate(); err != nil { return fmt.Errorf("invalid delete request: %w", err) @@ -359,7 +359,7 @@ func (m *Manager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) er } // shareChannel creates a shared channel configuration using the Mattermost API -func (m *Manager) shareChannel(req model.CreateChannelMappingRequest) error { +func (m *BridgeManager) shareChannel(req model.CreateChannelMappingRequest) error { if m.remoteID == "" { return fmt.Errorf("remote ID not set - plugin not registered for shared channels") } @@ -389,7 +389,7 @@ func (m *Manager) shareChannel(req model.CreateChannelMappingRequest) error { } // unshareChannel removes shared channel configuration using the Mattermost API -func (m *Manager) unshareChannel(channelID string) error { +func (m *BridgeManager) unshareChannel(channelID string) error { // Unshare the channel unshared, err := m.api.UnshareChannel(channelID) if err != nil { diff --git a/server/bridge/mattermost/bridge.go b/server/bridge/mattermost/bridge.go index da3728d..79baf6e 100644 --- a/server/bridge/mattermost/bridge.go +++ b/server/bridge/mattermost/bridge.go @@ -6,6 +6,7 @@ import ( "sync" "sync/atomic" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" @@ -15,9 +16,10 @@ import ( // mattermostBridge handles syncing messages between Mattermost instances type mattermostBridge struct { - logger logger.Logger - api plugin.API - kvstore kvstore.KVStore + logger logger.Logger + api plugin.API + kvstore kvstore.KVStore + userManager pluginModel.BridgeUserManager // Connection management connected atomic.Bool @@ -44,6 +46,7 @@ func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg * cancel: cancel, channelMappings: make(map[string]string), config: cfg, + userManager: bridge.NewUserManager("mattermost", log), } return bridge @@ -328,3 +331,8 @@ func (b *mattermostBridge) GetRoomMapping(roomID string) (string, error) { return channelID, nil } + +// GetUserManager returns the user manager for this bridge +func (b *mattermostBridge) GetUserManager() pluginModel.BridgeUserManager { + return b.userManager +} diff --git a/server/bridge/mattermost/user.go b/server/bridge/mattermost/user.go new file mode 100644 index 0000000..ba14ba9 --- /dev/null +++ b/server/bridge/mattermost/user.go @@ -0,0 +1,300 @@ +package mattermost + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" + mmModel "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// MattermostUser represents a Mattermost user that implements the BridgeUser interface +type MattermostUser struct { + // User identity + id string + displayName string + username string + email string + + // Mattermost API + api plugin.API + + // State management + state model.UserState + stateMu sync.RWMutex + + // Configuration + config *config.Configuration + + // Goroutine lifecycle + ctx context.Context + cancel context.CancelFunc + + // Logger + logger logger.Logger +} + +// NewMattermostUser creates a new Mattermost user +func NewMattermostUser(id, displayName, username, email string, api plugin.API, cfg *config.Configuration, logger logger.Logger) *MattermostUser { + ctx, cancel := context.WithCancel(context.Background()) + + return &MattermostUser{ + id: id, + displayName: displayName, + username: username, + email: email, + api: api, + state: model.UserStateOffline, + config: cfg, + ctx: ctx, + cancel: cancel, + logger: logger, + } +} + +// Validation +func (u *MattermostUser) Validate() error { + if u.id == "" { + return fmt.Errorf("user ID cannot be empty") + } + if u.username == "" { + return fmt.Errorf("username cannot be empty") + } + if u.config == nil { + return fmt.Errorf("configuration cannot be nil") + } + if u.api == nil { + return fmt.Errorf("Mattermost API cannot be nil") + } + return nil +} + +// Identity (bridge-agnostic) +func (u *MattermostUser) GetID() string { + return u.id +} + +func (u *MattermostUser) GetDisplayName() string { + return u.displayName +} + +// State management +func (u *MattermostUser) GetState() model.UserState { + u.stateMu.RLock() + defer u.stateMu.RUnlock() + return u.state +} + +func (u *MattermostUser) SetState(state model.UserState) error { + u.stateMu.Lock() + defer u.stateMu.Unlock() + + u.logger.LogDebug("Changing Mattermost user state", "user_id", u.id, "old_state", u.state, "new_state", state) + u.state = state + + // TODO: Update user status in Mattermost if needed + // This could involve setting custom status or presence indicators + + return nil +} + +// Channel operations (abstracted from rooms/channels/groups) +func (u *MattermostUser) JoinChannel(channelID string) error { + u.logger.LogDebug("Mattermost user joining channel", "user_id", u.id, "channel_id", channelID) + + // Add user to channel + _, appErr := u.api.AddUserToChannel(channelID, u.id, "") + if appErr != nil { + return fmt.Errorf("failed to add Mattermost user %s to channel %s: %w", u.id, channelID, appErr) + } + + u.logger.LogInfo("Mattermost user joined channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +func (u *MattermostUser) LeaveChannel(channelID string) error { + u.logger.LogDebug("Mattermost user leaving channel", "user_id", u.id, "channel_id", channelID) + + // Remove user from channel + appErr := u.api.DeleteChannelMember(channelID, u.id) + if appErr != nil { + return fmt.Errorf("failed to remove Mattermost user %s from channel %s: %w", u.id, channelID, appErr) + } + + u.logger.LogInfo("Mattermost user left channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +func (u *MattermostUser) SendMessageToChannel(channelID, message string) error { + u.logger.LogDebug("Mattermost user sending message to channel", "user_id", u.id, "channel_id", channelID) + + // Create post + post := &mmModel.Post{ + UserId: u.id, + ChannelId: channelID, + Message: message, + } + + // Send post + _, appErr := u.api.CreatePost(post) + if appErr != nil { + return fmt.Errorf("failed to send message to Mattermost channel %s: %w", channelID, appErr) + } + + u.logger.LogDebug("Mattermost user sent message to channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +// Connection lifecycle +func (u *MattermostUser) Connect() error { + u.logger.LogDebug("Connecting Mattermost user", "user_id", u.id, "username", u.username) + + // For Mattermost users, "connecting" means verifying the user exists and is accessible + user, appErr := u.api.GetUser(u.id) + if appErr != nil { + return fmt.Errorf("failed to verify Mattermost user %s: %w", u.id, appErr) + } + + // Update user information if it has changed + if user.GetDisplayName("") != u.displayName { + u.displayName = user.GetDisplayName("") + u.logger.LogDebug("Updated Mattermost user display name", "user_id", u.id, "display_name", u.displayName) + } + + u.logger.LogInfo("Mattermost user connected", "user_id", u.id, "username", u.username) + + // Update state to online + _ = u.SetState(model.UserStateOnline) + + return nil +} + +func (u *MattermostUser) Disconnect() error { + u.logger.LogDebug("Disconnecting Mattermost user", "user_id", u.id, "username", u.username) + + // For Mattermost users, "disconnecting" is mostly a state change + // The user still exists in Mattermost, but we're not actively managing them + + _ = u.SetState(model.UserStateOffline) + + u.logger.LogInfo("Mattermost user disconnected", "user_id", u.id, "username", u.username) + return nil +} + +func (u *MattermostUser) IsConnected() bool { + return u.GetState() == model.UserStateOnline +} + +func (u *MattermostUser) Ping() error { + if u.api == nil { + return fmt.Errorf("Mattermost API not initialized for user %s", u.id) + } + + // Test API connectivity by getting server version + version := u.api.GetServerVersion() + if version == "" { + return fmt.Errorf("Mattermost API ping returned empty server version for user %s", u.id) + } + + return nil +} + +// CheckChannelExists checks if a Mattermost channel exists +func (u *MattermostUser) CheckChannelExists(channelID string) (bool, error) { + if u.api == nil { + return false, fmt.Errorf("Mattermost API not initialized for user %s", u.id) + } + + // Try to get the channel by ID + _, appErr := u.api.GetChannel(channelID) + if appErr != nil { + // Check if it's a "not found" error + if appErr.StatusCode == 404 { + return false, nil // Channel doesn't exist + } + return false, fmt.Errorf("failed to check channel existence: %w", appErr) + } + + return true, nil +} + +// Goroutine lifecycle +func (u *MattermostUser) Start(ctx context.Context) error { + u.logger.LogDebug("Starting Mattermost user", "user_id", u.id, "username", u.username) + + // Update context + u.ctx = ctx + + // Connect to verify user exists + if err := u.Connect(); err != nil { + return fmt.Errorf("failed to start Mattermost user %s: %w", u.id, err) + } + + // Start monitoring in a goroutine + go u.monitor() + + u.logger.LogInfo("Mattermost user started", "user_id", u.id, "username", u.username) + return nil +} + +func (u *MattermostUser) Stop() error { + u.logger.LogDebug("Stopping Mattermost user", "user_id", u.id, "username", u.username) + + // Cancel context to stop goroutines + if u.cancel != nil { + u.cancel() + } + + // Disconnect + if err := u.Disconnect(); err != nil { + u.logger.LogWarn("Error disconnecting Mattermost user during stop", "user_id", u.id, "error", err) + } + + u.logger.LogInfo("Mattermost user stopped", "user_id", u.id, "username", u.username) + return nil +} + +// monitor periodically checks the user's status and updates information +func (u *MattermostUser) monitor() { + u.logger.LogDebug("Starting monitor for Mattermost user", "user_id", u.id) + + // Simple monitoring - check user exists periodically + for { + select { + case <-u.ctx.Done(): + u.logger.LogDebug("Monitor stopped for Mattermost user", "user_id", u.id) + return + default: + // Wait before next check + timeoutCtx, cancel := context.WithTimeout(u.ctx, 60*time.Second) + select { + case <-u.ctx.Done(): + cancel() + return + case <-timeoutCtx.Done(): + cancel() + continue + } + } + } +} + +// GetUsername returns the Mattermost username for this user (Mattermost-specific method) +func (u *MattermostUser) GetUsername() string { + return u.username +} + +// GetEmail returns the Mattermost email for this user (Mattermost-specific method) +func (u *MattermostUser) GetEmail() string { + return u.email +} + +// GetAPI returns the Mattermost API instance (for advanced operations) +func (u *MattermostUser) GetAPI() plugin.API { + return u.api +} \ No newline at end of file diff --git a/server/bridge/user.go b/server/bridge/user.go new file mode 100644 index 0000000..e565eb9 --- /dev/null +++ b/server/bridge/user.go @@ -0,0 +1,188 @@ +package bridge + +import ( + "context" + "fmt" + "sync" + + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" +) + +// Manager implements the BridgeUserManager interface with bridge-agnostic logic +type UserManager struct { + bridgeType string + logger logger.Logger + users map[string]model.BridgeUser + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc +} + +// NewUserManager creates a new user manager for a specific bridge type +func NewUserManager(bridgeType string, logger logger.Logger) model.BridgeUserManager { + ctx, cancel := context.WithCancel(context.Background()) + return &UserManager{ + bridgeType: bridgeType, + logger: logger, + users: make(map[string]model.BridgeUser), + ctx: ctx, + cancel: cancel, + } +} + +// CreateUser adds a user to the bridge system +func (m *UserManager) CreateUser(user model.BridgeUser) error { + // Validate the user first + if err := user.Validate(); err != nil { + return fmt.Errorf("invalid user: %w", err) + } + + userID := user.GetID() + + m.mu.Lock() + defer m.mu.Unlock() + + // Check if user already exists + if _, exists := m.users[userID]; exists { + return fmt.Errorf("user %s already exists", userID) + } + + m.logger.LogDebug("Adding bridge user", "bridge_type", m.bridgeType, "user_id", userID, "display_name", user.GetDisplayName()) + + // Store the user + m.users[userID] = user + + m.logger.LogInfo("Bridge user added successfully", "bridge_type", m.bridgeType, "user_id", userID) + return nil +} + +// GetUser retrieves a user by ID +func (m *UserManager) GetUser(userID string) (model.BridgeUser, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + user, exists := m.users[userID] + if !exists { + return nil, fmt.Errorf("user %s not found", userID) + } + + return user, nil +} + +// DeleteUser removes a user from the bridge system +func (m *UserManager) DeleteUser(userID string) error { + m.mu.Lock() + defer m.mu.Unlock() + + user, exists := m.users[userID] + if !exists { + return fmt.Errorf("user %s not found", userID) + } + + m.logger.LogDebug("Deleting bridge user", "bridge_type", m.bridgeType, "user_id", userID) + + // Stop the user first + if err := user.Stop(); err != nil { + m.logger.LogWarn("Error stopping user during deletion", "bridge_type", m.bridgeType, "user_id", userID, "error", err) + } + + // Disconnect if still connected + if user.IsConnected() { + if err := user.Disconnect(); err != nil { + m.logger.LogWarn("Error disconnecting user during deletion", "bridge_type", m.bridgeType, "user_id", userID, "error", err) + } + } + + // Remove from map + delete(m.users, userID) + + m.logger.LogInfo("Bridge user deleted successfully", "bridge_type", m.bridgeType, "user_id", userID) + return nil +} + +// ListUsers returns a list of all users +func (m *UserManager) ListUsers() []model.BridgeUser { + m.mu.RLock() + defer m.mu.RUnlock() + + users := make([]model.BridgeUser, 0, len(m.users)) + for _, user := range m.users { + users = append(users, user) + } + + return users +} + +// HasUser checks if a user exists +func (m *UserManager) HasUser(userID string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + _, exists := m.users[userID] + return exists +} + +// Start initializes the user manager +func (m *UserManager) Start(ctx context.Context) error { + m.logger.LogDebug("Starting user manager", "bridge_type", m.bridgeType) + + // Update context + m.ctx = ctx + + // Start all existing users + m.mu.RLock() + defer m.mu.RUnlock() + + for userID, user := range m.users { + if err := user.Start(ctx); err != nil { + m.logger.LogWarn("Failed to start user during manager startup", "bridge_type", m.bridgeType, "user_id", userID, "error", err) + // Continue starting other users even if one fails + } + } + + m.logger.LogInfo("User manager started", "bridge_type", m.bridgeType, "user_count", len(m.users)) + return nil +} + +// Stop shuts down the user manager +func (m *UserManager) Stop() error { + m.logger.LogDebug("Stopping user manager", "bridge_type", m.bridgeType) + + if m.cancel != nil { + m.cancel() + } + + // Stop all users + m.mu.RLock() + users := make([]model.BridgeUser, 0, len(m.users)) + for _, user := range m.users { + users = append(users, user) + } + m.mu.RUnlock() + + for _, user := range users { + if err := user.Stop(); err != nil { + m.logger.LogWarn("Error stopping user during manager shutdown", "bridge_type", m.bridgeType, "user_id", user.GetID(), "error", err) + } + } + + m.logger.LogInfo("User manager stopped", "bridge_type", m.bridgeType) + return nil +} + +// UpdateConfiguration updates configuration for all users +func (m *UserManager) UpdateConfiguration(cfg *config.Configuration) error { + m.logger.LogDebug("Updating configuration for user manager", "bridge_type", m.bridgeType) + + // For now, we don't propagate config changes to individual users + // This can be extended later if needed + m.logger.LogInfo("User manager configuration updated", "bridge_type", m.bridgeType) + return nil +} + +// GetBridgeType returns the bridge type this manager handles +func (m *UserManager) GetBridgeType() string { + return m.bridgeType +} diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index fb15df8..8ec1e0d 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -2,27 +2,28 @@ package xmpp import ( "context" - "crypto/tls" "sync" "sync/atomic" "time" "fmt" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore" - xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" "github.com/mattermost/mattermost/server/public/plugin" ) // xmppBridge handles syncing messages between Mattermost and XMPP type xmppBridge struct { - logger logger.Logger - api plugin.API - kvstore kvstore.KVStore - xmppClient *xmppClient.Client + logger logger.Logger + api plugin.API + kvstore kvstore.KVStore + bridgeUser model.BridgeUser // Handles the bridge user and main bridge XMPP connection + userManager pluginModel.BridgeUserManager // Connection management connected atomic.Bool @@ -41,7 +42,7 @@ type xmppBridge struct { // NewBridge creates a new XMPP bridge func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge { ctx, cancel := context.WithCancel(context.Background()) - bridge := &xmppBridge{ + b := &xmppBridge{ logger: log, api: api, kvstore: kvstore, @@ -49,32 +50,20 @@ func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg * cancel: cancel, channelMappings: make(map[string]string), config: cfg, + userManager: bridge.NewUserManager("xmpp", log), } // Initialize XMPP client with configuration if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" { - bridge.xmppClient = bridge.createXMPPClient(cfg) + b.bridgeUser = b.createXMPPClient(cfg) } - return bridge + return b } // createXMPPClient creates an XMPP client with the given configuration -func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Client { - // Create TLS config based on certificate verification setting - tlsConfig := &tls.Config{ - InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, - } - - return xmppClient.NewClientWithTLS( - cfg.XMPPServerURL, - cfg.XMPPUsername, - cfg.XMPPPassword, - cfg.GetXMPPResource(), - "", // remoteID not needed for bridge user - tlsConfig, - b.logger, - ) +func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) model.BridgeUser { + return NewXMPPUser("_bridge_", "Bridge User", cfg.XMPPUsername, cfg, b.logger) } // UpdateConfiguration updates the bridge configuration @@ -97,9 +86,9 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error { return fmt.Errorf("XMPP server URL, username, and password are required when sync is enabled") } - b.xmppClient = b.createXMPPClient(cfg) + b.bridgeUser = b.createXMPPClient(cfg) } else { - b.xmppClient = nil + b.bridgeUser = nil } // Check if we need to restart the bridge due to configuration changes @@ -108,7 +97,7 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error { // Log the configuration change if needsRestart { - b.logger.LogInfo("Configuration changed, restarting bridge", "old_config", oldConfig, "new_config", cfg) + b.logger.LogInfo("Configuration changed, restarting bridge") } else { b.logger.LogInfo("Configuration updated", "config", cfg) } @@ -175,8 +164,8 @@ func (b *xmppBridge) Stop() error { b.cancel() } - if b.xmppClient != nil { - if err := b.xmppClient.Disconnect(); err != nil { + if b.bridgeUser != nil { + if err := b.bridgeUser.Disconnect(); err != nil { b.logger.LogWarn("Error disconnecting from XMPP server", "error", err) } } @@ -188,13 +177,13 @@ func (b *xmppBridge) Stop() error { // connectToXMPP establishes connection to the XMPP server func (b *xmppBridge) connectToXMPP() error { - if b.xmppClient == nil { + if b.bridgeUser == nil { return fmt.Errorf("XMPP client is not initialized") } b.logger.LogDebug("Connecting to XMPP server") - err := b.xmppClient.Connect() + err := b.bridgeUser.Connect() if err != nil { b.connected.Store(false) return fmt.Errorf("failed to connect to XMPP server: %w", err) @@ -204,7 +193,7 @@ func (b *xmppBridge) connectToXMPP() error { b.logger.LogInfo("Successfully connected to XMPP server") // Set online presence after successful connection - if err := b.xmppClient.SetOnlinePresence(); err != nil { + if err := b.bridgeUser.SetState(pluginModel.UserStateOnline); err != nil { b.logger.LogWarn("Failed to set online presence", "error", err) // Don't fail the connection for presence issues } else { @@ -247,7 +236,7 @@ func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error { return fmt.Errorf("not connected to XMPP server") } - err := b.xmppClient.JoinRoom(roomJID) + err := b.bridgeUser.JoinChannel(roomJID) if err != nil { return fmt.Errorf("failed to join XMPP room: %w", err) } @@ -309,7 +298,7 @@ func (b *xmppBridge) connectionMonitor() { case <-b.ctx.Done(): return case <-ticker.C: - if err := b.checkConnection(); err != nil { + if err := b.Ping(); err != nil { b.logger.LogWarn("XMPP connection check failed", "error", err) b.handleReconnection() } @@ -317,14 +306,6 @@ func (b *xmppBridge) connectionMonitor() { } } -// checkConnection verifies the XMPP connection is still active -func (b *xmppBridge) checkConnection() error { - if !b.connected.Load() { - return fmt.Errorf("not connected") - } - return b.xmppClient.Ping() -} - // handleReconnection attempts to reconnect to XMPP and rejoin rooms func (b *xmppBridge) handleReconnection() { b.configMu.RLock() @@ -338,8 +319,8 @@ func (b *xmppBridge) handleReconnection() { b.logger.LogInfo("Attempting to reconnect to XMPP server") b.connected.Store(false) - if b.xmppClient != nil { - b.xmppClient.Disconnect() + if b.bridgeUser != nil { + _ = b.bridgeUser.Disconnect() } // Retry connection with exponential backoff @@ -382,14 +363,14 @@ func (b *xmppBridge) Ping() error { return fmt.Errorf("XMPP bridge is not connected") } - if b.xmppClient == nil { + if b.bridgeUser == nil { return fmt.Errorf("XMPP client not initialized") } b.logger.LogDebug("Testing XMPP bridge connectivity with ping") - // Use the XMPP client's ping method - if err := b.xmppClient.Ping(); err != nil { + // Use the XMPP user's ping method + if err := b.bridgeUser.Ping(); err != nil { b.logger.LogWarn("XMPP bridge ping failed", "error", err) return fmt.Errorf("XMPP bridge ping failed: %w", err) } @@ -416,7 +397,7 @@ func (b *xmppBridge) CreateChannelMapping(channelID, roomJID string) error { // Join the room if connected if b.connected.Load() { - if err := b.xmppClient.JoinRoom(roomJID); err != nil { + if err := b.bridgeUser.JoinChannel(roomJID); err != nil { b.logger.LogWarn("Failed to join newly mapped room", "channel_id", channelID, "room_jid", roomJID, "error", err) } } @@ -482,8 +463,8 @@ func (b *xmppBridge) DeleteChannelMapping(channelID string) error { b.mappingsMu.Unlock() // Leave the room if connected - if b.connected.Load() && b.xmppClient != nil { - if err := b.xmppClient.LeaveRoom(roomJID); err != nil { + if b.connected.Load() && b.bridgeUser != nil { + if err := b.bridgeUser.LeaveChannel(roomJID); err != nil { b.logger.LogWarn("Failed to leave unmapped room", "channel_id", channelID, "room_jid", roomJID, "error", err) // Don't fail the entire operation if leaving the room fails } else { @@ -501,14 +482,14 @@ func (b *xmppBridge) RoomExists(roomID string) (bool, error) { return false, fmt.Errorf("not connected to XMPP server") } - if b.xmppClient == nil { + if b.bridgeUser == nil { return false, fmt.Errorf("XMPP client not initialized") } b.logger.LogDebug("Checking if XMPP room exists", "room_jid", roomID) - // Use the XMPP client to check room existence - exists, err := b.xmppClient.CheckRoomExists(roomID) + // Use the XMPP user to check room existence + exists, err := b.bridgeUser.CheckChannelExists(roomID) if err != nil { b.logger.LogError("Failed to check room existence", "room_jid", roomID, "error", err) return false, fmt.Errorf("failed to check room existence: %w", err) @@ -539,3 +520,8 @@ func (b *xmppBridge) GetRoomMapping(roomID string) (string, error) { return channelID, nil } + +// GetUserManager returns the user manager for this bridge +func (b *xmppBridge) GetUserManager() pluginModel.BridgeUserManager { + return b.userManager +} diff --git a/server/bridge/xmpp/user.go b/server/bridge/xmpp/user.go new file mode 100644 index 0000000..d8ef461 --- /dev/null +++ b/server/bridge/xmpp/user.go @@ -0,0 +1,336 @@ +package xmpp + +import ( + "context" + "crypto/tls" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" + xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" +) + +// XMPPUser represents an XMPP user that implements the BridgeUser interface +type XMPPUser struct { + // User identity + id string + displayName string + jid string + + // XMPP client + client *xmppClient.Client + + // State management + state model.UserState + stateMu sync.RWMutex + connected atomic.Bool + + // Configuration + config *config.Configuration + + // Goroutine lifecycle + ctx context.Context + cancel context.CancelFunc + + // Logger + logger logger.Logger +} + +// NewXMPPUser creates a new XMPP user +func NewXMPPUser(id, displayName, jid string, cfg *config.Configuration, logger logger.Logger) *XMPPUser { + ctx, cancel := context.WithCancel(context.Background()) + + // Create TLS config based on certificate verification setting + tlsConfig := &tls.Config{ + InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, + } + + // Create XMPP client for this user + client := xmppClient.NewClientWithTLS( + cfg.XMPPServerURL, + jid, + cfg.XMPPPassword, // This might need to be user-specific in the future + cfg.GetXMPPResource(), + id, // Use user ID as remote ID + tlsConfig, + logger, + ) + + return &XMPPUser{ + id: id, + displayName: displayName, + jid: jid, + client: client, + state: model.UserStateOffline, + config: cfg, + ctx: ctx, + cancel: cancel, + logger: logger, + } +} + +// Validation +func (u *XMPPUser) Validate() error { + if u.id == "" { + return fmt.Errorf("user ID cannot be empty") + } + if u.jid == "" { + return fmt.Errorf("JID cannot be empty") + } + if u.config == nil { + return fmt.Errorf("configuration cannot be nil") + } + if u.config.XMPPServerURL == "" { + return fmt.Errorf("XMPP server URL cannot be empty") + } + if u.client == nil { + return fmt.Errorf("XMPP client cannot be nil") + } + return nil +} + +// Identity (bridge-agnostic) +func (u *XMPPUser) GetID() string { + return u.id +} + +func (u *XMPPUser) GetDisplayName() string { + return u.displayName +} + +// State management +func (u *XMPPUser) GetState() model.UserState { + u.stateMu.RLock() + defer u.stateMu.RUnlock() + return u.state +} + +func (u *XMPPUser) SetState(state model.UserState) error { + u.stateMu.Lock() + defer u.stateMu.Unlock() + + u.logger.LogDebug("Changing XMPP user state", "user_id", u.id, "old_state", u.state, "new_state", state) + u.state = state + + // TODO: Send presence update to XMPP server based on state + // This would involve mapping UserState to XMPP presence types + + return nil +} + +// Channel operations +func (u *XMPPUser) JoinChannel(channelID string) error { + if !u.connected.Load() { + return fmt.Errorf("user %s is not connected", u.id) + } + + u.logger.LogDebug("XMPP user joining channel", "user_id", u.id, "channel_id", channelID) + + // For XMPP, channelID is the room JID + err := u.client.JoinRoom(channelID) + if err != nil { + return fmt.Errorf("failed to join XMPP room %s: %w", channelID, err) + } + + u.logger.LogInfo("XMPP user joined channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +func (u *XMPPUser) LeaveChannel(channelID string) error { + if !u.connected.Load() { + return fmt.Errorf("user %s is not connected", u.id) + } + + u.logger.LogDebug("XMPP user leaving channel", "user_id", u.id, "channel_id", channelID) + + // For XMPP, channelID is the room JID + err := u.client.LeaveRoom(channelID) + if err != nil { + return fmt.Errorf("failed to leave XMPP room %s: %w", channelID, err) + } + + u.logger.LogInfo("XMPP user left channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +func (u *XMPPUser) SendMessageToChannel(channelID, message string) error { + if !u.connected.Load() { + return fmt.Errorf("user %s is not connected", u.id) + } + + u.logger.LogDebug("XMPP user sending message to channel", "user_id", u.id, "channel_id", channelID) + + // Create message request for XMPP + req := xmppClient.MessageRequest{ + RoomJID: channelID, + GhostUserJID: u.jid, + Message: message, + } + + _, err := u.client.SendMessage(req) + if err != nil { + return fmt.Errorf("failed to send message to XMPP room %s: %w", channelID, err) + } + + u.logger.LogDebug("XMPP user sent message to channel", "user_id", u.id, "channel_id", channelID) + return nil +} + +// Connection lifecycle +func (u *XMPPUser) Connect() error { + u.logger.LogDebug("Connecting XMPP user", "user_id", u.id, "jid", u.jid) + + err := u.client.Connect() + if err != nil { + u.connected.Store(false) + return fmt.Errorf("failed to connect XMPP user %s: %w", u.id, err) + } + + u.connected.Store(true) + u.logger.LogInfo("XMPP user connected", "user_id", u.id, "jid", u.jid) + + // Set online presence after successful connection + if err := u.client.SetOnlinePresence(); err != nil { + u.logger.LogWarn("Failed to set online presence for XMPP user", "user_id", u.id, "error", err) + // Don't fail the connection for presence issues + } + + // Update state to online + _ = u.SetState(model.UserStateOnline) + + return nil +} + +func (u *XMPPUser) Disconnect() error { + u.logger.LogDebug("Disconnecting XMPP user", "user_id", u.id, "jid", u.jid) + + if u.client == nil { + return nil + } + + err := u.client.Disconnect() + if err != nil { + u.logger.LogWarn("Error disconnecting XMPP user", "user_id", u.id, "error", err) + } + + u.connected.Store(false) + _ = u.SetState(model.UserStateOffline) + + u.logger.LogInfo("XMPP user disconnected", "user_id", u.id, "jid", u.jid) + return err +} + +func (u *XMPPUser) IsConnected() bool { + return u.connected.Load() +} + +func (u *XMPPUser) Ping() error { + if !u.connected.Load() { + return fmt.Errorf("XMPP user %s is not connected", u.id) + } + + if u.client == nil { + return fmt.Errorf("XMPP client not initialized for user %s", u.id) + } + + return u.client.Ping() +} + +// CheckChannelExists checks if an XMPP room/channel exists +func (u *XMPPUser) CheckChannelExists(channelID string) (bool, error) { + if !u.connected.Load() { + return false, fmt.Errorf("XMPP user %s is not connected", u.id) + } + + if u.client == nil { + return false, fmt.Errorf("XMPP client not initialized for user %s", u.id) + } + + return u.client.CheckRoomExists(channelID) +} + +// Goroutine lifecycle +func (u *XMPPUser) Start(ctx context.Context) error { + u.logger.LogDebug("Starting XMPP user", "user_id", u.id, "jid", u.jid) + + // Update context + u.ctx = ctx + + // Connect to XMPP server + if err := u.Connect(); err != nil { + return fmt.Errorf("failed to start XMPP user %s: %w", u.id, err) + } + + // Start connection monitoring in a goroutine + go u.connectionMonitor() + + u.logger.LogInfo("XMPP user started", "user_id", u.id, "jid", u.jid) + return nil +} + +func (u *XMPPUser) Stop() error { + u.logger.LogDebug("Stopping XMPP user", "user_id", u.id, "jid", u.jid) + + // Cancel context to stop goroutines + if u.cancel != nil { + u.cancel() + } + + // Disconnect from XMPP server + if err := u.Disconnect(); err != nil { + u.logger.LogWarn("Error disconnecting XMPP user during stop", "user_id", u.id, "error", err) + } + + u.logger.LogInfo("XMPP user stopped", "user_id", u.id, "jid", u.jid) + return nil +} + +// connectionMonitor monitors the XMPP connection for this user +func (u *XMPPUser) connectionMonitor() { + u.logger.LogDebug("Starting connection monitor for XMPP user", "user_id", u.id) + + // Simple monitoring - check connection periodically + for { + select { + case <-u.ctx.Done(): + u.logger.LogDebug("Connection monitor stopped for XMPP user", "user_id", u.id) + return + default: + // Check connection every 30 seconds + if u.connected.Load() { + if err := u.client.Ping(); err != nil { + u.logger.LogWarn("Connection check failed for XMPP user", "user_id", u.id, "error", err) + u.connected.Store(false) + _ = u.SetState(model.UserStateOffline) + + // TODO: Implement reconnection logic if needed + } + } + + // Wait before next check + timeoutCtx, cancel := context.WithTimeout(u.ctx, 30 * time.Second) // 30 seconds + select { + case <-u.ctx.Done(): + cancel() + return + case <-timeoutCtx.Done(): + cancel() + continue + } + } + } +} + +// GetJID returns the XMPP JID for this user (XMPP-specific method) +func (u *XMPPUser) GetJID() string { + return u.jid +} + +// GetClient returns the underlying XMPP client (for advanced operations) +func (u *XMPPUser) GetClient() *xmppClient.Client { + return u.client +} \ No newline at end of file diff --git a/server/model/bridge.go b/server/model/bridge.go index 3a0559c..bd00672 100644 --- a/server/model/bridge.go +++ b/server/model/bridge.go @@ -1,6 +1,11 @@ package model -import "fmt" +import ( + "context" + "fmt" + + "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" +) type BridgeID string @@ -144,27 +149,59 @@ type Bridge interface { // Ping actively tests the bridge connection health by sending a lightweight request. Ping() error + + // GetUserManager returns the user manager for this bridge. + GetUserManager() BridgeUserManager } +// BridgeUser represents a user connected to any bridge service +type BridgeUser interface { + // Validation + Validate() error + + // Identity (bridge-agnostic) + GetID() string + GetDisplayName() string + + // State management + GetState() UserState + SetState(state UserState) error + + // Channel operations (abstracted from rooms/channels/groups) + JoinChannel(channelID string) error + LeaveChannel(channelID string) error + SendMessageToChannel(channelID, message string) error + + // Connection lifecycle + Connect() error + Disconnect() error + IsConnected() bool + Ping() error + + // Channel existence check + CheckChannelExists(channelID string) (bool, error) + + // Goroutine lifecycle + Start(ctx context.Context) error + Stop() error +} + +// BridgeUserManager manages users for a specific bridge type BridgeUserManager interface { - // CreateUser creates a new user in the bridge system. - CreateUser(userID string, userData any) error - - // GetUser retrieves user data for a given user ID. - GetUser(userID string) (any, error) - - // UpdateUser updates user data for a given user ID. - UpdateUser(userID string, userData any) error - - // DeleteUser removes a user from the bridge system. + // User lifecycle + CreateUser(user BridgeUser) error + GetUser(userID string) (BridgeUser, error) DeleteUser(userID string) error - - // ListUsers returns a list of all users in the bridge system. - ListUsers() ([]string, error) - - // HasUser checks if a user exists in the bridge system. + ListUsers() []BridgeUser HasUser(userID string) bool - // OnUserStateChange is called when a user's state changes (e.g., online, away, offline). - OnUserStateChange(userID string, state UserState) error + // Manager lifecycle + Start(ctx context.Context) error + Stop() error + + // Configuration updates + UpdateConfiguration(config *config.Configuration) error + + // Bridge type identification + GetBridgeType() string } diff --git a/server/plugin.go b/server/plugin.go index 439c04c..f7a5f81 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -83,7 +83,7 @@ func (p *Plugin) OnActivate() error { } // Initialize bridge manager - p.bridgeManager = bridge.NewManager(p.logger, p.API, p.remoteID) + p.bridgeManager = bridge.NewBridgeManager(p.logger, p.API, p.remoteID) // Initialize and register bridges with current configuration if err := p.initBridges(*cfg); err != nil { From 65038fb7a29eb2834ed53334dffdc09b6ded2a15 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 4 Aug 2025 18:04:10 +0200 Subject: [PATCH 10/10] feat: restore XMPP bridge to use direct client connection instead of bridge user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace bridgeUser with bridgeClient (*xmppClient.Client) in XMPP bridge - Update createXMPPClient to return XMPP client with TLS configuration - Migrate connection, disconnection, and room operations to use bridgeClient - Update Ping() and RoomExists() methods to use client methods directly - Maintain bridge-agnostic user management system for additional users - Fix formatting and import organization across bridge components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/xmpp-client-doctor/main.go | 10 ++-- server/bridge/mattermost/user.go | 10 ++-- server/bridge/xmpp/bridge.go | 72 +++++++++++++++++------------ server/bridge/xmpp/user.go | 78 ++++++++++++++++---------------- server/config/config.go | 16 +++---- server/logger/logger.go | 2 +- 6 files changed, 101 insertions(+), 87 deletions(-) diff --git a/cmd/xmpp-client-doctor/main.go b/cmd/xmpp-client-doctor/main.go index 10c0ed0..ac179e8 100644 --- a/cmd/xmpp-client-doctor/main.go +++ b/cmd/xmpp-client-doctor/main.go @@ -178,7 +178,7 @@ func testXMPPClient(config *Config) error { var mucDuration time.Duration var dmDuration time.Duration var roomExistsDuration time.Duration - + // Test MUC operations if requested if config.TestMUC { start = time.Now() @@ -286,7 +286,7 @@ func testMUCOperations(client *xmpp.Client, config *Config) error { return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err) } joinDuration := time.Since(start) - + var sendDuration time.Duration if config.Verbose { @@ -300,7 +300,7 @@ func testMUCOperations(client *xmpp.Client, config *Config) error { RoomJID: config.TestRoom, Message: testMessage, } - + start = time.Now() _, err = client.SendMessage(messageReq) if err != nil { @@ -352,7 +352,7 @@ func testDirectMessage(client *xmpp.Client, config *Config) error { // Send a test message to the admin user testMessage := fmt.Sprintf("Test direct message from XMPP doctor at %s", time.Now().Format("15:04:05")) adminJID := "admin@localhost" // Default admin user for development server - + start := time.Now() err := client.SendDirectMessage(adminJID, testMessage) if err != nil { @@ -447,4 +447,4 @@ func (l *SimpleLogger) LogWarn(msg string, args ...interface{}) { // LogError logs error messages func (l *SimpleLogger) LogError(msg string, args ...interface{}) { log.Printf("[ERROR] "+msg, args...) -} \ No newline at end of file +} diff --git a/server/bridge/mattermost/user.go b/server/bridge/mattermost/user.go index ba14ba9..8c4e0bd 100644 --- a/server/bridge/mattermost/user.go +++ b/server/bridge/mattermost/user.go @@ -194,13 +194,13 @@ func (u *MattermostUser) Ping() error { if u.api == nil { return fmt.Errorf("Mattermost API not initialized for user %s", u.id) } - + // Test API connectivity by getting server version version := u.api.GetServerVersion() if version == "" { return fmt.Errorf("Mattermost API ping returned empty server version for user %s", u.id) } - + return nil } @@ -209,7 +209,7 @@ func (u *MattermostUser) CheckChannelExists(channelID string) (bool, error) { if u.api == nil { return false, fmt.Errorf("Mattermost API not initialized for user %s", u.id) } - + // Try to get the channel by ID _, appErr := u.api.GetChannel(channelID) if appErr != nil { @@ -219,7 +219,7 @@ func (u *MattermostUser) CheckChannelExists(channelID string) (bool, error) { } return false, fmt.Errorf("failed to check channel existence: %w", appErr) } - + return true, nil } @@ -297,4 +297,4 @@ func (u *MattermostUser) GetEmail() string { // GetAPI returns the Mattermost API instance (for advanced operations) func (u *MattermostUser) GetAPI() plugin.API { return u.api -} \ No newline at end of file +} diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index 8ec1e0d..4a1ffe6 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -2,6 +2,7 @@ package xmpp import ( "context" + "crypto/tls" "sync" "sync/atomic" "time" @@ -11,19 +12,19 @@ import ( "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" - "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore" + xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" "github.com/mattermost/mattermost/server/public/plugin" ) // xmppBridge handles syncing messages between Mattermost and XMPP type xmppBridge struct { - logger logger.Logger - api plugin.API - kvstore kvstore.KVStore - bridgeUser model.BridgeUser // Handles the bridge user and main bridge XMPP connection - userManager pluginModel.BridgeUserManager + logger logger.Logger + api plugin.API + kvstore kvstore.KVStore + bridgeClient *xmppClient.Client // Main bridge XMPP client connection + userManager pluginModel.BridgeUserManager // Connection management connected atomic.Bool @@ -55,15 +56,28 @@ func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg * // Initialize XMPP client with configuration if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" { - b.bridgeUser = b.createXMPPClient(cfg) + b.bridgeClient = b.createXMPPClient(cfg) } return b } // createXMPPClient creates an XMPP client with the given configuration -func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) model.BridgeUser { - return NewXMPPUser("_bridge_", "Bridge User", cfg.XMPPUsername, cfg, b.logger) +func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Client { + // Create TLS config based on certificate verification setting + tlsConfig := &tls.Config{ + InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, + } + + return xmppClient.NewClientWithTLS( + cfg.XMPPServerURL, + cfg.XMPPUsername, + cfg.XMPPPassword, + cfg.GetXMPPResource(), + "", // remoteID not needed for bridge client + tlsConfig, + b.logger, + ) } // UpdateConfiguration updates the bridge configuration @@ -86,9 +100,9 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error { return fmt.Errorf("XMPP server URL, username, and password are required when sync is enabled") } - b.bridgeUser = b.createXMPPClient(cfg) + b.bridgeClient = b.createXMPPClient(cfg) } else { - b.bridgeUser = nil + b.bridgeClient = nil } // Check if we need to restart the bridge due to configuration changes @@ -164,8 +178,8 @@ func (b *xmppBridge) Stop() error { b.cancel() } - if b.bridgeUser != nil { - if err := b.bridgeUser.Disconnect(); err != nil { + if b.bridgeClient != nil { + if err := b.bridgeClient.Disconnect(); err != nil { b.logger.LogWarn("Error disconnecting from XMPP server", "error", err) } } @@ -177,13 +191,13 @@ func (b *xmppBridge) Stop() error { // connectToXMPP establishes connection to the XMPP server func (b *xmppBridge) connectToXMPP() error { - if b.bridgeUser == nil { + if b.bridgeClient == nil { return fmt.Errorf("XMPP client is not initialized") } b.logger.LogDebug("Connecting to XMPP server") - err := b.bridgeUser.Connect() + err := b.bridgeClient.Connect() if err != nil { b.connected.Store(false) return fmt.Errorf("failed to connect to XMPP server: %w", err) @@ -193,11 +207,11 @@ func (b *xmppBridge) connectToXMPP() error { b.logger.LogInfo("Successfully connected to XMPP server") // Set online presence after successful connection - if err := b.bridgeUser.SetState(pluginModel.UserStateOnline); err != nil { + if err := b.bridgeClient.SetOnlinePresence(); err != nil { b.logger.LogWarn("Failed to set online presence", "error", err) // Don't fail the connection for presence issues } else { - b.logger.LogDebug("Set bridge user online presence") + b.logger.LogDebug("Set bridge client online presence") } return nil @@ -236,7 +250,7 @@ func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error { return fmt.Errorf("not connected to XMPP server") } - err := b.bridgeUser.JoinChannel(roomJID) + err := b.bridgeClient.JoinRoom(roomJID) if err != nil { return fmt.Errorf("failed to join XMPP room: %w", err) } @@ -319,8 +333,8 @@ func (b *xmppBridge) handleReconnection() { b.logger.LogInfo("Attempting to reconnect to XMPP server") b.connected.Store(false) - if b.bridgeUser != nil { - _ = b.bridgeUser.Disconnect() + if b.bridgeClient != nil { + _ = b.bridgeClient.Disconnect() } // Retry connection with exponential backoff @@ -363,14 +377,14 @@ func (b *xmppBridge) Ping() error { return fmt.Errorf("XMPP bridge is not connected") } - if b.bridgeUser == nil { + if b.bridgeClient == nil { return fmt.Errorf("XMPP client not initialized") } b.logger.LogDebug("Testing XMPP bridge connectivity with ping") - // Use the XMPP user's ping method - if err := b.bridgeUser.Ping(); err != nil { + // Use the XMPP client's ping method + if err := b.bridgeClient.Ping(); err != nil { b.logger.LogWarn("XMPP bridge ping failed", "error", err) return fmt.Errorf("XMPP bridge ping failed: %w", err) } @@ -397,7 +411,7 @@ func (b *xmppBridge) CreateChannelMapping(channelID, roomJID string) error { // Join the room if connected if b.connected.Load() { - if err := b.bridgeUser.JoinChannel(roomJID); err != nil { + if err := b.bridgeClient.JoinRoom(roomJID); err != nil { b.logger.LogWarn("Failed to join newly mapped room", "channel_id", channelID, "room_jid", roomJID, "error", err) } } @@ -463,8 +477,8 @@ func (b *xmppBridge) DeleteChannelMapping(channelID string) error { b.mappingsMu.Unlock() // Leave the room if connected - if b.connected.Load() && b.bridgeUser != nil { - if err := b.bridgeUser.LeaveChannel(roomJID); err != nil { + if b.connected.Load() && b.bridgeClient != nil { + if err := b.bridgeClient.LeaveRoom(roomJID); err != nil { b.logger.LogWarn("Failed to leave unmapped room", "channel_id", channelID, "room_jid", roomJID, "error", err) // Don't fail the entire operation if leaving the room fails } else { @@ -482,14 +496,14 @@ func (b *xmppBridge) RoomExists(roomID string) (bool, error) { return false, fmt.Errorf("not connected to XMPP server") } - if b.bridgeUser == nil { + if b.bridgeClient == nil { return false, fmt.Errorf("XMPP client not initialized") } b.logger.LogDebug("Checking if XMPP room exists", "room_jid", roomID) - // Use the XMPP user to check room existence - exists, err := b.bridgeUser.CheckChannelExists(roomID) + // Use the XMPP client to check room existence + exists, err := b.bridgeClient.CheckRoomExists(roomID) if err != nil { b.logger.LogError("Failed to check room existence", "room_jid", roomID, "error", err) return false, fmt.Errorf("failed to check room existence: %w", err) diff --git a/server/bridge/xmpp/user.go b/server/bridge/xmpp/user.go index d8ef461..fd03afa 100644 --- a/server/bridge/xmpp/user.go +++ b/server/bridge/xmpp/user.go @@ -43,7 +43,7 @@ type XMPPUser struct { // NewXMPPUser creates a new XMPP user func NewXMPPUser(id, displayName, jid string, cfg *config.Configuration, logger logger.Logger) *XMPPUser { ctx, cancel := context.WithCancel(context.Background()) - + // Create TLS config based on certificate verification setting tlsConfig := &tls.Config{ InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, @@ -112,13 +112,13 @@ func (u *XMPPUser) GetState() model.UserState { func (u *XMPPUser) SetState(state model.UserState) error { u.stateMu.Lock() defer u.stateMu.Unlock() - + u.logger.LogDebug("Changing XMPP user state", "user_id", u.id, "old_state", u.state, "new_state", state) u.state = state - + // TODO: Send presence update to XMPP server based on state // This would involve mapping UserState to XMPP presence types - + return nil } @@ -127,15 +127,15 @@ func (u *XMPPUser) JoinChannel(channelID string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } - + u.logger.LogDebug("XMPP user joining channel", "user_id", u.id, "channel_id", channelID) - + // For XMPP, channelID is the room JID err := u.client.JoinRoom(channelID) if err != nil { return fmt.Errorf("failed to join XMPP room %s: %w", channelID, err) } - + u.logger.LogInfo("XMPP user joined channel", "user_id", u.id, "channel_id", channelID) return nil } @@ -144,15 +144,15 @@ func (u *XMPPUser) LeaveChannel(channelID string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } - + u.logger.LogDebug("XMPP user leaving channel", "user_id", u.id, "channel_id", channelID) - + // For XMPP, channelID is the room JID err := u.client.LeaveRoom(channelID) if err != nil { return fmt.Errorf("failed to leave XMPP room %s: %w", channelID, err) } - + u.logger.LogInfo("XMPP user left channel", "user_id", u.id, "channel_id", channelID) return nil } @@ -161,21 +161,21 @@ func (u *XMPPUser) SendMessageToChannel(channelID, message string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } - + u.logger.LogDebug("XMPP user sending message to channel", "user_id", u.id, "channel_id", channelID) - + // Create message request for XMPP req := xmppClient.MessageRequest{ RoomJID: channelID, GhostUserJID: u.jid, Message: message, } - + _, err := u.client.SendMessage(req) if err != nil { return fmt.Errorf("failed to send message to XMPP room %s: %w", channelID, err) } - + u.logger.LogDebug("XMPP user sent message to channel", "user_id", u.id, "channel_id", channelID) return nil } @@ -183,43 +183,43 @@ func (u *XMPPUser) SendMessageToChannel(channelID, message string) error { // Connection lifecycle func (u *XMPPUser) Connect() error { u.logger.LogDebug("Connecting XMPP user", "user_id", u.id, "jid", u.jid) - + err := u.client.Connect() if err != nil { u.connected.Store(false) return fmt.Errorf("failed to connect XMPP user %s: %w", u.id, err) } - + u.connected.Store(true) u.logger.LogInfo("XMPP user connected", "user_id", u.id, "jid", u.jid) - + // Set online presence after successful connection if err := u.client.SetOnlinePresence(); err != nil { u.logger.LogWarn("Failed to set online presence for XMPP user", "user_id", u.id, "error", err) // Don't fail the connection for presence issues } - + // Update state to online _ = u.SetState(model.UserStateOnline) - + return nil } func (u *XMPPUser) Disconnect() error { u.logger.LogDebug("Disconnecting XMPP user", "user_id", u.id, "jid", u.jid) - + if u.client == nil { return nil } - + err := u.client.Disconnect() if err != nil { u.logger.LogWarn("Error disconnecting XMPP user", "user_id", u.id, "error", err) } - + u.connected.Store(false) _ = u.SetState(model.UserStateOffline) - + u.logger.LogInfo("XMPP user disconnected", "user_id", u.id, "jid", u.jid) return err } @@ -232,11 +232,11 @@ func (u *XMPPUser) Ping() error { if !u.connected.Load() { return fmt.Errorf("XMPP user %s is not connected", u.id) } - + if u.client == nil { return fmt.Errorf("XMPP client not initialized for user %s", u.id) } - + return u.client.Ping() } @@ -245,46 +245,46 @@ func (u *XMPPUser) CheckChannelExists(channelID string) (bool, error) { if !u.connected.Load() { return false, fmt.Errorf("XMPP user %s is not connected", u.id) } - + if u.client == nil { return false, fmt.Errorf("XMPP client not initialized for user %s", u.id) } - + return u.client.CheckRoomExists(channelID) } // Goroutine lifecycle func (u *XMPPUser) Start(ctx context.Context) error { u.logger.LogDebug("Starting XMPP user", "user_id", u.id, "jid", u.jid) - + // Update context u.ctx = ctx - + // Connect to XMPP server if err := u.Connect(); err != nil { return fmt.Errorf("failed to start XMPP user %s: %w", u.id, err) } - + // Start connection monitoring in a goroutine go u.connectionMonitor() - + u.logger.LogInfo("XMPP user started", "user_id", u.id, "jid", u.jid) return nil } func (u *XMPPUser) Stop() error { u.logger.LogDebug("Stopping XMPP user", "user_id", u.id, "jid", u.jid) - + // Cancel context to stop goroutines if u.cancel != nil { u.cancel() } - + // Disconnect from XMPP server if err := u.Disconnect(); err != nil { u.logger.LogWarn("Error disconnecting XMPP user during stop", "user_id", u.id, "error", err) } - + u.logger.LogInfo("XMPP user stopped", "user_id", u.id, "jid", u.jid) return nil } @@ -292,7 +292,7 @@ func (u *XMPPUser) Stop() error { // connectionMonitor monitors the XMPP connection for this user func (u *XMPPUser) connectionMonitor() { u.logger.LogDebug("Starting connection monitor for XMPP user", "user_id", u.id) - + // Simple monitoring - check connection periodically for { select { @@ -306,13 +306,13 @@ func (u *XMPPUser) connectionMonitor() { u.logger.LogWarn("Connection check failed for XMPP user", "user_id", u.id, "error", err) u.connected.Store(false) _ = u.SetState(model.UserStateOffline) - + // TODO: Implement reconnection logic if needed } } - + // Wait before next check - timeoutCtx, cancel := context.WithTimeout(u.ctx, 30 * time.Second) // 30 seconds + timeoutCtx, cancel := context.WithTimeout(u.ctx, 30*time.Second) // 30 seconds select { case <-u.ctx.Done(): cancel() @@ -333,4 +333,4 @@ func (u *XMPPUser) GetJID() string { // GetClient returns the underlying XMPP client (for advanced operations) func (u *XMPPUser) GetClient() *xmppClient.Client { return u.client -} \ No newline at end of file +} diff --git a/server/config/config.go b/server/config/config.go index 824d2bc..db8c497 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -19,13 +19,13 @@ const DefaultXMPPUsernamePrefix = "xmpp" // If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep // copy appropriate for your types. type Configuration struct { - XMPPServerURL string `json:"XMPPServerURL"` - XMPPUsername string `json:"XMPPUsername"` - XMPPPassword string `json:"XMPPPassword"` - EnableSync bool `json:"EnableSync"` - XMPPUsernamePrefix string `json:"XMPPUsernamePrefix"` - XMPPResource string `json:"XMPPResource"` - XMPPInsecureSkipVerify bool `json:"XMPPInsecureSkipVerify"` + XMPPServerURL string `json:"XMPPServerURL"` + XMPPUsername string `json:"XMPPUsername"` + XMPPPassword string `json:"XMPPPassword"` + EnableSync bool `json:"EnableSync"` + XMPPUsernamePrefix string `json:"XMPPUsernamePrefix"` + XMPPResource string `json:"XMPPResource"` + XMPPInsecureSkipVerify bool `json:"XMPPInsecureSkipVerify"` } // Equals compares two configuration structs @@ -95,4 +95,4 @@ func (c *Configuration) IsValid() error { } return nil -} \ No newline at end of file +} diff --git a/server/logger/logger.go b/server/logger/logger.go index 25b4743..9b5b9dc 100644 --- a/server/logger/logger.go +++ b/server/logger/logger.go @@ -38,4 +38,4 @@ func (l *PluginAPILogger) LogWarn(message string, keyValuePairs ...any) { // LogError logs an error message func (l *PluginAPILogger) LogError(message string, keyValuePairs ...any) { l.api.LogError(message, keyValuePairs...) -} \ No newline at end of file +}