Send ephemeral message when a mentioned user is not a member of the team (#26153)

Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
Co-authored-by: Carrie Warner (Mattermost) <74422101+cwarnermm@users.noreply.github.com>
Co-authored-by: Sazzad Hossain <sazzad.hossain@marginedge.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
sazzad hossain 2024-04-05 20:56:22 +06:00 committed by GitHub
parent c5cf4101da
commit 5f6ef75fe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 34 deletions

View File

@ -1119,17 +1119,21 @@ func (a *App) sendNoUsersNotifiedByGroupInChannel(c request.CTX, sender *model.U
// sendOutOfChannelMentions sends an ephemeral post to the sender of a post if any of the given potential mentions // sendOutOfChannelMentions sends an ephemeral post to the sender of a post if any of the given potential mentions
// are outside of the post's channel. Returns whether or not an ephemeral post was sent. // are outside of the post's channel. Returns whether or not an ephemeral post was sent.
func (a *App) sendOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) { func (a *App) sendOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) {
outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(c, sender, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(c, sender, post, channel, potentialMentions)
if err != nil { if err != nil {
return false, err return false, err
} }
if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 { if len(outOfTeamUsers) == 0 && len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
return false, nil return false, nil
} }
a.SendEphemeralPost(c, post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers)) if len(outOfChannelUsers) != 0 || len(outOfGroupsUsers) != 0 {
a.SendEphemeralPost(c, post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers))
}
if len(outOfTeamUsers) != 0 {
a.SendEphemeralPost(c, post.UserId, makeOutOfTeamMentionPost(sender, post, outOfTeamUsers))
}
return true, nil return true, nil
} }
@ -1147,52 +1151,65 @@ func (a *App) FilterUsersByVisible(c request.CTX, viewer *model.User, otherUsers
return result, nil return result, nil
} }
func (a *App) filterOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, error) { func (a *App) filterOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, []*model.User, error) {
if post.IsSystemMessage() { if post.IsSystemMessage() {
return nil, nil, nil return nil, nil, nil, nil
} }
if channel.TeamId == "" || channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup { if channel.TeamId == "" || channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
return nil, nil, nil return nil, nil, nil, nil
} }
if len(potentialMentions) == 0 { if len(potentialMentions) == 0 {
return nil, nil, nil return nil, nil, nil, nil
} }
users, err := a.Srv().Store().User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}}) mentionedUsersInTheTeam, err := a.Srv().Store().User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}})
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
// Filter out inactive users and bots // Filter out inactive users and bots
allUsers := model.UserSlice(users).FilterByActive(true) teamUsers := model.UserSlice(mentionedUsersInTheTeam).FilterByActive(true)
allUsers = allUsers.FilterWithoutBots() teamUsers = teamUsers.FilterWithoutBots()
allUsers, appErr := a.FilterUsersByVisible(c, sender, allUsers) teamUsers, appErr := a.FilterUsersByVisible(c, sender, teamUsers)
if appErr != nil { if appErr != nil {
return nil, nil, appErr return nil, nil, nil, appErr
} }
if len(allUsers) == 0 { allMentionedUsers, err := a.Srv().Store().User().GetProfilesByUsernames(potentialMentions, nil)
return nil, nil, nil if err != nil {
return nil, nil, nil, err
} }
// Differentiate between users who can and can't be added to the channel outOfTeamUsers := model.UserSlice(allMentionedUsers).FilterWithoutID(teamUsers.IDs())
outOfTeamUsers = outOfTeamUsers.FilterByActive(true)
outOfTeamUsers = outOfTeamUsers.FilterWithoutBots()
outOfTeamUsers, appErr = a.FilterUsersByVisible(c, sender, outOfTeamUsers)
if appErr != nil {
return nil, nil, nil, appErr
}
if len(teamUsers) == 0 {
return outOfTeamUsers, nil, nil, nil
}
// Differentiate between mentionedUsersInTheTeam who can and can't be added to the channel
var outOfChannelUsers model.UserSlice var outOfChannelUsers model.UserSlice
var outOfGroupsUsers model.UserSlice var outOfGroupsUsers model.UserSlice
if channel.IsGroupConstrained() { if channel.IsGroupConstrained() {
nonMemberIDs, err := a.FilterNonGroupChannelMembers(allUsers.IDs(), channel) nonMemberIDs, err := a.FilterNonGroupChannelMembers(teamUsers.IDs(), channel)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
outOfChannelUsers = allUsers.FilterWithoutID(nonMemberIDs) outOfChannelUsers = teamUsers.FilterWithoutID(nonMemberIDs)
outOfGroupsUsers = allUsers.FilterByID(nonMemberIDs) outOfGroupsUsers = teamUsers.FilterByID(nonMemberIDs)
} else { } else {
outOfChannelUsers = allUsers outOfChannelUsers = teamUsers
} }
return outOfChannelUsers, outOfGroupsUsers, nil return outOfTeamUsers, outOfChannelUsers, outOfGroupsUsers, nil
} }
func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post { func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post {
@ -1268,6 +1285,37 @@ func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChan
} }
} }
func makeOutOfTeamMentionPost(sender *model.User, post *model.Post, outOfTeamUsers []*model.User) *model.Post {
otUsers := model.UserSlice(outOfTeamUsers)
otUsernames := otUsers.Usernames()
T := i18n.GetUserTranslations(sender.Locale)
ephemeralPostId := model.NewId()
var message string
if len(outOfTeamUsers) == 1 {
message += T("api.post.check_for_out_of_team_mentions.message.one", map[string]any{
"Username": otUsernames[0],
})
} else if len(outOfTeamUsers) > 1 {
preliminary, final := splitAtFinal(otUsernames)
message += T("api.post.check_for_out_of_team_mentions.message.multiple", map[string]any{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
return &model.Post{
Id: ephemeralPostId,
RootId: post.RootId,
ChannelId: post.ChannelId,
Message: message,
CreateAt: post.CreateAt + 1,
}
}
func splitAtFinal(items []string) (preliminary []string, final string) { func splitAtFinal(items []string) (preliminary []string, final string) {
if len(items) == 0 { if len(items) == 0 {
return return

View File

@ -644,6 +644,17 @@ func TestSendOutOfChannelMentions(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, sent) assert.False(t, sent)
}) })
t.Run("should send ephemeral post when there is an out of team mention", func(t *testing.T) {
outOfTeamUser := th.CreateUser()
post := &model.Post{}
potentialMentions := []string{outOfTeamUser.Username}
sent, err := th.App.sendOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err)
assert.True(t, sent)
})
} }
func TestFilterOutOfChannelMentions(t *testing.T) { func TestFilterOutOfChannelMentions(t *testing.T) {
@ -670,22 +681,40 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{user2.Username, user3.Username} potentialMentions := []string{user2.Username, user3.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Len(t, outOfChannelUsers, 2) assert.Len(t, outOfChannelUsers, 2)
assert.True(t, (outOfChannelUsers[0].Id == user2.Id || outOfChannelUsers[1].Id == user2.Id)) assert.True(t, (outOfChannelUsers[0].Id == user2.Id || outOfChannelUsers[1].Id == user2.Id))
assert.True(t, (outOfChannelUsers[0].Id == user3.Id || outOfChannelUsers[1].Id == user3.Id)) assert.True(t, (outOfChannelUsers[0].Id == user3.Id || outOfChannelUsers[1].Id == user3.Id))
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
t.Run("should return users not in the team", func(t *testing.T) {
notThisTeamUser1 := th.CreateUser()
notThisTeamUser2 := th.CreateUser()
post := &model.Post{}
potentialMentions := []string{notThisTeamUser1.Username, notThisTeamUser2.Username}
outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 2)
assert.Nil(t, outOfChannelUsers)
assert.True(t, (outOfTeamUsers[0].Id == notThisTeamUser1.Id || outOfTeamUsers[1].Id == notThisTeamUser1.Id))
assert.True(t, (outOfTeamUsers[0].Id == notThisTeamUser2.Id || outOfTeamUsers[1].Id == notThisTeamUser2.Id))
assert.Nil(t, outOfGroupUsers)
})
t.Run("should return only visible users not in the channel (for guests)", func(t *testing.T) { t.Run("should return only visible users not in the channel (for guests)", func(t *testing.T) {
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{user2.Username, user3.Username, user4.Username} potentialMentions := []string{user2.Username, user3.Username, user4.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, guest, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, guest, post, channel, potentialMentions)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
require.Len(t, outOfChannelUsers, 1) require.Len(t, outOfChannelUsers, 1)
assert.Equal(t, user4.Id, outOfChannelUsers[0].Id) assert.Equal(t, user4.Id, outOfChannelUsers[0].Id)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
@ -697,9 +726,10 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
} }
potentialMentions := []string{user2.Username, user3.Username} potentialMentions := []string{user2.Username, user3.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
@ -711,9 +741,10 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
} }
potentialMentions := []string{user2.Username, user3.Username} potentialMentions := []string{user2.Username, user3.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, directChannel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, directChannel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
@ -725,9 +756,10 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
} }
potentialMentions := []string{user2.Username, user3.Username} potentialMentions := []string{user2.Username, user3.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, groupChannel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, groupChannel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
@ -740,23 +772,24 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{inactiveUser.Username} potentialMentions := []string{inactiveUser.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
t.Run("should not return bot users", func(t *testing.T) { t.Run("should not return bot users", func(t *testing.T) {
botUser := th.CreateUser() botUser := th.CreateBot()
botUser.IsBot = true
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{botUser.Username} potentialMentions := []string{botUser.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
@ -765,9 +798,10 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{"foo", "bar"} potentialMentions := []string{"foo", "bar"}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, channel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Nil(t, outOfChannelUsers) assert.Nil(t, outOfChannelUsers)
assert.Nil(t, outOfGroupUsers) assert.Nil(t, outOfGroupUsers)
}) })
@ -799,9 +833,10 @@ func TestFilterOutOfChannelMentions(t *testing.T) {
post := &model.Post{} post := &model.Post{}
potentialMentions := []string{nonChannelMember.Username, nonGroupMember.Username} potentialMentions := []string{nonChannelMember.Username, nonGroupMember.Username}
outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, constrainedChannel, potentialMentions) outOfTeamUsers, outOfChannelUsers, outOfGroupUsers, err := th.App.filterOutOfChannelMentions(th.Context, user1, post, constrainedChannel, potentialMentions)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, outOfTeamUsers, 0)
assert.Len(t, outOfChannelUsers, 1) assert.Len(t, outOfChannelUsers, 1)
assert.Equal(t, nonChannelMember.Id, outOfChannelUsers[0].Id) assert.Equal(t, nonChannelMember.Id, outOfChannelUsers[0].Id)
assert.Len(t, outOfGroupUsers, 1) assert.Len(t, outOfGroupUsers, 1)

View File

@ -2500,6 +2500,14 @@
"id": "api.post.check_for_out_of_channel_mentions.message.one", "id": "api.post.check_for_out_of_channel_mentions.message.one",
"translation": "@{{.Username}} did not get notified by this mention because they are not in the channel." "translation": "@{{.Username}} did not get notified by this mention because they are not in the channel."
}, },
{
"id": "api.post.check_for_out_of_team_mentions.message.multiple",
"translation": "@{{.Usernames}} and @{{.LastUsername}} didn't get notified by this mention because they aren't members of this team."
},
{
"id": "api.post.check_for_out_of_team_mentions.message.one",
"translation": "@{{.Username}} didn't get notified by this mention because they aren't a member of this team."
},
{ {
"id": "api.post.create_post.can_not_post_to_deleted.error", "id": "api.post.create_post.can_not_post_to_deleted.error",
"translation": "Can not post to deleted channel." "translation": "Can not post to deleted channel."