Skip to content

Commit

Permalink
Prototype chat notifications (#654)
Browse files Browse the repository at this point in the history
  • Loading branch information
lieut-data authored May 20, 2024
1 parent c3ed607 commit 766cd1c
Show file tree
Hide file tree
Showing 23 changed files with 664 additions and 75 deletions.
7 changes: 7 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"help_text": "Set the value to 'true' to sync MS Teams guest users",
"default": false
},
{
"key": "syncNotifications",
"display_name": "Sync notifications",
"type": "bool",
"help_text": "Sync notifications for any connected user. Enabling this setting disables syncing of direct messages, group messages, and linked channels.",
"default": false
},
{
"key": "syncDirectMessages",
"display_name": "Sync direct messages",
Expand Down
9 changes: 1 addition & 8 deletions server/automute.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,7 @@ func (p *Plugin) getAutomuteIsEnabledForUser(userID string) bool {

// setAutomuteIsEnabledForUser sets a preference to track if we've muted all of the user's linked channels.
func (p *Plugin) setAutomuteIsEnabledForUser(userID string, channelsAutomuted bool) error {
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{
{
UserId: userID,
Category: PreferenceCategoryPlugin,
Name: PreferenceNameAutomuteEnabled,
Value: strconv.FormatBool(channelsAutomuted),
},
})
appErr := p.updatePreferenceForUser(userID, PreferenceNameAutomuteEnabled, strconv.FormatBool(channelsAutomuted))
if appErr != nil {
return errors.Wrap(appErr, fmt.Sprintf("Unable to set preference to track that channels are automuted for user %s", userID))
}
Expand Down
73 changes: 69 additions & 4 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ func (p *Plugin) createCommand(syncLinkedChannels bool) *model.Command {
p.API.LogWarn("Unable to get the MS Teams icon for the slash command")
}

autoCompleteData := getAutocompleteData(syncLinkedChannels)
p.subCommandsMutex.Lock()
defer p.subCommandsMutex.Unlock()
p.subCommands = make([]string, 0, len(autoCompleteData.SubCommands))
for i := range autoCompleteData.SubCommands {
p.subCommands = append(p.subCommands, autoCompleteData.SubCommands[i].Trigger)
}

return &model.Command{
Trigger: msteamsCommand,
AutoComplete: true,
AutoCompleteDesc: "Manage synced channels between MS Teams and Mattermost",
AutoCompleteHint: "[command]",
Username: botUsername,
DisplayName: botDisplayName,
AutocompleteData: getAutocompleteData(syncLinkedChannels),
AutocompleteData: autoCompleteData,
AutocompleteIconData: iconData,
}
}
Expand Down Expand Up @@ -98,6 +106,14 @@ func getAutocompleteData(syncLinkedChannels bool) *model.AutocompleteData {
promoteUser.RoleID = model.SystemAdminRoleId
cmd.AddCommand(promoteUser)

notifications := model.NewAutocompleteData("notifications", "", "Enable or disable notifications from MSTeams. You must be connected to perform this action.")
notifications.AddStaticListArgument("status", true, []model.AutocompleteListItem{
{Item: "status", HelpText: "Show current notification status."},
{Item: "on", HelpText: "Enable notifications."},
{Item: "off", HelpText: "Disable notifications."},
})
cmd.AddCommand(notifications)

return cmd
}

Expand Down Expand Up @@ -159,10 +175,14 @@ func (p *Plugin) ExecuteCommand(_ *plugin.Context, args *model.CommandArgs) (*mo
return p.executeStatusCommand(args)
}

if p.getConfiguration().SyncLinkedChannels {
return p.cmdError(args, "Unknown command. Valid options: link, unlink, show, show-links, connect, connect-bot, status, disconnect, disconnect-bot and promote.")
if action == "notifications" {
return p.executeNotificationsCommand(args, parameters)
}
return p.cmdError(args, "Unknown command. Valid options: connect, connect-bot, status, disconnect, disconnect-bot and promote.")

p.subCommandsMutex.RLock()
list := strings.Join(p.subCommands, ", ")
p.subCommandsMutex.RUnlock()
return p.cmdError(args, "Unknown command. Valid options: "+list)
}

func (p *Plugin) executeLinkCommand(args *model.CommandArgs, parameters []string) (*model.CommandResponse, *model.AppError) {
Expand Down Expand Up @@ -613,6 +633,51 @@ func (p *Plugin) executeStatusCommand(args *model.CommandArgs) (*model.CommandRe
return p.cmdSuccess(args, "Your account is not connected to Teams.")
}

func (p *Plugin) executeNotificationsCommand(args *model.CommandArgs, parameters []string) (*model.CommandResponse, *model.AppError) {
if len(parameters) != 1 {
return p.cmdSuccess(args, "Invalid notifications command, one argument is required.")
}

isConnected, err := p.isUserConnected(args.UserId)
if err != nil {
p.API.LogWarn("unable to check if the user is connected", "error", err.Error())
return p.cmdError(args, "Error: Unable to get the connection status")
}
if !isConnected {
return p.cmdSuccess(args, "Error: Your account is not connected to Teams. To use this feature, please connect your account with `/msteams connect`.")
}

notificationPreferenceEnabled := p.getNotificationPreference(args.UserId)
switch strings.ToLower(parameters[0]) {
case "status":
status := "disabled"
if notificationPreferenceEnabled {
status = "enabled"
}
return p.cmdSuccess(args, fmt.Sprintf("Notifications from MSTeams are currently %s.", status))
case "on":
if !notificationPreferenceEnabled {
err = p.setNotificationPreference(args.UserId, true)
if err != nil {
p.API.LogWarn("unable to enable notifications", "error", err.Error())
return p.cmdError(args, "Error: Unable to enable notifications.")
}
}
return p.cmdSuccess(args, "Notifications from MSTeams are now enabled.")
case "off":
if notificationPreferenceEnabled {
err = p.setNotificationPreference(args.UserId, false)
if err != nil {
p.API.LogWarn("unable to disable notifications", "error", err.Error())
return p.cmdError(args, "Error: Unable to disable notifications.")
}
}
return p.cmdSuccess(args, "Notifications from MSTeams are now disabled.")
}

return p.cmdSuccess(args, parameters[0]+" is not a valid argument.")
}

func getAutocompletePath(path string) string {
return "plugins/" + pluginID + "/autocomplete/" + path
}
196 changes: 196 additions & 0 deletions server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,35 @@ func TestGetAutocompleteData(t *testing.T) {
},
SubCommands: []*model.AutocompleteData{},
},
{
Trigger: "notifications",
HelpText: "Enable or disable notifications from MSTeams. You must be connected to perform this action.",
RoleID: model.SystemUserRoleId,
Arguments: []*model.AutocompleteArg{
{
Required: true,
Type: model.AutocompleteArgTypeStaticList,
HelpText: "status",
Data: &model.AutocompleteStaticListArg{
PossibleArguments: []model.AutocompleteListItem{
{
Item: "status",
HelpText: "Show current notification status.",
},
{
Item: "on",
HelpText: "Enable notifications.",
},
{
Item: "off",
HelpText: "Disable notifications.",
},
},
},
},
},
SubCommands: []*model.AutocompleteData{},
},
},
},
},
Expand Down Expand Up @@ -1135,6 +1164,35 @@ func TestGetAutocompleteData(t *testing.T) {
},
SubCommands: []*model.AutocompleteData{},
},
{
Trigger: "notifications",
HelpText: "Enable or disable notifications from MSTeams. You must be connected to perform this action.",
RoleID: model.SystemUserRoleId,
Arguments: []*model.AutocompleteArg{
{
Required: true,
Type: model.AutocompleteArgTypeStaticList,
HelpText: "status",
Data: &model.AutocompleteStaticListArg{
PossibleArguments: []model.AutocompleteListItem{
{
Item: "status",
HelpText: "Show current notification status.",
},
{
Item: "on",
HelpText: "Enable notifications.",
},
{
Item: "off",
HelpText: "Disable notifications.",
},
},
},
},
},
SubCommands: []*model.AutocompleteData{},
},
},
},
},
Expand Down Expand Up @@ -1378,3 +1436,141 @@ func TestStatusCommand(t *testing.T) {
assertEphemeralResponse(th, t, args, "Your account is connected to Teams.")
})
}

