diff --git a/plugin.json b/plugin.json index a2615fb14..08a4f9666 100644 --- a/plugin.json +++ b/plugin.json @@ -80,6 +80,24 @@ "type": "bool", "help_text": "Sync direct and group messages where any of the user in the conversation is a real Mattermost user connected to MS Teams account", "default": false + },{ + "key": "syncLinkedChannels", + "display_name": "Sync linked channels", + "type": "bool", + "help_text": "Sync messages from channels linked between Mattermost and MS Teams", + "default": false + },{ + "key": "syncReactions", + "display_name": "Sync reactions", + "type": "bool", + "help_text": "Sync reactions on messages", + "default": false + },{ + "key": "syncFileAttachments", + "display_name": "Sync file attachments", + "type": "bool", + "help_text": "Sync file attachments on messages", + "default": false },{ "key": "enabledTeams", "display_name": "Enabled Teams", diff --git a/server/command.go b/server/command.go index 48bd1f903..cbe34b51e 100644 --- a/server/command.go +++ b/server/command.go @@ -16,7 +16,7 @@ import ( const msteamsCommand = "msteams-sync" const commandWaitingMessage = "Please wait while your request is being processed." -func (p *Plugin) createMsteamsSyncCommand() *model.Command { +func (p *Plugin) createMsteamsSyncCommand(syncLinkedChannels bool) *model.Command { iconData, err := command.GetIconData(p.API, "assets/msteams-sync-icon.svg") if err != nil { p.API.LogWarn("Unable to get the MS Teams icon for the slash command") @@ -29,7 +29,7 @@ func (p *Plugin) createMsteamsSyncCommand() *model.Command { AutoCompleteHint: "[command]", Username: botUsername, DisplayName: botDisplayName, - AutocompleteData: getAutocompleteData(), + AutocompleteData: getAutocompleteData(syncLinkedChannels), AutocompleteIconData: iconData, } } @@ -51,23 +51,25 @@ func (p *Plugin) sendBotEphemeralPost(userID, channelID, message string) { }) } -func getAutocompleteData() *model.AutocompleteData { +func getAutocompleteData(syncLinkedChannels bool) *model.AutocompleteData { cmd := model.NewAutocompleteData(msteamsCommand, "[command]", "Manage MS Teams linked channels") - link := model.NewAutocompleteData("link", "[msteams-team-id] [msteams-channel-id]", "Link current channel to a MS Teams channel") - link.AddDynamicListArgument("[msteams-team-id]", getAutocompletePath("teams"), true) - link.AddDynamicListArgument("[msteams-channel-id]", getAutocompletePath("channels"), true) - cmd.AddCommand(link) + if syncLinkedChannels { + link := model.NewAutocompleteData("link", "[msteams-team-id] [msteams-channel-id]", "Link current channel to a MS Teams channel") + link.AddDynamicListArgument("[msteams-team-id]", getAutocompletePath("teams"), true) + link.AddDynamicListArgument("[msteams-channel-id]", getAutocompletePath("channels"), true) + cmd.AddCommand(link) - unlink := model.NewAutocompleteData("unlink", "", "Unlink the current channel from the MS Teams channel") - cmd.AddCommand(unlink) + unlink := model.NewAutocompleteData("unlink", "", "Unlink the current channel from the MS Teams channel") + cmd.AddCommand(unlink) - show := model.NewAutocompleteData("show", "", "Show MS Teams linked channel") - cmd.AddCommand(show) + show := model.NewAutocompleteData("show", "", "Show MS Teams linked channel") + cmd.AddCommand(show) - showLinks := model.NewAutocompleteData("show-links", "", "Show all MS Teams linked channels") - showLinks.RoleID = model.SystemAdminRoleId - cmd.AddCommand(showLinks) + showLinks := model.NewAutocompleteData("show-links", "", "Show all MS Teams linked channels") + showLinks.RoleID = model.SystemAdminRoleId + cmd.AddCommand(showLinks) + } connect := model.NewAutocompleteData("connect", "", "Connect your Mattermost account to your MS Teams account") cmd.AddCommand(connect) @@ -108,20 +110,22 @@ func (p *Plugin) ExecuteCommand(_ *plugin.Context, args *model.CommandArgs) (*mo return &model.CommandResponse{}, nil } - if action == "link" { - return p.executeLinkCommand(args, parameters) - } + if p.getConfiguration().SyncLinkedChannels { + if action == "link" { + return p.executeLinkCommand(args, parameters) + } - if action == "unlink" { - return p.executeUnlinkCommand(args) - } + if action == "unlink" { + return p.executeUnlinkCommand(args) + } - if action == "show" { - return p.executeShowCommand(args) - } + if action == "show" { + return p.executeShowCommand(args) + } - if action == "show-links" { - return p.executeShowLinksCommand(args) + if action == "show-links" { + return p.executeShowLinksCommand(args) + } } if action == "connect" { diff --git a/server/command_test.go b/server/command_test.go index c731b3ab7..c606e1bfe 100644 --- a/server/command_test.go +++ b/server/command_test.go @@ -1002,11 +1002,13 @@ func TestExecuteConnectBotCommand(t *testing.T) { func TestGetAutocompleteData(t *testing.T) { for _, testCase := range []struct { - description string - autocompleteData *model.AutocompleteData + description string + syncLinkedChannels bool + autocompleteData *model.AutocompleteData }{ { - description: "Successfully get all auto complete data", + description: "Successfully get all auto complete data", + syncLinkedChannels: true, autocompleteData: &model.AutocompleteData{ Trigger: "msteams-sync", Hint: "[command]", @@ -1117,9 +1119,76 @@ func TestGetAutocompleteData(t *testing.T) { }, }, }, + { + description: "Successfully get all auto complete data", + syncLinkedChannels: false, + autocompleteData: &model.AutocompleteData{ + Trigger: "msteams-sync", + Hint: "[command]", + HelpText: "Manage MS Teams linked channels", + RoleID: model.SystemUserRoleId, + Arguments: []*model.AutocompleteArg{}, + SubCommands: []*model.AutocompleteData{ + { + Trigger: "connect", + HelpText: "Connect your Mattermost account to your MS Teams account", + RoleID: model.SystemUserRoleId, + Arguments: []*model.AutocompleteArg{}, + SubCommands: []*model.AutocompleteData{}, + }, + { + Trigger: "disconnect", + HelpText: "Disconnect your Mattermost account from your MS Teams account", + RoleID: model.SystemUserRoleId, + Arguments: []*model.AutocompleteArg{}, + SubCommands: []*model.AutocompleteData{}, + }, + { + Trigger: "connect-bot", + HelpText: "Connect the bot account (only system admins can do this)", + RoleID: model.SystemAdminRoleId, + Arguments: []*model.AutocompleteArg{}, + SubCommands: []*model.AutocompleteData{}, + }, + { + Trigger: "disconnect-bot", + HelpText: "Disconnect the bot account (only system admins can do this)", + RoleID: model.SystemAdminRoleId, + Arguments: []*model.AutocompleteArg{}, + SubCommands: []*model.AutocompleteData{}, + }, + { + Trigger: "promote", + HelpText: "Promote a user from synthetic user account to regular mattermost account", + RoleID: model.SystemAdminRoleId, + Arguments: []*model.AutocompleteArg{ + { + HelpText: "Username of the existing mattermost user", + Type: "TextInput", + Required: true, + Data: &model.AutocompleteTextArg{ + Hint: "username", + Pattern: `^[a-z0-9\.\-_:]+$`, + }, + }, + { + HelpText: "The new username after the user is promoted", + Type: "TextInput", + Required: true, + Data: &model.AutocompleteTextArg{ + Hint: "new username", + Pattern: `^[a-z0-9\.\-_:]+$`, + }, + }, + }, + SubCommands: []*model.AutocompleteData{}, + }, + }, + }, + }, } { t.Run(testCase.description, func(t *testing.T) { - autocompleteData := getAutocompleteData() + autocompleteData := getAutocompleteData(testCase.syncLinkedChannels) assert.Equal(t, testCase.autocompleteData, autocompleteData) }) } diff --git a/server/configuration.go b/server/configuration.go index 54dd4dbee..d69be609a 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -28,6 +28,9 @@ type configuration struct { WebhookSecret string `json:"webhooksecret"` EnabledTeams string `json:"enabledteams"` SyncDirectMessages bool `json:"syncdirectmessages"` + SyncLinkedChannels bool `json:"synclinkedchannels"` + SyncReactions bool `json:"syncreactions"` + SyncFileAttachments bool `json:"syncfileattachments"` SyncUsers int `json:"syncusers"` SyncGuestUsers bool `json:"syncGuestUsers"` CertificatePublic string `json:"certificatepublic"` diff --git a/server/handlers/attachments.go b/server/handlers/attachments.go index 9036ff2bd..4c787a73b 100644 --- a/server/handlers/attachments.go +++ b/server/handlers/attachments.go @@ -145,6 +145,10 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg continue } + if !ah.plugin.GetSyncFileAttachments() { + continue + } + // handle the download var attachmentData []byte var err error diff --git a/server/handlers/attachments_test.go b/server/handlers/attachments_test.go index b47a14310..b963ca5eb 100644 --- a/server/handlers/attachments_test.go +++ b/server/handlers/attachments_test.go @@ -266,9 +266,36 @@ func TestHandleAttachments(t *testing.T) { expectedParentID string expectedError bool }{ + { + description: "File attachments disabled by configuration", + setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(false).Maybe() + p.On("GetClientForApp").Return(client).Maybe() + p.On("GetAPI").Return(mockAPI).Maybe() + p.On("GetMetrics").Return(mockmetrics).Maybe() + }, + setupAPI: func(mockAPI *plugintest.API) { + mockAPI.On("GetConfig").Return(&model.Config{ + FileSettings: model.FileSettings{ + MaxFileSize: model.NewInt64(5), + }, + }) + mockAPI.On("UploadFile", []byte{}, testutils.GetChannelID(), "mock-name").Return(&model.FileInfo{ + Id: testutils.GetID(), + }, nil) + }, + setupClient: func(client *mocksClient.Client) { + }, + setupMetrics: func(mockmetrics *mocksMetrics.Metrics) { + }, + attachments: []clientmodels.Attachment{}, + expectedText: "mock-text", + expectedAttachmentIDsCount: 0, + }, { description: "Successfully handled attachments", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetMaxSizeForCompleteDownload").Return(1).Times(1) @@ -302,6 +329,7 @@ func TestHandleAttachments(t *testing.T) { { description: "Client is nil", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetClientForApp").Return(nil) p.On("GetAPI").Return(mockAPI).Maybe() }, @@ -318,6 +346,7 @@ func TestHandleAttachments(t *testing.T) { { description: "Error uploading the file", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetMaxSizeForCompleteDownload").Return(1).Times(1) @@ -348,6 +377,7 @@ func TestHandleAttachments(t *testing.T) { { description: "Number of attachments are greater than 10", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetMaxSizeForCompleteDownload").Return(1).Times(10) @@ -378,6 +408,7 @@ func TestHandleAttachments(t *testing.T) { { description: "Attachment type code snippet", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetMetrics").Return(mockmetrics).Maybe() }, @@ -405,6 +436,7 @@ func TestHandleAttachments(t *testing.T) { { description: "Attachment type message reference", setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, client *mocksClient.Client, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncFileAttachments").Return(true).Maybe() p.On("GetMetrics").Return(mockmetrics).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetStore").Return(store, nil) diff --git a/server/handlers/getters_test.go b/server/handlers/getters_test.go index 9482c6090..5961dd334 100644 --- a/server/handlers/getters_test.go +++ b/server/handlers/getters_test.go @@ -27,6 +27,9 @@ type pluginMock struct { api plugin.API store store.Store syncDirectMessages bool + syncLinkedChannels bool + syncReactions bool + syncFileAttachments bool syncGuestUsers bool maxSizeForCompleteDownload int bufferSizeForStreaming int @@ -40,7 +43,10 @@ type pluginMock struct { func (pm *pluginMock) GetAPI() plugin.API { return pm.api } func (pm *pluginMock) GetStore() store.Store { return pm.store } +func (pm *pluginMock) GetSyncLinkedChannels() bool { return pm.syncLinkedChannels } func (pm *pluginMock) GetSyncDirectMessages() bool { return pm.syncDirectMessages } +func (pm *pluginMock) GetSyncFileAttachments() bool { return pm.syncFileAttachments } +func (pm *pluginMock) GetSyncReactions() bool { return pm.syncReactions } func (pm *pluginMock) GetSyncGuestUsers() bool { return pm.syncGuestUsers } func (pm *pluginMock) GetMaxSizeForCompleteDownload() int { return pm.maxSizeForCompleteDownload } func (pm *pluginMock) GetBufferSizeForStreaming() int { return pm.bufferSizeForStreaming } diff --git a/server/handlers/handlers.go b/server/handlers/handlers.go index 48497c0fb..01dcf4319 100644 --- a/server/handlers/handlers.go +++ b/server/handlers/handlers.go @@ -38,6 +38,9 @@ type PluginIface interface { GetStore() store.Store GetMetrics() metrics.Metrics GetSyncDirectMessages() bool + GetSyncLinkedChannels() bool + GetSyncReactions() bool + GetSyncFileAttachments() bool GetSyncGuestUsers() bool GetMaxSizeForCompleteDownload() int GetBufferSizeForStreaming() int @@ -310,6 +313,11 @@ func (ah *ActivityHandler) handleCreatedActivity(msg *clientmodels.Message, subs } senderID, _ = ah.plugin.GetStore().TeamsToMattermostUserID(msg.UserID) } else { + if !ah.plugin.GetSyncLinkedChannels() { + // Skipping because linked channels are disabled + return metrics.DiscardedReasonLinkedChannelsDisabled + } + senderID, _ = ah.getOrCreateSyntheticUser(msteamsUser, true) channelLink, _ := ah.plugin.GetStore().GetLinkByMSTeamsChannelID(msg.TeamID, msg.ChannelID) if channelLink != nil { @@ -389,6 +397,11 @@ func (ah *ActivityHandler) handleUpdatedActivity(msg *clientmodels.Message, subs channelID := "" if chat == nil { + if !ah.plugin.GetSyncLinkedChannels() { + // Skipping because linked channels are disabled + return metrics.DiscardedReasonLinkedChannelsDisabled + } + var channelLink *storemodels.ChannelLink channelLink, err = ah.plugin.GetStore().GetLinkByMSTeamsChannelID(msg.TeamID, msg.ChannelID) if err != nil || channelLink == nil { @@ -456,6 +469,10 @@ func (ah *ActivityHandler) handleUpdatedActivity(msg *clientmodels.Message, subs } func (ah *ActivityHandler) handleReactions(postID, channelID string, isDirectMessage bool, reactions []clientmodels.Reaction) { + if !ah.plugin.GetSyncReactions() { + return + } + postReactions, appErr := ah.plugin.GetAPI().GetReactions(postID) if appErr != nil { return diff --git a/server/handlers/handlers_test.go b/server/handlers/handlers_test.go index dbeb00ca5..8c84c3b41 100644 --- a/server/handlers/handlers_test.go +++ b/server/handlers/handlers_test.go @@ -448,6 +448,45 @@ func TestHandleCreatedActivity(t *testing.T) { mockmetrics.On("ObserveMessage", metrics.ActionCreated, metrics.ActionSourceMSTeams, true).Times(1) }, }, + { + description: "Valid: sync linked channels disabled", + activityIds: clientmodels.ActivityIds{ + TeamID: "mockTeamID", + ChannelID: testutils.GetChannelID(), + MessageID: testutils.GetMessageID(), + }, + setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncLinkedChannels").Return(false).Times(1) + p.On("GetClientForApp").Return(client).Maybe() + p.On("GetAPI").Return(mockAPI).Maybe() + p.On("GetStore").Return(store).Maybe() + p.On("GetBotUserID").Return("mock-BotUserID").Times(1) + p.On("GetMetrics").Return(mockmetrics).Maybe() + }, + setupClient: func(client *mocksClient.Client) { + client.On("GetMessage", "mockTeamID", testutils.GetChannelID(), testutils.GetMessageID()).Return(&clientmodels.Message{ + ID: testutils.GetMessageID(), + UserID: testutils.GetSenderID(), + TeamID: "mockTeamID", + ChannelID: testutils.GetChannelID(), + UserDisplayName: "mockUserDisplayName", + Text: "mockText", + CreateAt: msteamsCreateAtTime, + LastUpdateAt: msteamsCreateAtTime, + }, nil).Times(1) + client.On("GetUser", testutils.GetSenderID()).Return(&clientmodels.User{ID: testutils.GetSenderID()}, nil).Once() + }, + setupAPI: func(mockAPI *plugintest.API) { + mockAPI.On("GetUser", testutils.GetUserID()).Return(testutils.GetUser(model.ChannelAdminRoleId, "test@test.com"), nil).Once() + mockAPI.On("CreatePost", testutils.GetPostFromTeamsMessage(mmCreateAtTime)).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), mmCreateAtTime), nil).Times(1) + }, + setupStore: func(store *mocksStore.Store) { + store.On("MattermostToTeamsUserID", "mock-BotUserID").Return(testutils.GetTeamsUserID(), nil).Times(1) + store.On("GetPostInfoByMSTeamsID", testutils.GetChannelID(), testutils.GetMessageID()).Return(nil, nil).Times(1) + }, + setupMetrics: func(mockmetrics *mocksMetrics.Metrics) { + }, + }, { description: "Valid: channel message", activityIds: clientmodels.ActivityIds{ @@ -456,6 +495,7 @@ func TestHandleCreatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncLinkedChannels").Return(true).Times(1) p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetStore").Return(store).Maybe() @@ -546,6 +586,7 @@ func TestHandleUpdatedActivity(t *testing.T) { ChatID: "invalid-ChatID", }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() }, @@ -564,6 +605,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) @@ -591,6 +633,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) p.On("GetAPI").Return(mockAPI).Maybe() @@ -618,6 +661,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) p.On("GetAPI").Return(mockAPI).Maybe() @@ -654,6 +698,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) p.On("GetStore").Return(store).Maybe() @@ -691,6 +736,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) p.On("GetAPI").Return(mockAPI).Maybe() @@ -736,6 +782,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(1) p.On("GetAPI").Return(mockAPI).Maybe() @@ -784,6 +831,7 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetClientForTeamsUser", testutils.GetTeamsUserID()).Return(client, nil).Times(2) p.On("GetAPI").Return(mockAPI).Maybe() @@ -831,6 +879,48 @@ func TestHandleUpdatedActivity(t *testing.T) { mockmetrics.On("ObserveMessage", metrics.ActionUpdated, metrics.ActionSourceMSTeams, true).Times(1) }, }, + { + description: "Valid: sync linked channels disabled", + activityIds: clientmodels.ActivityIds{ + TeamID: "mockTeamID", + ChannelID: testutils.GetChannelID(), + MessageID: testutils.GetMessageID(), + }, + setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncLinkedChannels").Return(false).Once() + p.On("GetSyncReactions").Return(true).Maybe() + p.On("GetClientForApp").Return(client).Maybe() + p.On("GetAPI").Return(mockAPI).Maybe() + p.On("GetStore").Return(store).Maybe() + p.On("GetBotUserID").Return("mock-BotUserID").Times(1) + p.On("GetMetrics").Return(mockmetrics).Maybe() + }, + setupClient: func(client *mocksClient.Client) { + client.On("GetMessage", "mockTeamID", testutils.GetChannelID(), testutils.GetMessageID()).Return(&clientmodels.Message{ + ID: testutils.GetMessageID(), + UserID: testutils.GetSenderID(), + TeamID: "mockTeamID", + ChannelID: testutils.GetChannelID(), + UserDisplayName: "mockUserDisplayName", + Text: "mockText", + LastUpdateAt: msTeamsLastUpdateAtTime, + }, nil).Times(1) + }, + setupAPI: func(mockAPI *plugintest.API) { + mockAPI.On("GetPost", "mockMattermostID").Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetSenderID(), time.Now().UnixMicro()), nil).Times(1) + mockAPI.On("GetReactions", "mockMattermostID").Return([]*model.Reaction{}, nil).Times(1) + mockAPI.On("GetUser", testutils.GetUserID()).Return(testutils.GetUser(model.ChannelAdminRoleId, "test@test.com"), nil).Once() + }, + setupStore: func(store *mocksStore.Store) { + store.On("MattermostToTeamsUserID", "mock-BotUserID").Return(testutils.GetTeamsUserID(), nil).Times(1) + store.On("GetPostInfoByMSTeamsID", testutils.GetChannelID(), testutils.GetMessageID()).Return(&storemodels.PostInfo{ + MSTeamsLastUpdateAt: time.Now(), + MattermostID: "mockMattermostID", + }, nil).Times(1) + }, + setupMetrics: func(mockmetrics *mocksMetrics.Metrics) { + }, + }, { description: "Valid: channel message", activityIds: clientmodels.ActivityIds{ @@ -839,6 +929,8 @@ func TestHandleUpdatedActivity(t *testing.T) { MessageID: testutils.GetMessageID(), }, setupPlugin: func(p *mocksPlugin.PluginIface, client *mocksClient.Client, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncLinkedChannels").Return(true).Once() + p.On("GetSyncReactions").Return(true).Maybe() p.On("GetClientForApp").Return(client).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetStore").Return(store).Maybe() @@ -1005,10 +1097,22 @@ func TestHandleReactions(t *testing.T) { setupStore func(*mocksStore.Store) setupMetrics func(*mocksMetrics.Metrics) }{ + { + description: "Disabled by configuration", + reactions: []clientmodels.Reaction{}, + setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(false).Once() + }, + setupAPI: func(mockAPI *plugintest.API) { + }, + setupStore: func(store *mocksStore.Store) {}, + setupMetrics: func(mockmetrics *mocksMetrics.Metrics) {}, + }, { description: "Reactions list is empty", reactions: []clientmodels.Reaction{}, setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Once() p.On("GetAPI").Return(mockAPI).Maybe() }, setupAPI: func(mockAPI *plugintest.API) { @@ -1026,6 +1130,7 @@ func TestHandleReactions(t *testing.T) { }, }, setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Once() p.On("GetAPI").Return(mockAPI).Maybe() }, setupAPI: func(mockAPI *plugintest.API) { @@ -1043,6 +1148,7 @@ func TestHandleReactions(t *testing.T) { }, }, setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Once() p.On("GetStore").Return(store).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetMetrics").Return(mockmetrics).Maybe() @@ -1079,6 +1185,7 @@ func TestHandleReactions(t *testing.T) { }, }, setupPlugin: func(p *mocksPlugin.PluginIface, mockAPI *plugintest.API, store *mocksStore.Store, mockmetrics *mocksMetrics.Metrics) { + p.On("GetSyncReactions").Return(true).Once() p.On("GetStore").Return(store).Maybe() p.On("GetAPI").Return(mockAPI).Maybe() p.On("GetMetrics").Return(mockmetrics).Maybe() diff --git a/server/handlers/mocks/PluginIface.go b/server/handlers/mocks/PluginIface.go index 6be2c4754..2362ab32b 100644 --- a/server/handlers/mocks/PluginIface.go +++ b/server/handlers/mocks/PluginIface.go @@ -198,6 +198,20 @@ func (_m *PluginIface) GetSyncDirectMessages() bool { return r0 } +// GetSyncFileAttachments provides a mock function with given fields: +func (_m *PluginIface) GetSyncFileAttachments() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // GetSyncGuestUsers provides a mock function with given fields: func (_m *PluginIface) GetSyncGuestUsers() bool { ret := _m.Called() @@ -212,6 +226,34 @@ func (_m *PluginIface) GetSyncGuestUsers() bool { return r0 } +// GetSyncLinkedChannels provides a mock function with given fields: +func (_m *PluginIface) GetSyncLinkedChannels() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GetSyncReactions provides a mock function with given fields: +func (_m *PluginIface) GetSyncReactions() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // GetURL provides a mock function with given fields: func (_m *PluginIface) GetURL() string { ret := _m.Called() diff --git a/server/message_hooks.go b/server/message_hooks.go index c1bcddc6c..567d84b63 100644 --- a/server/message_hooks.go +++ b/server/message_hooks.go @@ -37,6 +37,13 @@ func (p *Plugin) UserWillLogIn(_ *plugin.Context, user *model.User) string { } func (p *Plugin) MessageHasBeenPosted(_ *plugin.Context, post *model.Post) { + channel, appErr := p.API.GetChannel(post.ChannelId) + if appErr != nil { + return + } + + isDirectOrGroupMessage := channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup + if post.Props != nil { if _, ok := post.Props["msteams_sync_"+p.userID].(bool); ok { return @@ -47,38 +54,47 @@ func (p *Plugin) MessageHasBeenPosted(_ *plugin.Context, post *model.Post) { return } - link, err := p.store.GetLinkByChannelID(post.ChannelId) - if err != nil || link == nil { - channel, appErr := p.API.GetChannel(post.ChannelId) + if isDirectOrGroupMessage { + if !p.getConfiguration().SyncDirectMessages { + return + } + + members, appErr := p.API.GetChannelMembers(post.ChannelId, 0, math.MaxInt32) if appErr != nil { return } - if (channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup) && p.getConfiguration().SyncDirectMessages { - members, appErr := p.API.GetChannelMembers(post.ChannelId, 0, math.MaxInt32) - if appErr != nil { - return - } - dstUsers := []string{} - for _, m := range members { - dstUsers = append(dstUsers, m.UserId) - } - _, err = p.SendChat(post.UserId, dstUsers, post) - if err != nil { - p.API.LogWarn("Unable to handle message sent", "error", err.Error()) - } + dstUsers := []string{} + for _, m := range members { + dstUsers = append(dstUsers, m.UserId) + } + _, err := p.SendChat(post.UserId, dstUsers, post) + if err != nil { + p.API.LogWarn("Unable to handle message sent", "error", err.Error()) + } + } else { + link, err := p.store.GetLinkByChannelID(post.ChannelId) + if err != nil || link == nil { + return + } + + if !p.getConfiguration().SyncLinkedChannels { + return } - return - } - user, _ := p.API.GetUser(post.UserId) + user, _ := p.API.GetUser(post.UserId) - _, err = p.Send(link.MSTeamsTeam, link.MSTeamsChannel, user, post) - if err != nil { - p.API.LogWarn("Unable to handle message sent", "error", err.Error()) + _, err = p.Send(link.MSTeamsTeam, link.MSTeamsChannel, user, post) + if err != nil { + p.API.LogWarn("Unable to handle message sent", "error", err.Error()) + } } } func (p *Plugin) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) { + if !p.getConfiguration().SyncReactions { + return + } + updateRequired := true if c.RequestId == "" { _, ignoreHookForReaction := p.activityHandler.IgnorePluginHooksMap.LoadAndDelete(fmt.Sprintf("%s_%s_%s", reaction.PostId, reaction.UserId, reaction.EmojiName)) @@ -119,6 +135,10 @@ func (p *Plugin) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reactio } func (p *Plugin) ReactionHasBeenRemoved(_ *plugin.Context, reaction *model.Reaction) { + if !p.getConfiguration().SyncReactions { + return + } + if reaction.ChannelId == "removedfromplugin" { return } @@ -209,6 +229,10 @@ func (p *Plugin) MessageHasBeenUpdated(c *plugin.Context, newPost, oldPost *mode return } + if !p.getConfiguration().SyncLinkedChannels { + return + } + err = p.Update(link.MSTeamsTeam, link.MSTeamsChannel, user, newPost, oldPost, updateRequired) if err != nil { p.API.LogWarn("Unable to handle message update", "error", err.Error()) @@ -448,6 +472,10 @@ func (p *Plugin) SendChat(srcUser string, usersIDs []string, post *model.Post) ( var attachments []*clientmodels.Attachment for _, fileID := range post.FileIds { + if !p.GetSyncFileAttachments() { + continue + } + fileInfo, appErr := p.API.GetFileInfo(fileID) if appErr != nil { p.API.LogWarn("Unable to get file info", "error", appErr) @@ -542,6 +570,10 @@ func (p *Plugin) Send(teamID, channelID string, user *model.User, post *model.Po var attachments []*clientmodels.Attachment for _, fileID := range post.FileIds { + if !p.GetSyncFileAttachments() { + continue + } + fileInfo, appErr := p.API.GetFileInfo(fileID) if appErr != nil { p.API.LogWarn("Unable to get file info", "error", appErr) diff --git a/server/message_hooks_test.go b/server/message_hooks_test.go index 086613bcf..a16f5eeb7 100644 --- a/server/message_hooks_test.go +++ b/server/message_hooks_test.go @@ -31,13 +31,30 @@ func TestReactionHasBeenAdded(t *testing.T) { } for _, test := range []struct { Name string + SetupPlugin func(*Plugin) SetupAPI func(*plugintest.API) SetupStore func(*storemocks.Store) SetupClient func(*clientmocks.Client, *clientmocks.Client) SetupMetrics func(*metricsmocks.Metrics) }{ { - Name: "ReactionHasBeenAdded: Unable to get the post info", + Name: "ReactionHasBeenAdded: disabled by configuration", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = false + }, + SetupAPI: func(api *plugintest.API) {}, + SetupStore: func(store *storemocks.Store) { + }, + SetupClient: func(client *clientmocks.Client, uclient *clientmocks.Client) {}, + SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {}, + }, + { + Name: "ReactionHasBeenAdded: Unable to get the post info", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) {}, SetupStore: func(store *storemocks.Store) { store.On("GetPostInfoByMattermostID", testutils.GetID()).Return(nil, nil).Times(1) @@ -47,6 +64,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Unable to get the link by channel ID", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) }, @@ -61,6 +82,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Unable to get the link by channel ID and channel", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(nil, testutils.GetInternalServerAppError("unable to get the channel")).Times(1) }, @@ -73,6 +98,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Unable to get the post", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get the post")).Times(1) @@ -86,6 +115,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Unable to set the reaction", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -107,6 +140,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Unable to set the post last updateAt time", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -130,6 +167,10 @@ func TestReactionHasBeenAdded(t *testing.T) { }, { Name: "ReactionHasBeenAdded: Valid", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -154,7 +195,7 @@ func TestReactionHasBeenAdded(t *testing.T) { } { t.Run(test.Name, func(t *testing.T) { p := newTestPlugin(t) - p.configuration.SyncDirectMessages = true + test.SetupPlugin(p) test.SetupAPI(p.API.(*plugintest.API)) test.SetupStore(p.store.(*storemocks.Store)) test.SetupClient(p.msteamsAppClient.(*clientmocks.Client), p.clientBuilderWithToken("", "", "", "", nil, nil).(*clientmocks.Client)) @@ -173,13 +214,30 @@ func TestReactionHasBeenRemoved(t *testing.T) { } for _, test := range []struct { Name string + SetupPlugin func(*Plugin) SetupAPI func(*plugintest.API) SetupStore func(*storemocks.Store) SetupClient func(*clientmocks.Client, *clientmocks.Client) SetupMetrics func(*metricsmocks.Metrics) }{ { - Name: "ReactionHasBeenRemoved: Unable to get the post info", + Name: "ReactionHasBeenRemoved: disabled by configuration", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = false + }, + SetupAPI: func(api *plugintest.API) {}, + SetupStore: func(store *storemocks.Store) { + }, + SetupClient: func(client *clientmocks.Client, uclient *clientmocks.Client) {}, + SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {}, + }, + { + Name: "ReactionHasBeenRemoved: Unable to get the post info", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) {}, SetupStore: func(store *storemocks.Store) { store.On("GetPostInfoByMattermostID", testutils.GetID()).Return(nil, nil).Times(1) @@ -189,6 +247,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Unable to get the post", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetPost", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get the post")).Times(1) }, @@ -202,6 +264,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Unable to get the link by channel ID", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) @@ -219,6 +285,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Unable to get the link by channel ID and channel", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(nil, testutils.GetInternalServerAppError("unable to get the channel")).Times(1) @@ -234,6 +304,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Unable to remove the reaction", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -262,6 +336,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Unable to set the post last updateAt time", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -292,6 +370,10 @@ func TestReactionHasBeenRemoved(t *testing.T) { }, { Name: "ReactionHasBeenRemoved: Valid", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncReactions = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) api.On("GetPost", testutils.GetID()).Return(testutils.GetPost(testutils.GetChannelID(), testutils.GetUserID(), time.Now().UnixMicro()), nil).Times(1) @@ -323,7 +405,7 @@ func TestReactionHasBeenRemoved(t *testing.T) { } { t.Run(test.Name, func(t *testing.T) { p := newTestPlugin(t) - p.configuration.SyncDirectMessages = true + test.SetupPlugin(p) test.SetupAPI(p.API.(*plugintest.API)) test.SetupStore(p.store.(*storemocks.Store)) test.SetupClient(p.msteamsAppClient.(*clientmocks.Client), p.clientBuilderWithToken("", "", "", "", nil, nil).(*clientmocks.Client)) @@ -355,6 +437,7 @@ func TestMessageHasBeenUpdated(t *testing.T) { } for _, test := range []struct { Name string + SetupPlugin func(*Plugin) SetupAPI func(*plugintest.API) SetupStore func(*storemocks.Store) SetupClient func(*clientmocks.Client, *clientmocks.Client) @@ -362,6 +445,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }{ { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) @@ -394,6 +481,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID and channel", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(nil, testutils.GetInternalServerAppError("unable to get the channel")).Times(1) @@ -407,6 +498,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID and channel type is Open", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeOpen), nil).Times(1) @@ -420,6 +515,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID and unable to get channel members", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) @@ -434,6 +533,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID and unable to update the chat", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) @@ -454,6 +557,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Unable to get the link by channel ID and unable to create or get chat for users", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetChannel", testutils.GetChannelID()).Return(testutils.GetChannel(model.ChannelTypeDirect), nil).Times(1) @@ -475,6 +582,10 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Able to get the link by channel ID", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: model.NewString("/")}}, nil).Times(2) @@ -508,6 +619,46 @@ func TestMessageHasBeenUpdated(t *testing.T) { }, { Name: "MessageHasBeenUpdated: Able to get the link by channel ID but unable to update post", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = true + }, + SetupAPI: func(api *plugintest.API) { + api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) + api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: model.NewString("/")}}, nil).Times(2) + api.On("KVSetWithOptions", "mutex_post_mutex_"+testutils.GetID(), mock.Anything, mock.Anything).Return(true, nil).Times(2) + }, + SetupStore: func(store *storemocks.Store) { + store.On("GetTokenForMattermostUser", testutils.GetID()).Return(&fakeToken, nil).Times(2) + store.On("GetLinkByChannelID", testutils.GetChannelID()).Return(&storemodels.ChannelLink{ + MattermostTeamID: "mockMattermostTeamID", + MattermostChannelID: "mockMattermostChannelID", + MSTeamsTeam: "mockTeamsTeamID", + MSTeamsChannel: "mockTeamsChannelID", + }, nil).Times(1) + store.On("GetPostInfoByMattermostID", testutils.GetID()).Return(&storemodels.PostInfo{ + MattermostID: testutils.GetID(), + }, nil).Times(2) + store.On("LinkPosts", storemodels.PostInfo{ + MattermostID: testutils.GetID(), + MSTeamsID: "mockTeamsTeamID", + MSTeamsChannel: "mockTeamsChannelID", + }).Return(nil).Times(1) + }, + SetupClient: func(client *clientmocks.Client, uclient *clientmocks.Client) { + uclient.On("UpdateMessage", "mockTeamsTeamID", "mockTeamsChannelID", "", "", "", []models.ChatMessageMentionable{}).Return(nil, errors.New("unable to update the post")).Times(1) + }, + SetupMetrics: func(mockmetrics *metricsmocks.Metrics) { + mockmetrics.On("ObserveMessage", metrics.ActionUpdated, metrics.ActionSourceMattermost, false).Times(1) + mockmetrics.On("ObserveMSGraphClientMethodDuration", "Client.UpdateMessage", "false", mock.AnythingOfType("float64")).Once() + }, + }, + { + Name: "MessageHasBeenUpdated: Sync linked channels disabled", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncDirectMessages = true + p.configuration.SyncLinkedChannels = false + }, SetupAPI: func(api *plugintest.API) { api.On("GetUser", testutils.GetID()).Return(testutils.GetUser(model.SystemAdminRoleId, "test@test.com"), nil).Times(1) api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: model.NewString("/")}}, nil).Times(2) @@ -541,7 +692,7 @@ func TestMessageHasBeenUpdated(t *testing.T) { } { t.Run(test.Name, func(t *testing.T) { p := newTestPlugin(t) - p.configuration.SyncDirectMessages = true + test.SetupPlugin(p) test.SetupAPI(p.API.(*plugintest.API)) test.SetupStore(p.store.(*storemocks.Store)) test.SetupClient(p.msteamsAppClient.(*clientmocks.Client), p.clientBuilderWithToken("", "", "", "", nil, nil).(*clientmocks.Client)) @@ -1088,6 +1239,7 @@ func TestSendChat(t *testing.T) { } for _, test := range []struct { Name string + SetupPlugin func(*Plugin) SetupAPI func(*plugintest.API) SetupStore func(*storemocks.Store) SetupClient func(*clientmocks.Client, *clientmocks.Client) @@ -1096,7 +1248,10 @@ func TestSendChat(t *testing.T) { ExpectedError string }{ { - Name: "SendChat: Unable to get the source user ID", + Name: "SendChat: Unable to get the source user ID", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) {}, SetupStore: func(store *storemocks.Store) { store.On("GetPostInfoByMattermostID", "mockRootID").Return(nil, nil).Once() @@ -1109,6 +1264,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to get the client", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("SendEphemeralPost", testutils.GetUserID(), &model.Post{ UserId: "bot-user-id", @@ -1129,6 +1287,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to create or get the chat", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { }, SetupStore: func(store *storemocks.Store) { @@ -1146,6 +1307,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to send the chat", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1176,6 +1340,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Able to send the chat and not able to store the post", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1214,6 +1381,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to get the parent message", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1254,8 +1424,43 @@ func TestSendChat(t *testing.T) { }, ExpectedMessage: "mockMessageID", }, + { + Name: "SendChat: File attachments disabled", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = false + }, + SetupAPI: func(api *plugintest.API) { + }, + SetupStore: func(store *storemocks.Store) { + store.On("GetPostInfoByMattermostID", "mockRootID").Return(nil, nil).Once() + store.On("MattermostToTeamsUserID", testutils.GetID()).Return(testutils.GetID(), nil).Times(3) + store.On("GetTokenForMattermostUser", testutils.GetID()).Return(&fakeToken, nil).Times(1) + store.On("LinkPosts", storemodels.PostInfo{ + MattermostID: testutils.GetID(), + MSTeamsChannel: testutils.GetChatID(), + MSTeamsID: "mockMessageID", + }).Return(nil).Times(1) + }, + SetupClient: func(client *clientmocks.Client, uclient *clientmocks.Client) { + uclient.On("CreateOrGetChatForUsers", mock.AnythingOfType("[]string")).Return(mockChat, nil).Times(1) + uclient.On("GetChat", testutils.GetChatID()).Return(mockChat, nil).Times(1) + uclient.On("SendChat", testutils.GetChatID(), "

mockMessage??????????

\n", (*clientmodels.Message)(nil), ([]*clientmodels.Attachment)(nil), []models.ChatMessageMentionable{}).Return(&clientmodels.Message{ + ID: "mockMessageID", + }, nil).Times(1) + }, + SetupMetrics: func(mockmetrics *metricsmocks.Metrics) { + mockmetrics.On("ObserveMessage", metrics.ActionCreated, metrics.ActionSourceMattermost, true).Times(1) + mockmetrics.On("ObserveMSGraphClientMethodDuration", "Client.CreateOrGetChatForUsers", "true", mock.AnythingOfType("float64")).Once() + mockmetrics.On("ObserveMSGraphClientMethodDuration", "Client.GetChat", "true", mock.AnythingOfType("float64")).Once() + mockmetrics.On("ObserveMSGraphClientMethodDuration", "Client.SendChat", "true", mock.AnythingOfType("float64")).Once() + }, + ExpectedMessage: "mockMessageID", + }, { Name: "SendChat: Unable to get the file info", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get file attachment")).Times(1) }, @@ -1287,6 +1492,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to get the file attachment from Mattermost", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get the file attachment from Mattermost")).Times(1) @@ -1319,6 +1527,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Unable to upload the attachments", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1367,6 +1578,9 @@ func TestSendChat(t *testing.T) { }, { Name: "SendChat: Valid", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1407,6 +1621,7 @@ func TestSendChat(t *testing.T) { t.Run(test.Name, func(t *testing.T) { assert := assert.New(t) p := newTestPlugin(t) + test.SetupPlugin(p) test.SetupAPI(p.API.(*plugintest.API)) test.SetupStore(p.store.(*storemocks.Store)) test.SetupClient(p.msteamsAppClient.(*clientmocks.Client), p.clientBuilderWithToken("", "", "", "", nil, nil).(*clientmocks.Client)) @@ -1429,6 +1644,7 @@ func TestSendChat(t *testing.T) { func TestSend(t *testing.T) { for _, test := range []struct { Name string + SetupPlugin func(*Plugin) SetupAPI func(*plugintest.API) SetupStore func(*storemocks.Store) SetupClient func(*clientmocks.Client, *clientmocks.Client) @@ -1437,7 +1653,10 @@ func TestSend(t *testing.T) { ExpectedError string }{ { - Name: "Send: Unable to get the client", + Name: "Send: Unable to get the client", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) {}, SetupStore: func(store *storemocks.Store) { store.On("GetTokenForMattermostUser", testutils.GetID()).Return(nil, nil).Times(1) @@ -1447,8 +1666,37 @@ func TestSend(t *testing.T) { SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {}, ExpectedError: "not connected user", }, + { + Name: "Send: File attachments disabled", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = false + }, + SetupAPI: func(api *plugintest.API) { + }, + SetupStore: func(store *storemocks.Store) { + store.On("GetTokenForMattermostUser", testutils.GetID()).Return(&fakeToken, nil).Times(1) + store.On("LinkPosts", storemodels.PostInfo{ + MattermostID: testutils.GetID(), + MSTeamsID: "mockMessageID", + MSTeamsChannel: testutils.GetChannelID(), + }).Return(nil).Times(1) + }, + SetupClient: func(client *clientmocks.Client, uclient *clientmocks.Client) { + uclient.On("SendMessageWithAttachments", testutils.GetID(), testutils.GetChannelID(), "", "

mockMessage??????????

\n", ([]*clientmodels.Attachment)(nil), []models.ChatMessageMentionable{}).Return(&clientmodels.Message{ + ID: "mockMessageID", + }, nil).Times(1) + }, + SetupMetrics: func(mockmetrics *metricsmocks.Metrics) { + mockmetrics.On("ObserveMessage", metrics.ActionCreated, metrics.ActionSourceMattermost, false).Times(1) + mockmetrics.On("ObserveMSGraphClientMethodDuration", "Client.SendMessageWithAttachments", "true", mock.AnythingOfType("float64")).Once() + }, + ExpectedMessage: "mockMessageID", + }, { Name: "Send: Unable to get the file info", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get file attachment")).Times(1) }, @@ -1474,6 +1722,9 @@ func TestSend(t *testing.T) { }, { Name: "Send: Unable to get file attachment from Mattermost", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return(nil, testutils.GetInternalServerAppError("unable to get the file attachment from Mattermost")).Times(1) @@ -1500,6 +1751,9 @@ func TestSend(t *testing.T) { }, { Name: "Send: Unable to send message with attachments", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1527,6 +1781,9 @@ func TestSend(t *testing.T) { }, { Name: "Send: Able to send message with attachments but unable to store posts", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1561,6 +1818,9 @@ func TestSend(t *testing.T) { }, { Name: "Send: Able to send message with attachments with no error", + SetupPlugin: func(p *Plugin) { + p.configuration.SyncFileAttachments = true + }, SetupAPI: func(api *plugintest.API) { api.On("GetFileInfo", testutils.GetID()).Return(testutils.GetFileInfo(), nil).Times(1) api.On("GetFile", testutils.GetID()).Return([]byte("mockData"), nil).Times(1) @@ -1597,6 +1857,7 @@ func TestSend(t *testing.T) { t.Run(test.Name, func(t *testing.T) { assert := assert.New(t) p := newTestPlugin(t) + test.SetupPlugin(p) test.SetupAPI(p.API.(*plugintest.API)) test.SetupStore(p.store.(*storemocks.Store)) test.SetupClient(p.msteamsAppClient.(*clientmocks.Client), p.clientBuilderWithToken("", "", "", "", nil, nil).(*clientmocks.Client)) diff --git a/server/metrics/metrics.go b/server/metrics/metrics.go index d485aa399..a57002636 100644 --- a/server/metrics/metrics.go +++ b/server/metrics/metrics.go @@ -42,6 +42,7 @@ const ( DiscardedReasonNotUserEvent = "no_user_event" DiscardedReasonOther = "other" DiscardedReasonDirectMessagesDisabled = "direct_messages_disabled" + DiscardedReasonLinkedChannelsDisabled = "linked_channels_disabled" DiscardedReasonInactiveUser = "inactive_user" DiscardedReasonDuplicatedPost = "duplicated_post" DiscardedReasonAlreadyAppliedChange = "already_applied_change" diff --git a/server/plugin.go b/server/plugin.go index b7dba562f..3d2e6e913 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -111,6 +111,18 @@ func (p *Plugin) GetSyncDirectMessages() bool { return p.getConfiguration().SyncDirectMessages } +func (p *Plugin) GetSyncLinkedChannels() bool { + return p.getConfiguration().SyncLinkedChannels +} + +func (p *Plugin) GetSyncReactions() bool { + return p.getConfiguration().SyncReactions +} + +func (p *Plugin) GetSyncFileAttachments() bool { + return p.getConfiguration().SyncFileAttachments +} + func (p *Plugin) GetSyncGuestUsers() bool { return p.getConfiguration().SyncGuestUsers } @@ -314,6 +326,14 @@ func (p *Plugin) start(isRestart bool) { // Run the job above right away so we immediately populate metrics. p.checkCredentials() + + // Unregister and re-register slash command to reflect any configuration changes. + if err = p.API.UnregisterCommand("", "msteams-sync"); err != nil { + p.API.LogWarn("Failed to unregister command", "error", err) + } + if err = p.API.RegisterCommand(p.createMsteamsSyncCommand(p.getConfiguration().SyncLinkedChannels)); err != nil { + p.API.LogError("Failed to register command", "error", err) + } } func (p *Plugin) getBase64Certificate() string { @@ -466,10 +486,6 @@ func (p *Plugin) OnActivate() error { } p.userID = botID - if err = p.API.RegisterCommand(p.createMsteamsSyncCommand()); err != nil { - return err - } - if p.store == nil { if p.apiClient.Store.DriverName() != model.DatabaseDriverPostgres { return fmt.Errorf("unsupported database driver: %s", p.apiClient.Store.DriverName()) diff --git a/server/testutils/containere2e/containere2e.go b/server/testutils/containere2e/containere2e.go index 80e196300..c185f0ef4 100644 --- a/server/testutils/containere2e/containere2e.go +++ b/server/testutils/containere2e/containere2e.go @@ -63,6 +63,7 @@ func NewE2ETestPlugin(t *testing.T, extraOptions ...mmcontainer.MattermostCustom "maxsizeforcompletedownload": 20, "tenantid": "tenant-id", "webhooksecret": "webhook-secret", + "synclinkedchannels": true, } options := []mmcontainer.MattermostCustomizeRequestOption{