MM-14417: Messaging for mentions of users who are not in associated channel groups. (#10594)

* MM-14417: Adds support for out-of-channel notifications of users who are not in associated groups of group-constrained channels.

* MM-14417: Fix for mobile backwards compatibility.
Этот коммит содержится в:
Martin Kraft
2019-04-17 10:44:45 -04:00
коммит произвёл GitHub
родитель 762c257277
Коммит 84a59ddb39
5 изменённых файлов: 138 добавлений и 36 удалений

Просмотреть файл

@@ -22,7 +22,7 @@ const (
THREAD_ROOT = "root"
)
func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, *model.AppError) {
func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, error) {
// Do not send notifications in archived channels
if channel.DeleteAt > 0 {
return []string{}, nil
@@ -126,10 +126,19 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() {
if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil {
outOfChannelMentions := result.Data.([]*model.User)
channelMentions := model.UserSlice(result.Data.([]*model.User))
nonMemberIDs, err := a.FilterNonGroupChannelMembers(channelMentions.IDs(), channel)
if err != nil {
return nil, err
}
outOfChannelMentions := channelMentions.FilterWithoutID(nonMemberIDs)
outOfGroupsMentions := channelMentions.FilterByID(nonMemberIDs)
if channel.Type != model.CHANNEL_GROUP {
a.Srv.Go(func() {
a.sendOutOfChannelMentions(sender, post, outOfChannelMentions)
a.sendOutOfChannelMentions(sender, post, outOfChannelMentions, outOfGroupsMentions)
})
}
}
@@ -355,42 +364,70 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
return mentionedUsersList, nil
}
func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, users []*model.User) *model.AppError {
if len(users) == 0 {
func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.AppError {
if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
return nil
}
var usernames []string
for _, user := range users {
usernames = append(usernames, user.Username)
}
sort.Strings(usernames)
allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
var userIds []string
for _, user := range users {
userIds = append(userIds, user.Id)
}
ocUsers := model.UserSlice(outOfChannelUsers)
ocUsernames := ocUsers.Usernames()
ocUserIDs := ocUsers.IDs()
ogUsers := model.UserSlice(outOfGroupsUsers)
ogUsernames := ogUsers.Usernames()
T := utils.GetUserTranslations(sender.Locale)
ephemeralPostId := model.NewId()
var message string
if len(users) == 1 {
if len(outOfChannelUsers) == 1 {
message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
"Username": usernames[0],
"Username": ocUsernames[0],
})
} else {
} else if len(outOfChannelUsers) > 1 {
preliminary, final := splitAtFinal(ocUsernames)
message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
"Usernames": strings.Join(usernames[:len(usernames)-1], ", @"),
"LastUsername": usernames[len(usernames)-1],
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
if len(outOfGroupsUsers) == 1 {
if len(message) > 0 {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.one", map[string]interface{}{
"Username": ogUsernames[0],
})
} else if len(outOfGroupsUsers) > 1 {
preliminary, final := splitAtFinal(ogUsernames)
if len(message) > 0 {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.multiple", map[string]interface{}{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
props := model.StringInterface{
model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{
"post_id": ephemeralPostId,
"usernames": usernames,
"user_ids": userIds,
"post_id": ephemeralPostId,
"usernames": allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
"not_in_channel_usernames": ocUsernames,
"user_ids": allUsers.IDs(), // Kept for backwards compatibility of mobile app.
"not_in_channel_user_ids": ocUserIDs,
"not_in_groups_usernames": ogUsernames,
"not_in_groups_user_ids": ogUsers.IDs(),
},
}
@@ -409,6 +446,15 @@ func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, use
return nil
}
func splitAtFinal(items []string) (preliminary []string, final string) {
if len(items) == 0 {
return
}
preliminary = items[:len(items)-1]
final = items[len(items)-1]
return
}
type ExplicitMentions struct {
// MentionedUserIds contains a key for each user mentioned by keyword.
MentionedUserIds map[string]bool

Просмотреть файл

@@ -30,9 +30,9 @@ func TestSendNotifications(t *testing.T) {
t.Fatal(err)
}
mentions, err := th.App.SendNotifications(post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil)
if err != nil {
t.Fatal(err)
mentions, err2 := th.App.SendNotifications(post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil)
if err2 != nil {
t.Fatal(err2)
} else if mentions == nil {
t.Log(mentions)
t.Fatal("user should have been mentioned")
@@ -56,9 +56,9 @@ func TestSendNotifications(t *testing.T) {
t.Fatal(err)
}
_, err = th.App.SendNotifications(post2, th.BasicTeam, dm, th.BasicUser, nil)
if err != nil {
t.Fatal(err)
_, err2 = th.App.SendNotifications(post2, th.BasicTeam, dm, th.BasicUser, nil)
if err2 != nil {
t.Fatal(err2)
}
th.App.UpdateActive(th.BasicUser2, false)
@@ -74,14 +74,14 @@ func TestSendNotifications(t *testing.T) {
t.Fatal(err)
}
_, err = th.App.SendNotifications(post3, th.BasicTeam, dm, th.BasicUser, nil)
if err != nil {
t.Fatal(err)
_, err2 = th.App.SendNotifications(post3, th.BasicTeam, dm, th.BasicUser, nil)
if err2 != nil {
t.Fatal(err2)
}
th.BasicChannel.DeleteAt = 1
mentions, err = th.App.SendNotifications(post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil)
assert.Nil(t, err)
mentions, err2 = th.App.SendNotifications(post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil)
assert.Nil(t, err2)
assert.Len(t, mentions, 0)
}

Просмотреть файл

@@ -362,7 +362,7 @@ func (a *App) FillInPostProps(post *model.Post, channel *model.Channel) *model.A
return nil
}
func (a *App) handlePostEvents(post *model.Post, user *model.User, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList) *model.AppError {
func (a *App) handlePostEvents(post *model.Post, user *model.User, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList) error {
var team *model.Team
if len(channel.TeamId) > 0 {
result := <-a.Srv.Store.Team().Get(channel.TeamId)

Просмотреть файл

@@ -1440,13 +1440,21 @@
"id": "api.plugin.upload.no_file.app_error",
"translation": "Missing file in multipart/form request"
},
{
"id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple",
"translation": "@{{.Usernames}} and @{{.LastUsername}} did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel."
},
{
"id": "api.post.check_for_out_of_channel_groups_mentions.message.one",
"translation": "@{{.Username}} did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel."
},
{
"id": "api.post.check_for_out_of_channel_mentions.message.multiple",
"translation": "@{{.Usernames}} and @{{.LastUsername}} were mentioned, but they did not receive notifications because they do not belong to this channel."
"translation": "@{{.Usernames}} and @{{.LastUsername}} did not get notified by this mention because they are not in the channel."
},
{
"id": "api.post.check_for_out_of_channel_mentions.message.one",
"translation": "@{{.Username}} was mentioned, but they did not receive notifications because they do not belong to this channel."
"translation": "@{{.Username}} did not get notified by this mention because they are not in the channel."
},
{
"id": "api.post.create_post.can_not_post_to_deleted.error",

Просмотреть файл

@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"regexp"
"sort"
"strings"
"unicode/utf8"
@@ -117,6 +118,53 @@ type UserForIndexing struct {
ChannelsIds []string `json:"channel_id"`
}
type UserSlice []*User
func (u UserSlice) Usernames() []string {
usernames := []string{}
for _, user := range u {
usernames = append(usernames, user.Username)
}
sort.Strings(usernames)
return usernames
}
func (u UserSlice) IDs() []string {
ids := []string{}
for _, user := range u {
ids = append(ids, user.Id)
}
return ids
}
func (u UserSlice) FilterByID(ids []string) UserSlice {
var matches []*User
for _, user := range u {
for _, id := range ids {
if id == user.Id {
matches = append(matches, user)
}
}
}
return UserSlice(matches)
}
func (u UserSlice) FilterWithoutID(ids []string) UserSlice {
var keep []*User
for _, user := range u {
present := false
for _, id := range ids {
if id == user.Id {
present = true
}
}
if !present {
keep = append(keep, user)
}
}
return UserSlice(keep)
}
func (u *User) DeepCopy() *User {
copyUser := *u
if u.AuthData != nil {