func TestNotificationCommand(t *testing.T) {
th := setupTestHelper(t)

team := th.SetupTeam(t)
user1 := th.SetupUser(t, team)
args := &model.CommandArgs{
UserId: user1.Id,
ChannelId: model.NewId(),
}

th.SetupWebsocketClientForUser(t, user1.Id)

reset := func(th *testHelper, t *testing.T, connectUser bool) {
t.Helper()
th.Reset(t)

if connectUser {
th.ConnectUser(t, user1.Id)
}

err := th.p.API.DeletePreferencesForUser(user1.Id, []model.Preference{{
UserId: user1.Id,
Category: PreferenceCategoryPlugin,
Name: storemodels.PreferenceNameNotification,
}})
require.Nil(t, err)
}

t.Run("not connected user should be rejected", func(t *testing.T) {
reset(th, t, false)
subCommands := []string{"status", "on", "off"}
for _, subCommand := range subCommands {
t.Run("subcommand "+subCommand, func(t *testing.T) {
commandResponse, appErr := th.p.executeNotificationsCommand(args, []string{subCommand})
require.Nil(t, appErr)
assertNoCommandResponse(t, commandResponse)
assertEphemeralResponse(th, t, args, "Error: Your account is not connected to Teams. To use this feature, please connect your account with `/msteams connect`.")
})
}
})

t.Run("status", func(t *testing.T) {
t.Run("connected user should get the appropriate message", func(t *testing.T) {
cases := []struct {
name string
enabled *bool
expected string
}{
{
name: "enabled",
enabled: model.NewBool(true),
expected: "Notifications from MSTeams are currently enabled.",
},
{
name: "disabled",
enabled: model.NewBool(false),
expected: "Notifications from MSTeams are currently disabled.",
},
{
name: "not set",
enabled: nil,
expected: "Notifications from MSTeams are currently disabled.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
reset(th, t, true)
if tc.enabled != nil {
err := th.p.setNotificationPreference(user1.Id, *tc.enabled)
require.Nil(t, err)
}

commandResponse, appErr := th.p.executeNotificationsCommand(args, []string{"status"})
require.Nil(t, appErr)
assertNoCommandResponse(t, commandResponse)
assertEphemeralResponse(th, t, args, tc.expected)
})
}
})
})

