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:
|
version: "2"
|
||||||
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
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- errcheck
|
- errcheck
|
||||||
- gocritic
|
- gocritic
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- revive
|
- revive
|
||||||
- staticcheck
|
- staticcheck # Now includes gosimple and stylecheck
|
||||||
- stylecheck
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- whitespace
|
- 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:
|
issues:
|
||||||
exclude-rules:
|
max-issues-per-linter: 0
|
||||||
- path: server/configuration.go
|
max-same-issues: 0
|
||||||
linters:
|
fix: false
|
||||||
- unused
|
|
||||||
- path: _test\.go
|
|
||||||
linters:
|
|
||||||
- bodyclose
|
|
||||||
- scopelint # https://github.com/kyoh86/scopelint/issues/4
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
# Testing and Quality Assurance
|
# Testing and Quality Assurance
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
|
|
||||||
|
GOLANGCI_LINT_BINARY = ./build/bin/golangci-lint
|
||||||
|
GOTESTSUM_BINARY = ./build/bin/gotestsum
|
||||||
|
|
||||||
## Install go tools
|
## Install go tools
|
||||||
install-go-tools:
|
install-go-tools:
|
||||||
@echo Installing go tools
|
@echo "Installing development tools..."
|
||||||
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
|
@pluginctl tools install --bin-dir ./build/bin
|
||||||
$(GO) install gotest.tools/gotestsum@v1.7.0
|
|
||||||
|
|
||||||
## Runs eslint and golangci-lint
|
## Runs eslint and golangci-lint
|
||||||
.PHONY: check-style
|
.PHONY: check-style
|
||||||
|
@ -24,14 +26,14 @@ endif
|
||||||
ifneq ($(HAS_SERVER),)
|
ifneq ($(HAS_SERVER),)
|
||||||
@echo Running golangci-lint
|
@echo Running golangci-lint
|
||||||
$(GO) vet ./...
|
$(GO) vet ./...
|
||||||
$(GOBIN)/golangci-lint run ./...
|
$(GOLANGCI_LINT_BINARY) run ./...
|
||||||
endif
|
endif
|
||||||
|
|
||||||
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: apply webapp/node_modules install-go-tools
|
test: apply webapp/node_modules install-go-tools
|
||||||
ifneq ($(HAS_SERVER),)
|
ifneq ($(HAS_SERVER),)
|
||||||
$(GOBIN)/gotestsum -- -v ./...
|
$(GOTESTSUM_BINARY) -- -v ./...
|
||||||
endif
|
endif
|
||||||
ifneq ($(HAS_WEBAPP),)
|
ifneq ($(HAS_WEBAPP),)
|
||||||
cd webapp && $(NPM) run test;
|
cd webapp && $(NPM) run test;
|
||||||
|
@ -42,7 +44,7 @@ endif
|
||||||
.PHONY: test-ci
|
.PHONY: test-ci
|
||||||
test-ci: apply webapp/node_modules install-go-tools
|
test-ci: apply webapp/node_modules install-go-tools
|
||||||
ifneq ($(HAS_SERVER),)
|
ifneq ($(HAS_SERVER),)
|
||||||
$(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./...
|
$(GOTESTSUM_BINARY) --format standard-verbose --junitfile report.xml -- ./...
|
||||||
endif
|
endif
|
||||||
ifneq ($(HAS_WEBAPP),)
|
ifneq ($(HAS_WEBAPP),)
|
||||||
cd webapp && $(NPM) run test;
|
cd webapp && $(NPM) run test;
|
||||||
|
|
|
@ -27,6 +27,8 @@ type Config struct {
|
||||||
Resource string
|
Resource string
|
||||||
TestRoom string
|
TestRoom string
|
||||||
TestMUC bool
|
TestMUC bool
|
||||||
|
TestDirectMessage bool
|
||||||
|
TestRoomExists bool
|
||||||
Verbose bool
|
Verbose bool
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
}
|
}
|
||||||
|
@ -41,20 +43,23 @@ func main() {
|
||||||
flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource")
|
flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource")
|
||||||
flag.StringVar(&config.TestRoom, "test-room", defaultTestRoom, "MUC room JID for testing")
|
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.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.Verbose, "verbose", true, "Enable verbose logging")
|
||||||
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity and MUC operations\n\n")
|
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, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n")
|
||||||
fmt.Fprintf(os.Stderr, "performing connection tests, optionally testing MUC room operations,\n")
|
fmt.Fprintf(os.Stderr, "performing connection tests, room existence checks, optionally testing MUC room operations\n")
|
||||||
fmt.Fprintf(os.Stderr, "and then disconnecting gracefully.\n\n")
|
fmt.Fprintf(os.Stderr, "and direct messages, and then disconnecting gracefully.\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "Examples:\n")
|
fmt.Fprintf(os.Stderr, "Examples:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s # Test basic connectivity\n", os.Args[0])
|
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 # 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")
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n")
|
fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n")
|
||||||
|
@ -75,6 +80,12 @@ func main() {
|
||||||
if config.TestMUC {
|
if config.TestMUC {
|
||||||
log.Printf(" Test Room: %s", config.TestRoom)
|
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
|
// Test the XMPP client
|
||||||
|
@ -89,6 +100,12 @@ func main() {
|
||||||
if config.TestMUC {
|
if config.TestMUC {
|
||||||
fmt.Println("✅ XMPP MUC operations test passed!")
|
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...")
|
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
|
// Create XMPP client with optional TLS configuration
|
||||||
var client *xmpp.Client
|
var client *xmpp.Client
|
||||||
if config.InsecureSkipVerify {
|
if config.InsecureSkipVerify {
|
||||||
|
@ -113,6 +133,7 @@ func testXMPPClient(config *Config) error {
|
||||||
config.Resource,
|
config.Resource,
|
||||||
"doctor-remote-id",
|
"doctor-remote-id",
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
|
doctorLogger,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
client = xmpp.NewClient(
|
client = xmpp.NewClient(
|
||||||
|
@ -121,6 +142,7 @@ func testXMPPClient(config *Config) error {
|
||||||
config.Password,
|
config.Password,
|
||||||
config.Resource,
|
config.Resource,
|
||||||
"doctor-remote-id",
|
"doctor-remote-id",
|
||||||
|
doctorLogger,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +165,7 @@ func testXMPPClient(config *Config) error {
|
||||||
|
|
||||||
// Test connection health
|
// Test connection health
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
err = client.TestConnection()
|
err = client.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connection health test failed: %w", err)
|
return fmt.Errorf("connection health test failed: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -154,7 +176,9 @@ func testXMPPClient(config *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var mucDuration time.Duration
|
var mucDuration time.Duration
|
||||||
|
var dmDuration time.Duration
|
||||||
|
var roomExistsDuration time.Duration
|
||||||
|
|
||||||
// Test MUC operations if requested
|
// Test MUC operations if requested
|
||||||
if config.TestMUC {
|
if config.TestMUC {
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
|
@ -165,6 +189,26 @@ func testXMPPClient(config *Config) error {
|
||||||
mucDuration = time.Since(start)
|
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 {
|
if config.Verbose {
|
||||||
log.Printf("Disconnecting from XMPP server...")
|
log.Printf("Disconnecting from XMPP server...")
|
||||||
}
|
}
|
||||||
|
@ -185,11 +229,23 @@ func testXMPPClient(config *Config) error {
|
||||||
if config.TestMUC {
|
if config.TestMUC {
|
||||||
log.Printf(" MUC operations time: %v", mucDuration)
|
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)
|
log.Printf(" Disconnect time: %v", disconnectDuration)
|
||||||
totalTime := connectDuration + pingDuration + disconnectDuration
|
totalTime := connectDuration + pingDuration + disconnectDuration
|
||||||
if config.TestMUC {
|
if config.TestMUC {
|
||||||
totalTime += mucDuration
|
totalTime += mucDuration
|
||||||
}
|
}
|
||||||
|
if config.TestDirectMessage {
|
||||||
|
totalTime += dmDuration
|
||||||
|
}
|
||||||
|
if config.TestRoomExists {
|
||||||
|
totalTime += roomExistsDuration
|
||||||
|
}
|
||||||
log.Printf(" Total time: %v", totalTime)
|
log.Printf(" Total time: %v", totalTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,17 +255,38 @@ func testXMPPClient(config *Config) error {
|
||||||
func testMUCOperations(client *xmpp.Client, config *Config) error {
|
func testMUCOperations(client *xmpp.Client, config *Config) error {
|
||||||
if config.Verbose {
|
if config.Verbose {
|
||||||
log.Printf("Testing MUC operations with room: %s", config.TestRoom)
|
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
|
// Test joining the room
|
||||||
start := time.Now()
|
start = time.Now()
|
||||||
err := client.JoinRoom(config.TestRoom)
|
err = client.JoinRoom(config.TestRoom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err)
|
return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err)
|
||||||
}
|
}
|
||||||
joinDuration := time.Since(start)
|
joinDuration := time.Since(start)
|
||||||
|
|
||||||
var sendDuration time.Duration
|
var sendDuration time.Duration
|
||||||
|
|
||||||
if config.Verbose {
|
if config.Verbose {
|
||||||
|
@ -223,7 +300,7 @@ func testMUCOperations(client *xmpp.Client, config *Config) error {
|
||||||
RoomJID: config.TestRoom,
|
RoomJID: config.TestRoom,
|
||||||
Message: testMessage,
|
Message: testMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
_, err = client.SendMessage(messageReq)
|
_, err = client.SendMessage(messageReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -255,11 +332,84 @@ func testMUCOperations(client *xmpp.Client, config *Config) error {
|
||||||
if config.Verbose {
|
if config.Verbose {
|
||||||
log.Printf("✅ Successfully left MUC room in %v", leaveDuration)
|
log.Printf("✅ Successfully left MUC room in %v", leaveDuration)
|
||||||
log.Printf("MUC operations summary:")
|
log.Printf("MUC operations summary:")
|
||||||
|
log.Printf(" Room existence check time: %v", checkDuration)
|
||||||
log.Printf(" Join time: %v", joinDuration)
|
log.Printf(" Join time: %v", joinDuration)
|
||||||
log.Printf(" Send message time: %v", sendDuration)
|
log.Printf(" Send message time: %v", sendDuration)
|
||||||
log.Printf(" Wait time: 5s")
|
log.Printf(" Wait time: 5s")
|
||||||
log.Printf(" Leave time: %v", leaveDuration)
|
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
|
return nil
|
||||||
|
@ -270,4 +420,31 @@ func maskPassword(password string) string {
|
||||||
return "****"
|
return "****"
|
||||||
}
|
}
|
||||||
return password[:2] + "****"
|
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
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/mock v1.6.0
|
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/mattermost/mattermost/server/public v0.1.10
|
github.com/mattermost/mattermost/server/public v0.1.10
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
@ -13,53 +12,225 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||||
|
4d63.com/gochecknoglobals v0.2.1 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // 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/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/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/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/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/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-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-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/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/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gordonklaus/ineffassign v0.1.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // 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/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-plugin v1.6.3 // 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/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/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/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||||
github.com/mattermost/logr/v2 v2.0.21 // indirect
|
github.com/mattermost/logr/v2 v2.0.21 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/oklog/run v1.1.0 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/pborman/uuid v1.2.1 // indirect
|
github.com/pborman/uuid v1.2.1 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // 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/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/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/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/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/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/wiggin77/merror v1.0.5 // indirect
|
github.com/wiggin77/merror v1.0.5 // indirect
|
||||||
github.com/wiggin77/srslog v1.0.1 // 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/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/mod v0.22.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.29.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/text v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.29.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/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
|
||||||
google.golang.org/grpc v1.70.0 // indirect
|
google.golang.org/grpc v1.70.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.4 // 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/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/reader v0.1.0 // indirect
|
||||||
mellium.im/xmlstream v0.15.4 // 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",
|
"name": "Mattermost Bridge for XMPP",
|
||||||
"description": "This plugin provides a bridge connecting Mattermost and XMPP servers.",
|
"description": "This plugin provides a bridge connecting Mattermost and XMPP servers.",
|
||||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp",
|
"homepage_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp",
|
||||||
"support_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp/issues",
|
"support_url": "https://github.com/mattermost/mattermost-plugin-bridge-xmpp/issues",
|
||||||
"icon_path": "assets/logo.png",
|
"icon_path": "assets/logo.png",
|
||||||
"version": "",
|
"version": "",
|
||||||
"min_server_version": "6.2.1",
|
"min_server_version": "9.5.0",
|
||||||
"server": {
|
"server": {
|
||||||
"executables": {
|
"executables": {
|
||||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||||
|
@ -29,28 +29,40 @@
|
||||||
"display_name": "XMPP Server URL",
|
"display_name": "XMPP Server URL",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"help_text": "The URL of the XMPP server to connect to (e.g., xmpp.example.com:5222)",
|
"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",
|
"key": "XMPPUsername",
|
||||||
"display_name": "XMPP Username",
|
"display_name": "XMPP Username",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"help_text": "The username for authenticating with the XMPP server",
|
"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",
|
"key": "XMPPPassword",
|
||||||
"display_name": "XMPP Password",
|
"display_name": "XMPP Password",
|
||||||
"type": "text",
|
"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",
|
"key": "EnableSync",
|
||||||
"display_name": "Enable Message Synchronization",
|
"display_name": "Enable Message Synchronization",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"help_text": "When enabled, messages will be synchronized between Mattermost and XMPP",
|
"help_text": "When enabled, messages will be synchronized between Mattermost and XMPP",
|
||||||
"default": false
|
"placeholder": "",
|
||||||
|
"default": false,
|
||||||
|
"hosting": "",
|
||||||
|
"secret": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "XMPPUsernamePrefix",
|
"key": "XMPPUsernamePrefix",
|
||||||
|
@ -58,7 +70,9 @@
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"help_text": "Prefix for XMPP users in Mattermost (e.g., 'xmpp' creates usernames like 'xmpp:user@domain')",
|
"help_text": "Prefix for XMPP users in Mattermost (e.g., 'xmpp' creates usernames like 'xmpp:user@domain')",
|
||||||
"placeholder": "xmpp",
|
"placeholder": "xmpp",
|
||||||
"default": "xmpp"
|
"default": "xmpp",
|
||||||
|
"hosting": "",
|
||||||
|
"secret": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "XMPPResource",
|
"key": "XMPPResource",
|
||||||
|
@ -66,20 +80,26 @@
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"help_text": "XMPP resource identifier for the bridge client",
|
"help_text": "XMPP resource identifier for the bridge client",
|
||||||
"placeholder": "mattermost-bridge",
|
"placeholder": "mattermost-bridge",
|
||||||
"default": "mattermost-bridge"
|
"default": "mattermost-bridge",
|
||||||
|
"hosting": "",
|
||||||
|
"secret": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "XMPPInsecureSkipVerify",
|
"key": "XMPPInsecureSkipVerify",
|
||||||
"display_name": "Skip TLS Certificate Verification",
|
"display_name": "Skip TLS Certificate Verification",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"help_text": "Skip TLS certificate verification for XMPP connections (use only for testing/development)",
|
"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": {
|
"props": {
|
||||||
"pluginctl": {
|
"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/logger"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
"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
|
// BridgeManager manages multiple bridge instances
|
||||||
type Manager struct {
|
type BridgeManager struct {
|
||||||
bridges map[string]model.Bridge
|
bridges map[string]model.Bridge
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
|
api plugin.API
|
||||||
|
remoteID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new bridge manager
|
// NewBridgeManager creates a new bridge manager
|
||||||
func NewManager(logger logger.Logger) model.BridgeManager {
|
func NewBridgeManager(logger logger.Logger, api plugin.API, remoteID string) model.BridgeManager {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
panic("logger cannot be nil")
|
panic("logger cannot be nil")
|
||||||
}
|
}
|
||||||
|
if api == nil {
|
||||||
|
panic("plugin API cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
return &Manager{
|
return &BridgeManager{
|
||||||
bridges: make(map[string]model.Bridge),
|
bridges: make(map[string]model.Bridge),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
api: api,
|
||||||
|
remoteID: remoteID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterBridge registers a bridge with the manager
|
// 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 == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("bridge name cannot be empty")
|
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
|
// StartBridge starts a specific bridge
|
||||||
func (m *Manager) StartBridge(name string) error {
|
func (m *BridgeManager) StartBridge(name string) error {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
bridge, exists := m.bridges[name]
|
bridge, exists := m.bridges[name]
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
@ -71,7 +80,7 @@ func (m *Manager) StartBridge(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopBridge stops a specific bridge
|
// StopBridge stops a specific bridge
|
||||||
func (m *Manager) StopBridge(name string) error {
|
func (m *BridgeManager) StopBridge(name string) error {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
bridge, exists := m.bridges[name]
|
bridge, exists := m.bridges[name]
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
@ -92,7 +101,7 @@ func (m *Manager) StopBridge(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterBridge removes a bridge from the manager
|
// UnregisterBridge removes a bridge from the manager
|
||||||
func (m *Manager) UnregisterBridge(name string) error {
|
func (m *BridgeManager) UnregisterBridge(name string) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
@ -115,7 +124,7 @@ func (m *Manager) UnregisterBridge(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBridge retrieves a bridge by name
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
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
|
// ListBridges returns a list of all registered bridge names
|
||||||
func (m *Manager) ListBridges() []string {
|
func (m *BridgeManager) ListBridges() []string {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
@ -141,7 +150,7 @@ func (m *Manager) ListBridges() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasBridge checks if a bridge with the given name is registered
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
@ -150,7 +159,7 @@ func (m *Manager) HasBridge(name string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasBridges checks if any bridges are registered
|
// HasBridges checks if any bridges are registered
|
||||||
func (m *Manager) HasBridges() bool {
|
func (m *BridgeManager) HasBridges() bool {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
@ -158,7 +167,7 @@ func (m *Manager) HasBridges() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops and unregisters all bridges
|
// Shutdown stops and unregisters all bridges
|
||||||
func (m *Manager) Shutdown() error {
|
func (m *BridgeManager) Shutdown() error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
@ -187,7 +196,7 @@ func (m *Manager) Shutdown() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPluginConfigurationChange propagates configuration changes to all registered bridges
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
@ -213,4 +222,185 @@ func (m *Manager) OnPluginConfigurationChange(config any) error {
|
||||||
|
|
||||||
m.logger.LogInfo("Configuration changes propagated to all bridges")
|
m.logger.LogInfo("Configuration changes propagated to all bridges")
|
||||||
return nil
|
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"
|
"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/config"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||||
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
||||||
|
@ -19,10 +20,11 @@ import (
|
||||||
|
|
||||||
// xmppBridge handles syncing messages between Mattermost and XMPP
|
// xmppBridge handles syncing messages between Mattermost and XMPP
|
||||||
type xmppBridge struct {
|
type xmppBridge struct {
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
api plugin.API
|
api plugin.API
|
||||||
kvstore kvstore.KVStore
|
kvstore kvstore.KVStore
|
||||||
xmppClient *xmppClient.Client
|
bridgeClient *xmppClient.Client // Main bridge XMPP client connection
|
||||||
|
userManager pluginModel.BridgeUserManager
|
||||||
|
|
||||||
// Connection management
|
// Connection management
|
||||||
connected atomic.Bool
|
connected atomic.Bool
|
||||||
|
@ -41,7 +43,7 @@ type xmppBridge struct {
|
||||||
// NewBridge creates a new XMPP bridge
|
// NewBridge creates a new XMPP bridge
|
||||||
func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge {
|
func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
bridge := &xmppBridge{
|
b := &xmppBridge{
|
||||||
logger: log,
|
logger: log,
|
||||||
api: api,
|
api: api,
|
||||||
kvstore: kvstore,
|
kvstore: kvstore,
|
||||||
|
@ -49,14 +51,15 @@ func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
channelMappings: make(map[string]string),
|
channelMappings: make(map[string]string),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
userManager: bridge.NewUserManager("xmpp", log),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize XMPP client with configuration
|
// Initialize XMPP client with configuration
|
||||||
if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" {
|
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
|
// 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.XMPPUsername,
|
||||||
cfg.XMPPPassword,
|
cfg.XMPPPassword,
|
||||||
cfg.GetXMPPResource(),
|
cfg.GetXMPPResource(),
|
||||||
"", // remoteID not needed for bridge user
|
"", // remoteID not needed for bridge client
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
|
b.logger,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,28 +90,28 @@ func (b *xmppBridge) UpdateConfiguration(newConfig any) error {
|
||||||
b.configMu.Lock()
|
b.configMu.Lock()
|
||||||
oldConfig := b.config
|
oldConfig := b.config
|
||||||
b.config = cfg
|
b.config = cfg
|
||||||
|
defer b.configMu.Unlock()
|
||||||
|
|
||||||
|
b.logger.LogInfo("XMPP bridge configuration updated")
|
||||||
|
|
||||||
// Initialize or update XMPP client with new configuration
|
// Initialize or update XMPP client with new configuration
|
||||||
if cfg.EnableSync {
|
if cfg.EnableSync {
|
||||||
if cfg.XMPPServerURL == "" || cfg.XMPPUsername == "" || cfg.XMPPPassword == "" {
|
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")
|
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 {
|
} else {
|
||||||
b.xmppClient = nil
|
b.bridgeClient = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
b.configMu.Unlock()
|
|
||||||
|
|
||||||
// Check if we need to restart the bridge due to configuration changes
|
// Check if we need to restart the bridge due to configuration changes
|
||||||
wasConnected := b.connected.Load()
|
wasConnected := b.connected.Load()
|
||||||
needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected
|
needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected
|
||||||
|
|
||||||
// Log the configuration change
|
// Log the configuration change
|
||||||
if needsRestart {
|
if needsRestart {
|
||||||
b.logger.LogInfo("Configuration changed, restarting bridge", "old_config", oldConfig, "new_config", cfg)
|
b.logger.LogInfo("Configuration changed, restarting bridge")
|
||||||
} else {
|
} else {
|
||||||
b.logger.LogInfo("Configuration updated", "config", cfg)
|
b.logger.LogInfo("Configuration updated", "config", cfg)
|
||||||
}
|
}
|
||||||
|
@ -142,9 +146,6 @@ func (b *xmppBridge) Start() error {
|
||||||
return fmt.Errorf("bridge configuration not set")
|
return fmt.Errorf("bridge configuration not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the configuration for debugging
|
|
||||||
b.logger.LogDebug("Bridge configuration", "config", config)
|
|
||||||
|
|
||||||
if !config.EnableSync {
|
if !config.EnableSync {
|
||||||
b.logger.LogInfo("XMPP sync is disabled, bridge will not start")
|
b.logger.LogInfo("XMPP sync is disabled, bridge will not start")
|
||||||
return nil
|
return nil
|
||||||
|
@ -177,8 +178,8 @@ func (b *xmppBridge) Stop() error {
|
||||||
b.cancel()
|
b.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.xmppClient != nil {
|
if b.bridgeClient != nil {
|
||||||
if err := b.xmppClient.Disconnect(); err != nil {
|
if err := b.bridgeClient.Disconnect(); err != nil {
|
||||||
b.logger.LogWarn("Error disconnecting from XMPP server", "error", err)
|
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
|
// connectToXMPP establishes connection to the XMPP server
|
||||||
func (b *xmppBridge) connectToXMPP() error {
|
func (b *xmppBridge) connectToXMPP() error {
|
||||||
if b.xmppClient == nil {
|
if b.bridgeClient == nil {
|
||||||
return fmt.Errorf("XMPP client is not initialized")
|
return fmt.Errorf("XMPP client is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.logger.LogDebug("Connecting to XMPP server")
|
b.logger.LogDebug("Connecting to XMPP server")
|
||||||
|
|
||||||
err := b.xmppClient.Connect()
|
err := b.bridgeClient.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.connected.Store(false)
|
b.connected.Store(false)
|
||||||
return fmt.Errorf("failed to connect to XMPP server: %w", err)
|
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")
|
b.logger.LogInfo("Successfully connected to XMPP server")
|
||||||
|
|
||||||
// Set online presence after successful connection
|
// 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)
|
b.logger.LogWarn("Failed to set online presence", "error", err)
|
||||||
// Don't fail the connection for presence issues
|
// Don't fail the connection for presence issues
|
||||||
} else {
|
} else {
|
||||||
b.logger.LogDebug("Set bridge user online presence")
|
b.logger.LogDebug("Set bridge client online presence")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -249,7 +250,7 @@ func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error {
|
||||||
return fmt.Errorf("not connected to XMPP server")
|
return fmt.Errorf("not connected to XMPP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.xmppClient.JoinRoom(roomJID)
|
err := b.bridgeClient.JoinRoom(roomJID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to join XMPP room: %w", err)
|
return fmt.Errorf("failed to join XMPP room: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -311,7 +312,7 @@ func (b *xmppBridge) connectionMonitor() {
|
||||||
case <-b.ctx.Done():
|
case <-b.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
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.logger.LogWarn("XMPP connection check failed", "error", err)
|
||||||
b.handleReconnection()
|
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
|
// handleReconnection attempts to reconnect to XMPP and rejoin rooms
|
||||||
func (b *xmppBridge) handleReconnection() {
|
func (b *xmppBridge) handleReconnection() {
|
||||||
b.configMu.RLock()
|
b.configMu.RLock()
|
||||||
|
@ -340,8 +333,8 @@ func (b *xmppBridge) handleReconnection() {
|
||||||
b.logger.LogInfo("Attempting to reconnect to XMPP server")
|
b.logger.LogInfo("Attempting to reconnect to XMPP server")
|
||||||
b.connected.Store(false)
|
b.connected.Store(false)
|
||||||
|
|
||||||
if b.xmppClient != nil {
|
if b.bridgeClient != nil {
|
||||||
b.xmppClient.Disconnect()
|
_ = b.bridgeClient.Disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection with exponential backoff
|
// Retry connection with exponential backoff
|
||||||
|
@ -378,19 +371,35 @@ func (b *xmppBridge) IsConnected() bool {
|
||||||
return b.connected.Load()
|
return b.connected.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateChannelRoomMapping creates a mapping between a Mattermost channel and XMPP room
|
// Ping actively tests the XMPP connection health
|
||||||
func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error {
|
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 {
|
if b.kvstore == nil {
|
||||||
return fmt.Errorf("KV store not initialized")
|
return fmt.Errorf("KV store not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store forward and reverse mappings using bridge-agnostic keys
|
err := b.kvstore.Set(kvstore.BuildChannelMapKey("xmpp", roomJID), []byte(channelID))
|
||||||
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))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to store reverse room mapping: %w", err)
|
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
|
// Join the room if connected
|
||||||
if b.connected.Load() {
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelRoomMapping gets the XMPP room JID for a Mattermost channel
|
// GetChannelMapping gets the XMPP room JID for a Mattermost channel
|
||||||
func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
func (b *xmppBridge) GetChannelMapping(channelID string) (string, error) {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
b.mappingsMu.RLock()
|
b.mappingsMu.RLock()
|
||||||
roomJID, exists := b.channelMappings[channelID]
|
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
|
// 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 {
|
if err != nil {
|
||||||
return "", nil // Unmapped channels are expected
|
return "", nil // Unmapped channels are expected
|
||||||
}
|
}
|
||||||
|
@ -441,3 +450,92 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
||||||
|
|
||||||
return roomJID, nil
|
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]", "")
|
mapSubcommand.AddTextArgument("XMPP room JID (e.g., room@conference.example.com)", "[room_jid]", "")
|
||||||
xmppBridgeData.AddCommand(mapSubcommand)
|
xmppBridgeData.AddCommand(mapSubcommand)
|
||||||
|
|
||||||
|
unmapSubcommand := model.NewAutocompleteData("unmap", "", "Unmap current channel from XMPP room")
|
||||||
|
xmppBridgeData.AddCommand(unmapSubcommand)
|
||||||
|
|
||||||
statusSubcommand := model.NewAutocompleteData("status", "", "Show bridge connection status")
|
statusSubcommand := model.NewAutocompleteData("status", "", "Show bridge connection status")
|
||||||
xmppBridgeData.AddCommand(statusSubcommand)
|
xmppBridgeData.AddCommand(statusSubcommand)
|
||||||
|
|
||||||
|
@ -36,7 +39,7 @@ func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.Bridg
|
||||||
Trigger: xmppBridgeCommandTrigger,
|
Trigger: xmppBridgeCommandTrigger,
|
||||||
AutoComplete: true,
|
AutoComplete: true,
|
||||||
AutoCompleteDesc: "Manage XMPP bridge mappings",
|
AutoCompleteDesc: "Manage XMPP bridge mappings",
|
||||||
AutoCompleteHint: "[map|status]",
|
AutoCompleteHint: "[map|unmap|status]",
|
||||||
AutocompleteData: xmppBridgeData,
|
AutocompleteData: xmppBridgeData,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// 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) {
|
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], "/")
|
trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
|
||||||
switch trigger {
|
switch trigger {
|
||||||
case xmppBridgeCommandTrigger:
|
case xmppBridgeCommandTrigger:
|
||||||
|
@ -72,6 +83,7 @@ func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.Comma
|
||||||
|
|
||||||
**Available commands:**
|
**Available commands:**
|
||||||
- ` + "`/xmppbridge map <room_jid>`" + ` - Map current channel to XMPP room
|
- ` + "`/xmppbridge map <room_jid>`" + ` - Map current channel to XMPP room
|
||||||
|
- ` + "`/xmppbridge unmap`" + ` - Unmap current channel from XMPP room
|
||||||
- ` + "`/xmppbridge status`" + ` - Show bridge connection status
|
- ` + "`/xmppbridge status`" + ` - Show bridge connection status
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
@ -83,6 +95,8 @@ func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.Comma
|
||||||
switch subcommand {
|
switch subcommand {
|
||||||
case "map":
|
case "map":
|
||||||
return c.executeMapCommand(args, fields)
|
return c.executeMapCommand(args, fields)
|
||||||
|
case "unmap":
|
||||||
|
return c.executeUnmapCommand(args)
|
||||||
case "status":
|
case "status":
|
||||||
return c.executeStatusCommand(args)
|
return c.executeStatusCommand(args)
|
||||||
default:
|
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")
|
bridge, err := c.bridgeManager.GetBridge("xmpp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
|
@ -130,7 +144,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if channel is already mapped
|
// Check if channel is already mapped
|
||||||
existingMapping, err := bridge.GetChannelRoomMapping(channelID)
|
existingMapping, err := bridge.GetChannelMapping(channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
@ -145,21 +159,73 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the mapping
|
// Create the mapping using BridgeManager
|
||||||
err = bridge.CreateChannelRoomMapping(channelID, roomJID)
|
mappingReq := pluginModel.CreateChannelMappingRequest{
|
||||||
|
ChannelID: channelID,
|
||||||
|
BridgeName: "xmpp",
|
||||||
|
BridgeRoomID: roomJID,
|
||||||
|
UserID: args.UserId,
|
||||||
|
TeamID: args.TeamId,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.bridgeManager.CreateChannelMapping(mappingReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.CommandResponse{
|
return c.formatMappingError("create", roomJID, err)
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
|
||||||
Text: fmt.Sprintf("❌ Failed to create channel mapping: %v", err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeInChannel,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: fmt.Sprintf("✅ Successfully mapped this channel to XMPP room: `%s`", roomJID),
|
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 {
|
func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandResponse {
|
||||||
// Get the XMPP bridge
|
// Get the XMPP bridge
|
||||||
bridge, err := c.bridgeManager.GetBridge("xmpp")
|
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
|
// Check if current channel is mapped
|
||||||
channelID := args.ChannelId
|
channelID := args.ChannelId
|
||||||
roomJID, err := bridge.GetChannelRoomMapping(channelID)
|
roomJID, err := bridge.GetChannelMapping(channelID)
|
||||||
|
|
||||||
var mappingText string
|
var mappingText string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -201,6 +267,89 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe
|
||||||
%s
|
%s
|
||||||
|
|
||||||
**Commands:**
|
**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
|
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
|
||||||
// copy appropriate for your types.
|
// copy appropriate for your types.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
XMPPServerURL string `json:"XMPPServerURL"`
|
XMPPServerURL string `json:"XMPPServerURL"`
|
||||||
XMPPUsername string `json:"XMPPUsername"`
|
XMPPUsername string `json:"XMPPUsername"`
|
||||||
XMPPPassword string `json:"XMPPPassword"`
|
XMPPPassword string `json:"XMPPPassword"`
|
||||||
EnableSync bool `json:"EnableSync"`
|
EnableSync bool `json:"EnableSync"`
|
||||||
XMPPUsernamePrefix string `json:"XMPPUsernamePrefix"`
|
XMPPUsernamePrefix string `json:"XMPPUsernamePrefix"`
|
||||||
XMPPResource string `json:"XMPPResource"`
|
XMPPResource string `json:"XMPPResource"`
|
||||||
XMPPInsecureSkipVerify bool `json:"XMPPInsecureSkipVerify"`
|
XMPPInsecureSkipVerify bool `json:"XMPPInsecureSkipVerify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equals compares two configuration structs
|
// Equals compares two configuration structs
|
||||||
|
@ -95,4 +95,4 @@ func (c *Configuration) IsValid() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
// LogError logs an error message
|
||||||
func (l *PluginAPILogger) LogError(message string, keyValuePairs ...any) {
|
func (l *PluginAPILogger) LogError(message string, keyValuePairs ...any) {
|
||||||
l.api.LogError(message, keyValuePairs...)
|
l.api.LogError(message, keyValuePairs...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,77 @@
|
||||||
package model
|
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 {
|
type BridgeManager interface {
|
||||||
// RegisterBridge registers a bridge with the given name. Returns an error if the name is empty,
|
// 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.
|
// 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
|
// Returns an error if any bridge fails to update its configuration, but continues to
|
||||||
// attempt updating all bridges.
|
// attempt updating all bridges.
|
||||||
OnPluginConfigurationChange(config any) error
|
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 {
|
type Bridge interface {
|
||||||
|
@ -51,12 +129,79 @@ type Bridge interface {
|
||||||
// Stop stops the bridge
|
// Stop stops the bridge
|
||||||
Stop() error
|
Stop() error
|
||||||
|
|
||||||
// CreateChannelRoomMapping creates a mapping between a Mattermost channel ID and an bridge room ID.
|
// CreateChannelMapping creates a mapping between a Mattermost channel ID and an bridge room ID.
|
||||||
CreateChannelRoomMapping(channelID, roomJID string) error
|
CreateChannelMapping(channelID, roomJID string) error
|
||||||
|
|
||||||
// GetChannelRoomMapping retrieves the bridge room ID for a given Mattermost channel ID.
|
// GetChannelMapping retrieves the bridge room ID for a given Mattermost channel ID.
|
||||||
GetChannelRoomMapping(channelID string) (string, error)
|
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 checks if the bridge is connected to the remote service.
|
||||||
IsConnected() bool
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge"
|
"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"
|
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/command"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
||||||
|
@ -43,6 +44,10 @@ type Plugin struct {
|
||||||
// remoteID is the identifier returned by RegisterPluginForSharedChannels
|
// remoteID is the identifier returned by RegisterPluginForSharedChannels
|
||||||
remoteID string
|
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
|
backgroundJob *cluster.Job
|
||||||
|
|
||||||
// configurationLock synchronizes access to the configuration.
|
// configurationLock synchronizes access to the configuration.
|
||||||
|
@ -71,8 +76,14 @@ func (p *Plugin) OnActivate() error {
|
||||||
cfg := p.getConfiguration()
|
cfg := p.getConfiguration()
|
||||||
p.logger.LogDebug("Loaded configuration in OnActivate", "config", cfg)
|
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
|
// 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
|
// Initialize and register bridges with current configuration
|
||||||
if err := p.initBridges(*cfg); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,24 +153,69 @@ func (p *Plugin) initXMPPClient() {
|
||||||
cfg.XMPPPassword,
|
cfg.XMPPPassword,
|
||||||
cfg.GetXMPPResource(),
|
cfg.GetXMPPResource(),
|
||||||
p.remoteID,
|
p.remoteID,
|
||||||
|
p.logger,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) initBridges(cfg config.Configuration) error {
|
func (p *Plugin) initBridges(cfg config.Configuration) error {
|
||||||
// Create and register XMPP bridge
|
// Create and register XMPP bridge
|
||||||
bridge := xmppbridge.NewBridge(
|
xmppBridge := xmppbridge.NewBridge(
|
||||||
p.logger,
|
p.logger,
|
||||||
p.API,
|
p.API,
|
||||||
p.kvstore,
|
p.kvstore,
|
||||||
&cfg,
|
&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)
|
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")
|
p.logger.LogInfo("Bridge instances created and registered successfully")
|
||||||
return nil
|
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/
|
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||||
"mellium.im/sasl"
|
"mellium.im/sasl"
|
||||||
"mellium.im/xmpp"
|
"mellium.im/xmpp"
|
||||||
|
"mellium.im/xmpp/disco"
|
||||||
"mellium.im/xmpp/jid"
|
"mellium.im/xmpp/jid"
|
||||||
"mellium.im/xmpp/muc"
|
"mellium.im/xmpp/muc"
|
||||||
"mellium.im/xmpp/mux"
|
"mellium.im/xmpp/mux"
|
||||||
|
@ -22,18 +24,19 @@ type Client struct {
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
resource string
|
resource string
|
||||||
remoteID string // Plugin remote ID for metadata
|
remoteID string // Plugin remote ID for metadata
|
||||||
serverDomain string // explicit server domain for testing
|
serverDomain string // explicit server domain for testing
|
||||||
tlsConfig *tls.Config // custom TLS configuration
|
tlsConfig *tls.Config // custom TLS configuration
|
||||||
|
logger logger.Logger // Logger for debugging
|
||||||
|
|
||||||
// XMPP connection
|
// XMPP connection
|
||||||
session *xmpp.Session
|
session *xmpp.Session
|
||||||
jidAddr jid.JID
|
jidAddr jid.JID
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
mucClient *muc.Client
|
mucClient *muc.Client
|
||||||
mux *mux.ServeMux
|
mux *mux.ServeMux
|
||||||
sessionReady chan struct{}
|
sessionReady chan struct{}
|
||||||
sessionServing bool
|
sessionServing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +55,21 @@ type SendMessageResponse struct {
|
||||||
StanzaID string `json:"stanza_id"`
|
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
|
// GhostUser represents an XMPP ghost user
|
||||||
type GhostUser struct {
|
type GhostUser struct {
|
||||||
JID string `json:"jid"`
|
JID string `json:"jid"`
|
||||||
|
@ -65,17 +83,18 @@ type UserProfile struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new XMPP client.
|
// 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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
mucClient := &muc.Client{}
|
mucClient := &muc.Client{}
|
||||||
mux := mux.New("jabber:client", muc.HandleClient(mucClient))
|
mux := mux.New("jabber:client", muc.HandleClient(mucClient))
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
serverURL: serverURL,
|
serverURL: serverURL,
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
remoteID: remoteID,
|
remoteID: remoteID,
|
||||||
|
logger: logger,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
mucClient: mucClient,
|
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.
|
// NewClientWithTLS creates a new XMPP client with custom TLS configuration.
|
||||||
func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config) *Client {
|
func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config, logger logger.Logger) *Client {
|
||||||
client := NewClient(serverURL, username, password, resource, remoteID)
|
client := NewClient(serverURL, username, password, resource, remoteID, logger)
|
||||||
client.tlsConfig = tlsConfig
|
client.tlsConfig = tlsConfig
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
@ -164,11 +183,11 @@ func (c *Client) serveSession() {
|
||||||
close(c.sessionReady) // Signal failure
|
close(c.sessionReady) // Signal failure
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal that the session is ready to serve
|
// Signal that the session is ready to serve
|
||||||
c.sessionServing = true
|
c.sessionServing = true
|
||||||
close(c.sessionReady)
|
close(c.sessionReady)
|
||||||
|
|
||||||
err := c.session.Serve(c.mux)
|
err := c.session.Serve(c.mux)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sessionServing = false
|
c.sessionServing = false
|
||||||
|
@ -202,23 +221,6 @@ func (c *Client) Disconnect() error {
|
||||||
return nil
|
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
|
// JoinRoom joins an XMPP Multi-User Chat room
|
||||||
func (c *Client) JoinRoom(roomJID string) error {
|
func (c *Client) JoinRoom(roomJID string) error {
|
||||||
if c.session == nil {
|
if c.session == nil {
|
||||||
|
@ -251,7 +253,7 @@ func (c *Client) JoinRoom(roomJID string) error {
|
||||||
opts := []muc.Option{
|
opts := []muc.Option{
|
||||||
muc.MaxBytes(0), // Don't limit message history
|
muc.MaxBytes(0), // Don't limit message history
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the join operation in a goroutine to avoid blocking
|
// Run the join operation in a goroutine to avoid blocking
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -324,26 +326,12 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) {
|
||||||
sendCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second)
|
sendCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Create the message body structure
|
|
||||||
type messageBody struct {
|
|
||||||
XMLName xml.Name `xml:"body"`
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create complete message with body
|
// Create complete message with body
|
||||||
type message struct {
|
fullMsg := XMPPMessage{
|
||||||
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{
|
|
||||||
Type: "groupchat",
|
Type: "groupchat",
|
||||||
To: to.String(),
|
To: to.String(),
|
||||||
From: c.jidAddr.String(),
|
From: c.jidAddr.String(),
|
||||||
Body: messageBody{Text: req.Message},
|
Body: MessageBody{Text: req.Message},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message using the session encoder
|
// Send the message using the session encoder
|
||||||
|
@ -359,6 +347,39 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) {
|
||||||
return response, nil
|
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
|
// ResolveRoomAlias resolves a room alias to room JID
|
||||||
func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) {
|
func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) {
|
||||||
// For XMPP, return the alias as-is if it's already a valid JID
|
// 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
|
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