Skip to content

Commit

Permalink
MM-58580: show message on welcome bot dismiss (#692)
Browse files Browse the repository at this point in the history
* check bot dm channel on notifications enable

* update message on dismiss instead of just deleting

* fixup! check bot dm channel on notifications enable

* fixup! update message on dismiss instead of just deleting

* dismiss -> disable

* log when preference changed
  • Loading branch information
lieut-data authored Jun 12, 2024
1 parent fcdd376 commit 51dc236
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 64 deletions.
97 changes: 57 additions & 40 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func NewAPI(p *Plugin, store store.Store) *API {
router.HandleFunc("/account-connected", api.accountConnectedPage).Methods(http.MethodGet)
router.HandleFunc("/stats/site", api.siteStats).Methods("GET")
router.HandleFunc("/enable-notifications", api.enableNotifications).Methods("POST")
router.HandleFunc("/dismiss-notifications", api.dismissNotifications).Methods("POST")
router.HandleFunc("/disable-notifications", api.disableNotifications).Methods("POST")

// iFrame support
router.HandleFunc("/iframe/mattermostTab", api.iFrame).Methods("GET")
Expand Down Expand Up @@ -1074,25 +1074,60 @@ func (a *API) siteStats(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(data)
}

func (a *API) enableNotifications(w http.ResponseWriter, r *http.Request) {
func (a *API) preHandleNotifications(w http.ResponseWriter, r *http.Request) *model.Post {
userID := r.Header.Get("Mattermost-User-ID")

var actionHandler model.PostActionIntegrationRequest
if err := json.NewDecoder(r.Body).Decode(&actionHandler); err != nil {
a.p.API.LogWarn("Unable to decode the action handler", "error", err.Error())
http.Error(w, "unable to decode the action handler", http.StatusBadRequest)
return
return nil
}

err := a.p.setNotificationPreference(actionHandler.UserId, true)
post, err := a.p.apiClient.Post.GetPost(actionHandler.PostId)
if err != nil {
a.p.API.LogWarn("Error when updating the preferences", "error", err.Error())
http.Error(w, "error updating the preferences", http.StatusInternalServerError)
a.p.API.LogWarn("Unable to get the post", "error", err.Error())
http.Error(w, "unable to get the post", http.StatusBadRequest)
return nil
}

// Verify the post was authored by the bot itself.
if post.UserId != a.p.botUserID {
a.p.API.LogWarn("Attempt to update post not authored by the bot", "user_id", userID)
http.Error(w, "Forbidden", http.StatusForbidden)
return nil
}

// Verify the post is in the direct message channel between the bot and the user.
botDMChannel, err := a.p.apiClient.Channel.GetDirect(a.p.botUserID, userID)
if err != nil {
a.p.API.LogWarn("Unable to get the bot direct channel", "user_id", userID, "error", err.Error())
http.Error(w, "failed to authenticate the request", http.StatusInternalServerError)
return nil
} else if botDMChannel.Id != post.ChannelId {
a.p.API.LogWarn("Unable to get the bot direct channel", "user_id", userID)
http.Error(w, "Forbidden", http.StatusForbidden)
return nil
}

// At this point, it might be /another/ post by the bot in the DM with that user, but we
// allow this for now.

return post
}

func (a *API) enableNotifications(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")

post := a.preHandleNotifications(w, r)
if post == nil {
return
}

post, err := a.p.apiClient.Post.GetPost(actionHandler.PostId)
err := a.p.setNotificationPreference(userID, true)
if err != nil {
a.p.API.LogWarn("Unable to get the post", "error", err.Error())
http.Error(w, "unable to get the post", http.StatusBadRequest)
a.p.API.LogWarn("Error when updating the preferences", "error", err.Error())
http.Error(w, "error updating the preferences", http.StatusInternalServerError)
return
}

Expand All @@ -1109,47 +1144,29 @@ func (a *API) enableNotifications(w http.ResponseWriter, r *http.Request) {
}
}

func (a *API) dismissNotifications(w http.ResponseWriter, r *http.Request) {
func (a *API) disableNotifications(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")

var actionHandler model.PostActionIntegrationRequest
if err := json.NewDecoder(r.Body).Decode(&actionHandler); err != nil {
a.p.API.LogWarn("Unable to decode the action handler", "user_id", userID, "error", err.Error())
http.Error(w, "unable to decode the action handler", http.StatusBadRequest)
post := a.preHandleNotifications(w, r)
if post == nil {
return
}

post, err := a.p.apiClient.Post.GetPost(actionHandler.PostId)
err := a.p.setNotificationPreference(userID, false)
if err != nil {
a.p.API.LogWarn("Unable to get the post", "user_id", userID, "error", err.Error())
http.Error(w, "unable to get the post", http.StatusBadRequest)
return
}

// Verify the post was authored by the bot itself.
if post.UserId != a.p.botUserID {
a.p.API.LogWarn("Attempt to delete post not authored by the bot", "user_id", userID)
http.Error(w, "Forbidden", http.StatusForbidden)
a.p.API.LogWarn("Error when updating the preferences", "error", err.Error())
http.Error(w, "error updating the preferences", http.StatusInternalServerError)
return
}

// Verify the post is in the direct message channel between the bot and the user.
botDMChannel, err := a.p.apiClient.Channel.GetDirect(a.p.botUserID, userID)
if err != nil {
a.p.API.LogWarn("Unable to get the bot direct channel", "user_id", userID, "error", err.Error())
http.Error(w, "failed to authenticate the request", http.StatusInternalServerError)
return
} else if botDMChannel.Id != post.ChannelId {
a.p.API.LogWarn("Unable to get the bot direct channel", "user_id", userID)
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
post.Message = "You will not receive notifications from chats or group chats in Teams. To change this setting, open your user settings or run `/msteams notifications`"
post.DelProp("attachments")

// At this point, it might be /another/ post by the bot in the DM with that user, but we
// allow this for now.
err = a.p.apiClient.Post.DeletePost(actionHandler.PostId)
err = json.NewEncoder(w).Encode(model.PostActionIntegrationResponse{
Update: post,
})
if err != nil {
a.p.API.LogWarn("Unable to delete the post", "post_id", actionHandler.PostId, "user_id", userID, "error", err.Error())
http.Error(w, "unable to delete the post", http.StatusInternalServerError)
a.p.API.LogWarn("Unable to encode the response", "error", err.Error())
http.Error(w, "unable to encode the response", http.StatusInternalServerError)
}
}
98 changes: 78 additions & 20 deletions server/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,19 +1130,34 @@ func TestConnectionStatus(t *testing.T) {
})
}

func TestEnableNotifications(t *testing.T) {
func TestNotificationsWelcomeMessage(t *testing.T) {
th := setupTestHelper(t)
apiURL := th.pluginURL(t, "/enable-notifications")
team := th.SetupTeam(t)

sendRequest := func(t *testing.T, user *model.User, req model.PostActionIntegrationRequest) *http.Response {
triggerWelcomeMessage := func(th *testHelper, t *testing.T, user1 *model.User) *model.Post {
// Arrange: Send the welcome message and retrieve it
err := th.p.SendWelcomeMessageWithNotificationAction(user1.Id)
require.NoError(t, err)

dc, err := th.p.apiClient.Channel.GetDirect(user1.Id, th.p.botUserID)
require.NoError(t, err)

posts, err := th.p.apiClient.Post.GetPostsSince(dc.Id, time.Now().Add(-1*time.Minute).UnixMilli())
require.NoError(t, err)
require.Len(t, posts.Order, 1)
post := posts.Posts[posts.Order[0]]

return post
}

sendRequest := func(t *testing.T, user *model.User, url string, req model.PostActionIntegrationRequest) *http.Response {
t.Helper()
client1 := th.SetupClient(t, user.Id)

body, err := json.Marshal(req)
require.NoError(t, err)

request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(body))
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
require.NoError(t, err)

request.Header.Set(model.HeaderAuth, client1.AuthType+" "+client1.AuthToken)
Expand All @@ -1156,22 +1171,71 @@ func TestEnableNotifications(t *testing.T) {
return response
}

t.Run("enable notifications", func(t *testing.T) {
t.Run("enable", func(t *testing.T) {
th.Reset(t)
user1 := th.SetupUser(t, team)

// Arrange: Send the welcome message and retrieve it
err := th.p.SendWelcomeMessageWithNotificationAction(user1.Id)
post := triggerWelcomeMessage(th, t, user1)

// Act: make the request pretending the user clicked the button
response := sendRequest(t, user1, th.pluginURL(t, "/enable-notifications"), model.PostActionIntegrationRequest{
UserId: user1.Id,
PostId: post.Id,
})
assert.Equal(t, http.StatusOK, response.StatusCode)

// Assert: 1. we return an update for the post
var resp model.PostActionIntegrationResponse
err := json.NewDecoder(response.Body).Decode(&resp)
require.NoError(t, err)
dc, err := th.p.apiClient.Channel.GetDirect(user1.Id, th.p.botUserID)
assert.Len(t, resp.Update.Attachments(), 0)
assert.Equal(t, "You will now start receiving notifications from chats or group chats in Teams. To change this setting, open your user settings or run `/msteams notifications`", resp.Update.Message)

// Assert: 2. the notification preference is updated
assert.True(t, th.p.getNotificationPreference(user1.Id))
})

t.Run("disable, notifications previously disabled", func(t *testing.T) {
th.Reset(t)
user1 := th.SetupUser(t, team)

err := th.p.setNotificationPreference(user1.Id, false)
require.NoError(t, err)
posts, err := th.p.apiClient.Post.GetPostsSince(dc.Id, time.Now().Add(-1*time.Minute).UnixMilli())

// Arrange: Send the welcome message and retrieve it
post := triggerWelcomeMessage(th, t, user1)

// Act: make the request pretending the user clicked the button
response := sendRequest(t, user1, th.pluginURL(t, "/disable-notifications"), model.PostActionIntegrationRequest{
UserId: user1.Id,
PostId: post.Id,
})
assert.Equal(t, http.StatusOK, response.StatusCode)

// Assert: 1. we return an update for the post
var resp model.PostActionIntegrationResponse
err = json.NewDecoder(response.Body).Decode(&resp)
require.NoError(t, err)
require.Len(t, posts.Order, 1)
post := posts.Posts[posts.Order[0]]
assert.Len(t, resp.Update.Attachments(), 0)
assert.Equal(t, "You will not receive notifications from chats or group chats in Teams. To change this setting, open your user settings or run `/msteams notifications`", resp.Update.Message)

// Assert: 2. the notification preference is disabled
assert.False(t, th.p.getNotificationPreference(user1.Id))
})

t.Run("disable, notifications previously enabled", func(t *testing.T) {
th.Reset(t)
user1 := th.SetupUser(t, team)

err := th.p.setNotificationPreference(user1.Id, true)
require.NoError(t, err)

// Arrange: Send the welcome message and retrieve it
post := triggerWelcomeMessage(th, t, user1)

// Act: make the request pretending the user clicked the button
response := sendRequest(t, user1, model.PostActionIntegrationRequest{
response := sendRequest(t, user1, th.pluginURL(t, "/disable-notifications"), model.PostActionIntegrationRequest{
UserId: user1.Id,
PostId: post.Id,
})
Expand All @@ -1182,15 +1246,9 @@ func TestEnableNotifications(t *testing.T) {
err = json.NewDecoder(response.Body).Decode(&resp)
require.NoError(t, err)
assert.Len(t, resp.Update.Attachments(), 0)
assert.Equal(t, "You will now start receiving notifications from chats or group chats in Teams. To change this setting, open your user settings or run `/msteams notifications`", resp.Update.Message)
assert.Equal(t, "You will not receive notifications from chats or group chats in Teams. To change this setting, open your user settings or run `/msteams notifications`", resp.Update.Message)

// Assert: 2. the notification preference is updated
pref, appErr := th.p.API.GetPreferenceForUser(
user1.Id,
PreferenceCategoryPlugin,
storemodels.PreferenceNameNotification,
)
require.Nil(t, appErr)
assert.EqualValues(t, storemodels.PreferenceValueNotificationOn, pref.Value)
// Assert: 2. the notification preference is disabled
assert.False(t, th.p.getNotificationPreference(user1.Id))
})
}
4 changes: 2 additions & 2 deletions server/bot_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ func (p *Plugin) makeWelcomeMessageWithNotificationActionPost() *model.Post {
},
{
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("%s/dismiss-notifications", p.GetRelativeURL()),
URL: fmt.Sprintf("%s/disable-notifications", p.GetRelativeURL()),
},
Name: "Dismiss",
Name: "Disable",
Style: "default",
Type: model.PostActionTypeButton,
},
Expand Down
2 changes: 1 addition & 1 deletion server/bot_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ func TestSendWelcomeMessageWithNotificationAction(t *testing.T) {
require.EqualValues(t, "Enable Notifications", post.Attachments()[0].Actions[0].Name)
require.False(t, post.Attachments()[0].Actions[0].Disabled)
require.EqualValues(t, model.PostActionTypeButton, post.Attachments()[0].Actions[1].Type)
require.EqualValues(t, "Dismiss", post.Attachments()[0].Actions[1].Name)
require.EqualValues(t, "Disable", post.Attachments()[0].Actions[1].Name)
require.False(t, post.Attachments()[0].Actions[1].Disabled)
}
9 changes: 8 additions & 1 deletion server/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,17 @@ func (p *Plugin) setNotificationPreference(userID string, enable bool) error {
}

func (p *Plugin) updatePreferenceForUser(userID string, name string, value string) *model.AppError {
return p.API.UpdatePreferencesForUser(userID, []model.Preference{{
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{{
UserId: userID,
Category: PreferenceCategoryPlugin,
Name: name,
Value: value,
}})
if appErr != nil {
return appErr
}

p.apiClient.Log.Info("User changed preference", "user_id", userID, "category", PreferenceCategoryPlugin, "name", name, "value", value)

return nil
}

0 comments on commit 51dc236

Please sign in to comment.