t.Run("on", func(t *testing.T) {
reset(th, t, true)

cases := []struct {
name string
enabled *bool
}{
{name: "was enabled", enabled: model.NewBool(true)},
{name: "was disabled", enabled: model.NewBool(false)},
{name: "was not set", enabled: nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if tc.enabled != nil {
err := th.p.setNotificationPreference(user1.Id, *tc.enabled)
require.Nil(t, err)
}

commandResponse, appErr := th.p.executeNotificationsCommand(args, []string{"on"})
require.Nil(t, appErr)
assertNoCommandResponse(t, commandResponse)
assertEphemeralResponse(th, t, args, "Notifications from MSTeams are now enabled.")

require.True(t, th.p.getNotificationPreference(user1.Id))
})
}
})

t.Run("off", func(t *testing.T) {
reset(th, t, true)

cases := []struct {
name string
enabled *bool
}{
{name: "was enabled", enabled: model.NewBool(true)},
{name: "was disabled", enabled: model.NewBool(false)},
{name: "was not set", enabled: nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if tc.enabled != nil {
err := th.p.setNotificationPreference(user1.Id, *tc.enabled)
require.Nil(t, err)
}

commandResponse, appErr := th.p.executeNotificationsCommand(args, []string{"off"})
require.Nil(t, appErr)
assertNoCommandResponse(t, commandResponse)
assertEphemeralResponse(th, t, args, "Notifications from MSTeams are now disabled.")

require.False(t, th.p.getNotificationPreference(user1.Id))
})
}
})
}
1 change: 1 addition & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type configuration struct {
EvaluationAPI bool `json:"evaluationapi"`
WebhookSecret string `json:"webhooksecret"`
EnabledTeams string `json:"enabledteams"`
SyncNotifications bool `json:"syncnotifications"`
SyncDirectMessages bool `json:"syncdirectmessages"`
SyncGroupMessages bool `json:"syncgroupmessages"`
SelectiveSync bool `json:"selectiveSync"`
Expand Down
4 changes: 2 additions & 2 deletions server/handlers/attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (ah *ActivityHandler) ProcessAndUploadFileToMM(attachmentData []byte, attac
return fileInfo.Id, false
}

func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg *clientmodels.Message, chat *clientmodels.Chat, existingFileIDs []string) (string, model.StringArray, string, bool, bool) {
func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg *clientmodels.Message, chat *clientmodels.Chat, handleFileAttachments bool, existingFileIDs []string) (string, model.StringArray, string, bool, bool) {
attachments := []string{}
newText := text
parentID := ""
Expand Down Expand Up @@ -152,7 +152,7 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg
continue
}

if !ah.plugin.GetSyncFileAttachments() {
if !handleFileAttachments {
skippedFileAttachments = true
continue
}
Expand Down
Loading

0 comments on commit 766cd1c

Please sign in to comment.