Compare commits
10 commits
43f0fb1892
...
65038fb7a2
Author | SHA1 | Date | |
---|---|---|---|
65038fb7a2 | |||
db8037ffbf | |||
ea1711e94c | |||
35174c61a2 | |||
a95ca8fb76 | |||
1f45197aa8 | |||
a5eb80817c | |||
2e13d96dce | |||
5d143808a3 | |||
5551a8bc8d |
20 changed files with 3480 additions and 214 deletions
102
.golangci.yml
102
.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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -27,6 +27,8 @@ type Config struct {
|
|||
Resource string
|
||||
TestRoom string
|
||||
TestMUC bool
|
||||
TestDirectMessage bool
|
||||
TestRoomExists bool
|
||||
Verbose bool
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
@ -41,20 +43,23 @@ 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.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,\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")
|
||||
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 +80,12 @@ func main() {
|
|||
if config.TestMUC {
|
||||
log.Printf(" Test Room: %s", config.TestRoom)
|
||||
}
|
||||
if config.TestDirectMessage {
|
||||
log.Printf(" Test Direct Messages: enabled")
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
log.Printf(" Test Room Existence: enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the XMPP client
|
||||
|
@ -89,6 +100,12 @@ func main() {
|
|||
if config.TestMUC {
|
||||
fmt.Println("✅ XMPP MUC operations test passed!")
|
||||
}
|
||||
if config.TestDirectMessage {
|
||||
fmt.Println("✅ XMPP direct message test passed!")
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
fmt.Println("✅ XMPP room existence test passed!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,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 {
|
||||
|
@ -113,6 +133,7 @@ func testXMPPClient(config *Config) error {
|
|||
config.Resource,
|
||||
"doctor-remote-id",
|
||||
tlsConfig,
|
||||
doctorLogger,
|
||||
)
|
||||
} else {
|
||||
client = xmpp.NewClient(
|
||||
|
@ -121,6 +142,7 @@ func testXMPPClient(config *Config) error {
|
|||
config.Password,
|
||||
config.Resource,
|
||||
"doctor-remote-id",
|
||||
doctorLogger,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -143,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)
|
||||
}
|
||||
|
@ -154,7 +176,9 @@ 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()
|
||||
|
@ -165,6 +189,26 @@ 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)
|
||||
}
|
||||
|
||||
// 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...")
|
||||
}
|
||||
|
@ -185,11 +229,23 @@ 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)
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
log.Printf(" Room existence check time: %v", roomExistsDuration)
|
||||
}
|
||||
log.Printf(" Disconnect time: %v", disconnectDuration)
|
||||
totalTime := connectDuration + pingDuration + disconnectDuration
|
||||
if config.TestMUC {
|
||||
totalTime += mucDuration
|
||||
}
|
||||
if config.TestDirectMessage {
|
||||
totalTime += dmDuration
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
totalTime += roomExistsDuration
|
||||
}
|
||||
log.Printf(" Total time: %v", totalTime)
|
||||
}
|
||||
|
||||
|
@ -199,17 +255,38 @@ 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)
|
||||
}
|
||||
joinDuration := time.Since(start)
|
||||
|
||||
|
||||
var sendDuration time.Duration
|
||||
|
||||
if config.Verbose {
|
||||
|
@ -223,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 {
|
||||
|
@ -255,11 +332,84 @@ 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
|
||||
}
|
||||
|
||||
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 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
|
||||
|
@ -270,4 +420,31 @@ func maskPassword(password string) string {
|
|||
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...)
|
||||
}
|
||||
|
|
173
go.mod
173
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
|
||||
)
|
||||
|
|
44
plugin.json
44
plugin.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"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",
|
||||
"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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,29 +6,38 @@ 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
|
||||
// BridgeManager manages multiple bridge instances
|
||||
type BridgeManager struct {
|
||||
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 {
|
||||
// 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")
|
||||
}
|
||||
if api == nil {
|
||||
panic("plugin API cannot be nil")
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
bridges: make(map[string]model.Bridge),
|
||||
logger: logger,
|
||||
return &BridgeManager{
|
||||
bridges: make(map[string]model.Bridge),
|
||||
logger: logger,
|
||||
api: api,
|
||||
remoteID: remoteID,
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
@ -50,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()
|
||||
|
@ -71,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()
|
||||
|
@ -92,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()
|
||||
|
||||
|
@ -115,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()
|
||||
|
||||
|
@ -128,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()
|
||||
|
||||
|
@ -141,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()
|
||||
|
||||
|
@ -150,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()
|
||||
|
||||
|
@ -158,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()
|
||||
|
||||
|
@ -187,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()
|
||||
|
||||
|
@ -213,4 +222,185 @@ func (m *Manager) OnPluginConfigurationChange(config any) error {
|
|||
|
||||
m.logger.LogInfo("Configuration changes propagated to all bridges")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateChannelMapping handles the creation of a channel mapping by calling the appropriate bridge
|
||||
func (m *BridgeManager) CreateChannelMapping(req model.CreateChannelMappingRequest) 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", 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(req.BridgeName)
|
||||
if err != nil {
|
||||
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", 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)
|
||||
return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", req.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(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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// DeleteChannepMapping handles the deletion of a channel mapping by calling the appropriate bridges
|
||||
func (m *BridgeManager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) 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", req.ChannelID, "bridge_name", req.BridgeName, "user_id", req.UserID, "team_id", req.TeamID)
|
||||
|
||||
// Get the specific bridge
|
||||
bridge, err := m.GetBridge(req.BridgeName)
|
||||
if err != nil {
|
||||
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", req.BridgeName)
|
||||
}
|
||||
|
||||
// Delete the channel mapping from the specific bridge
|
||||
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
|
||||
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(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)
|
||||
}
|
||||
|
||||
// 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 *BridgeManager) shareChannel(req model.CreateChannelMappingRequest) 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 *BridgeManager) 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
|
||||
}
|
||||
|
|
338
server/bridge/mattermost/bridge.go
Normal file
338
server/bridge/mattermost/bridge.go
Normal file
|
@ -0,0 +1,338 @@
|
|||
package mattermost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
"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
|
||||
userManager pluginModel.BridgeUserManager
|
||||
|
||||
// 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,
|
||||
userManager: bridge.NewUserManager("mattermost", log),
|
||||
}
|
||||
|
||||
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()
|
||||
b.config = cfg
|
||||
b.configMu.Unlock()
|
||||
|
||||
// Log the configuration change
|
||||
b.logger.LogInfo("Mattermost bridge configuration updated")
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetUserManager returns the user manager for this bridge
|
||||
func (b *mattermostBridge) GetUserManager() pluginModel.BridgeUserManager {
|
||||
return b.userManager
|
||||
}
|
300
server/bridge/mattermost/user.go
Normal file
300
server/bridge/mattermost/user.go
Normal file
|
@ -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
|
||||
}
|
188
server/bridge/user.go
Normal file
188
server/bridge/user.go
Normal file
|
@ -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
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"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"
|
||||
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
||||
|
@ -19,10 +20,11 @@ import (
|
|||
|
||||
// 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
|
||||
bridgeClient *xmppClient.Client // Main bridge XMPP client connection
|
||||
userManager pluginModel.BridgeUserManager
|
||||
|
||||
// Connection management
|
||||
connected atomic.Bool
|
||||
|
@ -41,7 +43,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,14 +51,15 @@ 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.bridgeClient = b.createXMPPClient(cfg)
|
||||
}
|
||||
|
||||
return bridge
|
||||
return b
|
||||
}
|
||||
|
||||
// createXMPPClient creates an XMPP client with the given configuration
|
||||
|
@ -71,8 +74,9 @@ func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Cli
|
|||
cfg.XMPPUsername,
|
||||
cfg.XMPPPassword,
|
||||
cfg.GetXMPPResource(),
|
||||
"", // remoteID not needed for bridge user
|
||||
"", // remoteID not needed for bridge client
|
||||
tlsConfig,
|
||||
b.logger,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -86,28 +90,28 @@ 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")
|
||||
}
|
||||
|
||||
b.xmppClient = b.createXMPPClient(cfg)
|
||||
b.bridgeClient = b.createXMPPClient(cfg)
|
||||
} else {
|
||||
b.xmppClient = nil
|
||||
b.bridgeClient = 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
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -142,9 +146,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
|
||||
|
@ -177,8 +178,8 @@ func (b *xmppBridge) Stop() error {
|
|||
b.cancel()
|
||||
}
|
||||
|
||||
if b.xmppClient != nil {
|
||||
if err := b.xmppClient.Disconnect(); err != nil {
|
||||
if b.bridgeClient != nil {
|
||||
if err := b.bridgeClient.Disconnect(); err != nil {
|
||||
b.logger.LogWarn("Error disconnecting from XMPP server", "error", err)
|
||||
}
|
||||
}
|
||||
|
@ -190,13 +191,13 @@ func (b *xmppBridge) Stop() error {
|
|||
|
||||
// connectToXMPP establishes connection to the XMPP server
|
||||
func (b *xmppBridge) connectToXMPP() error {
|
||||
if b.xmppClient == nil {
|
||||
if b.bridgeClient == nil {
|
||||
return fmt.Errorf("XMPP client is not initialized")
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Connecting to XMPP server")
|
||||
|
||||
err := b.xmppClient.Connect()
|
||||
err := b.bridgeClient.Connect()
|
||||
if err != nil {
|
||||
b.connected.Store(false)
|
||||
return fmt.Errorf("failed to connect to XMPP server: %w", err)
|
||||
|
@ -206,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.xmppClient.SetOnlinePresence(); 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
|
||||
|
@ -249,7 +250,7 @@ func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error {
|
|||
return fmt.Errorf("not connected to XMPP server")
|
||||
}
|
||||
|
||||
err := b.xmppClient.JoinRoom(roomJID)
|
||||
err := b.bridgeClient.JoinRoom(roomJID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to join XMPP room: %w", err)
|
||||
}
|
||||
|
@ -311,7 +312,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()
|
||||
}
|
||||
|
@ -319,14 +320,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.TestConnection()
|
||||
}
|
||||
|
||||
// handleReconnection attempts to reconnect to XMPP and rejoin rooms
|
||||
func (b *xmppBridge) handleReconnection() {
|
||||
b.configMu.RLock()
|
||||
|
@ -340,8 +333,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.bridgeClient != nil {
|
||||
_ = b.bridgeClient.Disconnect()
|
||||
}
|
||||
|
||||
// Retry connection with exponential backoff
|
||||
|
@ -378,19 +371,35 @@ 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 {
|
||||
// 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.bridgeClient == 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.bridgeClient.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 {
|
||||
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)
|
||||
}
|
||||
|
@ -402,7 +411,7 @@ func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error {
|
|||
|
||||
// Join the room if connected
|
||||
if b.connected.Load() {
|
||||
if err := b.xmppClient.JoinRoom(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)
|
||||
}
|
||||
}
|
||||
|
@ -411,8 +420,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 +436,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
|
||||
}
|
||||
|
@ -441,3 +450,92 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
|||
|
||||
return roomJID, nil
|
||||
}
|
||||
|
||||
// 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.GetChannelMapping(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")
|
||||
}
|
||||
|
||||
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.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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.bridgeClient == 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.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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetUserManager returns the user manager for this bridge
|
||||
func (b *xmppBridge) GetUserManager() pluginModel.BridgeUserManager {
|
||||
return b.userManager
|
||||
}
|
||||
|
|
336
server/bridge/xmpp/user.go
Normal file
336
server/bridge/xmpp/user.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
@ -51,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:
|
||||
|
@ -72,6 +83,7 @@ func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.Comma
|
|||
|
||||
**Available commands:**
|
||||
- ` + "`/xmppbridge map <room_jid>`" + ` - Map current channel to XMPP room
|
||||
- ` + "`/xmppbridge unmap`" + ` - Unmap current channel from XMPP room
|
||||
- ` + "`/xmppbridge status`" + ` - Show bridge connection status
|
||||
|
||||
**Example:**
|
||||
|
@ -83,6 +95,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:
|
||||
|
@ -112,7 +126,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{
|
||||
|
@ -130,7 +144,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,
|
||||
|
@ -145,21 +159,73 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
|||
}
|
||||
}
|
||||
|
||||
// Create the mapping
|
||||
err = bridge.CreateChannelRoomMapping(channelID, roomJID)
|
||||
// Create the mapping using BridgeManager
|
||||
mappingReq := pluginModel.CreateChannelMappingRequest{
|
||||
ChannelID: channelID,
|
||||
BridgeName: "xmpp",
|
||||
BridgeRoomID: roomJID,
|
||||
UserID: args.UserId,
|
||||
TeamID: args.TeamId,
|
||||
}
|
||||
|
||||
err = c.bridgeManager.CreateChannelMapping(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{
|
||||
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 to check existing mappings
|
||||
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.GetChannelMapping(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
|
||||
deleteReq := pluginModel.DeleteChannelMappingRequest{
|
||||
ChannelID: channelID,
|
||||
BridgeName: "xmpp",
|
||||
UserID: args.UserId,
|
||||
TeamID: args.TeamId,
|
||||
}
|
||||
|
||||
err = c.bridgeManager.DeleteChannepMapping(deleteReq)
|
||||
if err != nil {
|
||||
return c.formatMappingError("delete", roomJID, 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")
|
||||
|
@ -181,7 +247,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 {
|
||||
|
@ -201,6 +267,89 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe
|
|||
%s
|
||||
|
||||
**Commands:**
|
||||
- Use `+"`/xmppbridge map <room_jid>`"+` to map this channel to an XMPP room`, statusText, mappingText),
|
||||
- Use `+"`/xmppbridge map <room_jid>`"+` to map this channel to an XMPP room
|
||||
- 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
46
server/hooks_sharedchannels.go
Normal file
46
server/hooks_sharedchannels.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,77 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
||||
)
|
||||
|
||||
type BridgeID string
|
||||
|
||||
type UserState int
|
||||
|
||||
const (
|
||||
UserStateOnline UserState = iota
|
||||
UserStateAway
|
||||
UserStateBusy
|
||||
UserStateOffline
|
||||
)
|
||||
|
||||
// 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)
|
||||
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 CreateChannelMappingRequest) 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
|
||||
}
|
||||
|
||||
// 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
|
||||
TeamID string // Team ID where the channel belongs
|
||||
}
|
||||
|
||||
// Validate checks if all required fields are present and valid
|
||||
func (r DeleteChannelMappingRequest) 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.
|
||||
|
@ -39,6 +111,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
|
||||
|
||||
// CreateChannelMapping is called when a channel mapping is created.
|
||||
CreateChannelMapping(req CreateChannelMappingRequest) error
|
||||
|
||||
// DeleteChannepMapping is called when a channel mapping is deleted.
|
||||
DeleteChannepMapping(req DeleteChannelMappingRequest) error
|
||||
}
|
||||
|
||||
type Bridge interface {
|
||||
|
@ -51,12 +129,79 @@ 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)
|
||||
|
||||
// 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
|
||||
|
||||
// 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 {
|
||||
// User lifecycle
|
||||
CreateUser(user BridgeUser) error
|
||||
GetUser(userID string) (BridgeUser, error)
|
||||
DeleteUser(userID string) error
|
||||
ListUsers() []BridgeUser
|
||||
HasUser(userID string) bool
|
||||
|
||||
// Manager lifecycle
|
||||
Start(ctx context.Context) error
|
||||
Stop() error
|
||||
|
||||
// Configuration updates
|
||||
UpdateConfiguration(config *config.Configuration) error
|
||||
|
||||
// Bridge type identification
|
||||
GetBridgeType() string
|
||||
}
|
||||
|
|
40
server/model/strings.go
Normal file
40
server/model/strings.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,8 +76,14 @@ 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)
|
||||
p.bridgeManager = bridge.NewBridgeManager(p.logger, p.API, p.remoteID)
|
||||
|
||||
// Initialize and register bridges with current configuration
|
||||
if err := p.initBridges(*cfg); err != nil {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -138,24 +153,69 @@ func (p *Plugin) initXMPPClient() {
|
|||
cfg.XMPPPassword,
|
||||
cfg.GetXMPPResource(),
|
||||
p.remoteID,
|
||||
p.logger,
|
||||
)
|
||||
}
|
||||
|
||||
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: true,
|
||||
}
|
||||
|
||||
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/
|
||||
|
|
|
@ -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"
|
||||
|
@ -22,18 +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
|
||||
}
|
||||
|
||||
|
@ -52,6 +55,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"`
|
||||
|
@ -65,17 +83,18 @@ 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))
|
||||
|
||||
|
||||
return &Client{
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
resource: resource,
|
||||
remoteID: remoteID,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
mucClient: mucClient,
|
||||
|
@ -85,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
|
||||
}
|
||||
|
@ -164,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
|
||||
|
@ -202,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 {
|
||||
|
@ -251,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() {
|
||||
|
@ -324,26 +326,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 +347,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
|
||||
|
@ -396,3 +417,121 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
Reference in a new issue