feat: complete XMPP bridge implementation with configuration fixes
- Fix configuration loading by matching JSON field names with plugin manifest keys - Move configuration to separate package to resolve type conflicts - Implement bridge startup logic that initializes on OnActivate and updates on OnConfigurationChange - Add certificate verification skip option for development/testing environments - Create XMPP client initialization helper function to avoid code duplication - Add SetOnlinePresence() method to XMPP client for presence management - Set bridge user online presence automatically upon successful XMPP connection - Remove unused mock generation and test files as requested - Update bridge constructor to accept configuration parameter - Implement proper bridge lifecycle management with Start/Stop methods The bridge now properly loads configuration from admin console, creates XMPP connections with appropriate TLS settings, and manages online presence for the bridge user. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
07ff46624d
commit
4d6929bab6
12 changed files with 801 additions and 242 deletions
|
@ -4,35 +4,48 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
client *pluginapi.Client
|
||||
bridge pluginModel.Bridge
|
||||
}
|
||||
|
||||
type Command interface {
|
||||
Handle(args *model.CommandArgs) (*model.CommandResponse, error)
|
||||
executeHelloCommand(args *model.CommandArgs) *model.CommandResponse
|
||||
executeXMPPBridgeCommand(args *model.CommandArgs) *model.CommandResponse
|
||||
}
|
||||
|
||||
const helloCommandTrigger = "hello"
|
||||
const xmppBridgeCommandTrigger = "xmppbridge"
|
||||
|
||||
// Register all your slash commands in the NewCommandHandler function.
|
||||
func NewCommandHandler(client *pluginapi.Client) Command {
|
||||
func NewCommandHandler(client *pluginapi.Client, bridge pluginModel.Bridge) Command {
|
||||
// Register XMPP bridge command
|
||||
xmppBridgeData := model.NewAutocompleteData(xmppBridgeCommandTrigger, "", "Manage XMPP bridge")
|
||||
mapSubcommand := model.NewAutocompleteData("map", "[room_jid]", "Map current channel to XMPP room")
|
||||
mapSubcommand.AddTextArgument("XMPP room JID (e.g., room@conference.example.com)", "[room_jid]", "")
|
||||
xmppBridgeData.AddCommand(mapSubcommand)
|
||||
|
||||
statusSubcommand := model.NewAutocompleteData("status", "", "Show bridge connection status")
|
||||
xmppBridgeData.AddCommand(statusSubcommand)
|
||||
|
||||
err := client.SlashCommand.Register(&model.Command{
|
||||
Trigger: helloCommandTrigger,
|
||||
Trigger: xmppBridgeCommandTrigger,
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: "Say hello to someone",
|
||||
AutoCompleteHint: "[@username]",
|
||||
AutocompleteData: model.NewAutocompleteData(helloCommandTrigger, "[@username]", "Username to say hello to"),
|
||||
AutoCompleteDesc: "Manage XMPP bridge mappings",
|
||||
AutoCompleteHint: "[map|status]",
|
||||
AutocompleteData: xmppBridgeData,
|
||||
})
|
||||
if err != nil {
|
||||
client.Log.Error("Failed to register command", "error", err)
|
||||
client.Log.Error("Failed to register XMPP bridge command", "error", err)
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
client: client,
|
||||
bridge: bridge,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,8 +53,8 @@ func NewCommandHandler(client *pluginapi.Client) Command {
|
|||
func (c *Handler) Handle(args *model.CommandArgs) (*model.CommandResponse, error) {
|
||||
trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
|
||||
switch trigger {
|
||||
case helloCommandTrigger:
|
||||
return c.executeHelloCommand(args), nil
|
||||
case xmppBridgeCommandTrigger:
|
||||
return c.executeXMPPBridgeCommand(args), nil
|
||||
default:
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
|
@ -50,15 +63,126 @@ func (c *Handler) Handle(args *model.CommandArgs) (*model.CommandResponse, error
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Handler) executeHelloCommand(args *model.CommandArgs) *model.CommandResponse {
|
||||
if len(strings.Fields(args.Command)) < 2 {
|
||||
func (c *Handler) executeXMPPBridgeCommand(args *model.CommandArgs) *model.CommandResponse {
|
||||
fields := strings.Fields(args.Command)
|
||||
if len(fields) < 2 {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: "Please specify a username",
|
||||
Text: `### XMPP Bridge Commands
|
||||
|
||||
**Available commands:**
|
||||
- ` + "`/xmppbridge map <room_jid>`" + ` - Map current channel to XMPP room
|
||||
- ` + "`/xmppbridge status`" + ` - Show bridge connection status
|
||||
|
||||
**Example:**
|
||||
` + "`/xmppbridge map general@conference.example.com`",
|
||||
}
|
||||
}
|
||||
username := strings.Fields(args.Command)[1]
|
||||
return &model.CommandResponse{
|
||||
Text: "Hello, " + username,
|
||||
|
||||
subcommand := fields[1]
|
||||
switch subcommand {
|
||||
case "map":
|
||||
return c.executeMapCommand(args, fields)
|
||||
case "status":
|
||||
return c.executeStatusCommand(args)
|
||||
default:
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("Unknown subcommand: %s. Use `/xmppbridge` for help.", subcommand),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *model.CommandResponse {
|
||||
if len(fields) < 3 {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: "Please specify an XMPP room JID. Example: `/xmppbridge map general@conference.example.com`",
|
||||
}
|
||||
}
|
||||
|
||||
roomJID := fields[2]
|
||||
channelID := args.ChannelId
|
||||
|
||||
// Validate room JID format (basic validation)
|
||||
if !strings.Contains(roomJID, "@") {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: "Invalid room JID format. Please use format: `room@conference.server.com`",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if bridge is connected
|
||||
if !c.bridge.IsConnected() {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: "❌ XMPP bridge is not connected. Please check the plugin configuration.",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if channel is already mapped
|
||||
existingMapping, err := c.bridge.GetChannelRoomMapping(channelID)
|
||||
if err != nil {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("Error checking existing mapping: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
if existingMapping != "" {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("❌ This channel is already mapped to XMPP room: `%s`", existingMapping),
|
||||
}
|
||||
}
|
||||
|
||||
// Create the mapping
|
||||
err = c.bridge.CreateChannelRoomMapping(channelID, roomJID)
|
||||
if err != nil {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("❌ Failed to create channel mapping: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeInChannel,
|
||||
Text: fmt.Sprintf("✅ Successfully mapped this channel to XMPP room: `%s`", roomJID),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandResponse {
|
||||
isConnected := c.bridge.IsConnected()
|
||||
|
||||
var statusText string
|
||||
if isConnected {
|
||||
statusText = "✅ **Connected** - XMPP bridge is active"
|
||||
} else {
|
||||
statusText = "❌ **Disconnected** - XMPP bridge is not connected"
|
||||
}
|
||||
|
||||
// Check if current channel is mapped
|
||||
channelID := args.ChannelId
|
||||
roomJID, err := c.bridge.GetChannelRoomMapping(channelID)
|
||||
|
||||
var mappingText string
|
||||
if err != nil {
|
||||
mappingText = fmt.Sprintf("⚠️ Error checking channel mapping: %v", err)
|
||||
} else if roomJID != "" {
|
||||
mappingText = fmt.Sprintf("🔗 **Current channel mapping:** `%s`", roomJID)
|
||||
} else {
|
||||
mappingText = "📝 **Current channel:** Not mapped to any XMPP room"
|
||||
}
|
||||
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf(`### XMPP Bridge Status
|
||||
|
||||
%s
|
||||
|
||||
%s
|
||||
|
||||
**Commands:**
|
||||
- Use `+"`/xmppbridge map <room_jid>`"+` to map this channel to an XMPP room`, statusText, mappingText),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type env struct {
|
||||
client *pluginapi.Client
|
||||
api *plugintest.API
|
||||
}
|
||||
|
||||
func setupTest() *env {
|
||||
api := &plugintest.API{}
|
||||
driver := &plugintest.Driver{}
|
||||
client := pluginapi.NewClient(api, driver)
|
||||
|
||||
return &env{
|
||||
client: client,
|
||||
api: api,
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelloCommand(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
env := setupTest()
|
||||
|
||||
env.api.On("RegisterCommand", &model.Command{
|
||||
Trigger: helloCommandTrigger,
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: "Say hello to someone",
|
||||
AutoCompleteHint: "[@username]",
|
||||
AutocompleteData: model.NewAutocompleteData("hello", "[@username]", "Username to say hello to"),
|
||||
}).Return(nil)
|
||||
cmdHandler := NewCommandHandler(env.client)
|
||||
|
||||
args := &model.CommandArgs{
|
||||
Command: "/hello world",
|
||||
}
|
||||
response, err := cmdHandler.Handle(args)
|
||||
assert.Nil(err)
|
||||
assert.Equal("Hello, world", response.Text)
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command (interfaces: Command)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// MockCommand is a mock of Command interface.
|
||||
type MockCommand struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCommandMockRecorder
|
||||
}
|
||||
|
||||
// MockCommandMockRecorder is the mock recorder for MockCommand.
|
||||
type MockCommandMockRecorder struct {
|
||||
mock *MockCommand
|
||||
}
|
||||
|
||||
// NewMockCommand creates a new mock instance.
|
||||
func NewMockCommand(ctrl *gomock.Controller) *MockCommand {
|
||||
mock := &MockCommand{ctrl: ctrl}
|
||||
mock.recorder = &MockCommandMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockCommand) EXPECT() *MockCommandMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Handle mocks base method.
|
||||
func (m *MockCommand) Handle(arg0 *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Handle", arg0)
|
||||
ret0, _ := ret[0].(*model.CommandResponse)
|
||||
ret1, _ := ret[1].(*model.AppError)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Handle indicates an expected call of Handle.
|
||||
func (mr *MockCommandMockRecorder) Handle(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockCommand)(nil).Handle), arg0)
|
||||
}
|
||||
|
||||
// executeHelloCommand mocks base method.
|
||||
func (m *MockCommand) executeHelloCommand(arg0 *model.CommandArgs) *model.CommandResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "executeHelloCommand", arg0)
|
||||
ret0, _ := ret[0].(*model.CommandResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// executeHelloCommand indicates an expected call of executeHelloCommand.
|
||||
func (mr *MockCommandMockRecorder) executeHelloCommand(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeHelloCommand", reflect.TypeOf((*MockCommand)(nil).executeHelloCommand), arg0)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue