* MM-30882: Fix read-after-write issue for demoting user In (*App).DemoteUserToGuest, we would demote a user, and then immediately read it back to do future operations from the user. This reading back of the user had the effect of sticking the old value into the cache after which it would never be updated. There was another issue along with this, which was when the invalidation message would broadcast across the cluster, it would hit the cache invalidation problem where an unrelated store call would miss the cache because it was invalidated, and then again read from replica and stick the old value. To fix all these, we return the new value directly from the store method to avoid having the app to read it again. And we add a map in the localcache layer which tracks invalidations made, and then switch to use master if it's true. The core change is fairly limited, but due to changing the store method signatures, a lot of code needed to be updated to pass "context.Background". Therefore the PR just "appears" to be big, but the main changes are limited to app/user.go, sqlstore/user_store.go and user_layer.go https://mattermost.atlassian.net/browse/MM-30882 ```release-note Fix an issue where demoting a user to guest would not take effect in an environment with read replicas. ``` * Fix concurrent map access * Fixing mistakes * fix tests
1151 строка
36 KiB
Go
1151 строка
36 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
"github.com/mattermost/mattermost-server/v5/store"
|
|
"github.com/mattermost/mattermost-server/v5/utils"
|
|
"github.com/mattermost/mattermost-server/v5/utils/markdown"
|
|
)
|
|
|
|
func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
|
|
// Do not send notifications in archived channels
|
|
if channel.DeleteAt > 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
pchan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
props, err := a.Srv().Store.User().GetAllProfilesInChannel(context.Background(), channel.Id, true)
|
|
pchan <- store.StoreResult{Data: props, NErr: err}
|
|
close(pchan)
|
|
}()
|
|
|
|
cmnchan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
props, err := a.Srv().Store.Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
|
|
cmnchan <- store.StoreResult{Data: props, NErr: err}
|
|
close(cmnchan)
|
|
}()
|
|
|
|
var gchan chan store.StoreResult
|
|
if a.allowGroupMentions(post) {
|
|
gchan = make(chan store.StoreResult, 1)
|
|
go func() {
|
|
groupsMap, err := a.getGroupsAllowedForReferenceInChannel(channel, team)
|
|
gchan <- store.StoreResult{Data: groupsMap, NErr: err}
|
|
close(gchan)
|
|
}()
|
|
}
|
|
|
|
var fchan chan store.StoreResult
|
|
if len(post.FileIds) != 0 {
|
|
fchan = make(chan store.StoreResult, 1)
|
|
go func() {
|
|
fileInfos, err := a.Srv().Store.FileInfo().GetForPost(post.Id, true, false, true)
|
|
fchan <- store.StoreResult{Data: fileInfos, NErr: err}
|
|
close(fchan)
|
|
}()
|
|
}
|
|
|
|
result := <-pchan
|
|
if result.NErr != nil {
|
|
return nil, result.NErr
|
|
}
|
|
profileMap := result.Data.(map[string]*model.User)
|
|
|
|
result = <-cmnchan
|
|
if result.NErr != nil {
|
|
return nil, result.NErr
|
|
}
|
|
channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
|
|
|
|
groups := make(map[string]*model.Group)
|
|
if gchan != nil {
|
|
result = <-gchan
|
|
if result.NErr != nil {
|
|
return nil, result.NErr
|
|
}
|
|
groups = result.Data.(map[string]*model.Group)
|
|
}
|
|
|
|
mentions := &ExplicitMentions{}
|
|
allActivityPushUserIds := []string{}
|
|
|
|
if channel.Type == model.CHANNEL_DIRECT {
|
|
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
|
|
|
|
_, ok := profileMap[otherUserId]
|
|
if ok {
|
|
mentions.addMention(otherUserId, DMMention)
|
|
}
|
|
|
|
if post.GetProp("from_webhook") == "true" {
|
|
mentions.addMention(post.UserId, DMMention)
|
|
}
|
|
} else {
|
|
allowChannelMentions := a.allowChannelMentions(post, len(profileMap))
|
|
keywords := a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
|
|
|
|
mentions = getExplicitMentions(post, keywords, groups)
|
|
|
|
// Add an implicit mention when a user is added to a channel
|
|
// even if the user has set 'username mentions' to false in account settings.
|
|
if post.Type == model.POST_ADD_TO_CHANNEL {
|
|
addedUserId, ok := post.GetProp(model.POST_PROPS_ADDED_USER_ID).(string)
|
|
if ok {
|
|
mentions.addMention(addedUserId, KeywordMention)
|
|
}
|
|
}
|
|
|
|
// Iterate through all groups that were mentioned and insert group members into the list of mentions or potential mentions
|
|
for _, group := range mentions.GroupMentions {
|
|
anyUsersMentionedByGroup, err := a.insertGroupMentions(group, channel, profileMap, mentions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !anyUsersMentionedByGroup {
|
|
a.sendNoUsersNotifiedByGroupInChannel(sender, post, channel, group)
|
|
}
|
|
}
|
|
|
|
// get users that have comment thread mentions enabled
|
|
if post.RootId != "" && parentPostList != nil {
|
|
for _, threadPost := range parentPostList.Posts {
|
|
profile := profileMap[threadPost.UserId]
|
|
if profile == nil {
|
|
continue
|
|
}
|
|
// If this is the root post and it was posted by an OAuth bot, don't notify the user
|
|
if threadPost.Id == parentPostList.Order[0] && threadPost.IsFromOAuthBot() {
|
|
continue
|
|
}
|
|
if profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == model.COMMENTS_NOTIFY_ANY || (profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == model.COMMENTS_NOTIFY_ROOT && threadPost.Id == parentPostList.Order[0]) {
|
|
mentionType := ThreadMention
|
|
if threadPost.Id == parentPostList.Order[0] {
|
|
mentionType = CommentMention
|
|
}
|
|
|
|
mentions.addMention(threadPost.UserId, mentionType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// prevent the user from mentioning themselves
|
|
if post.GetProp("from_webhook") != "true" {
|
|
mentions.removeMention(post.UserId)
|
|
}
|
|
|
|
go func() {
|
|
_, err := a.sendOutOfChannelMentions(sender, post, channel, mentions.OtherPotentialMentions)
|
|
if err != nil {
|
|
mlog.Error("Failed to send warning for out of channel mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
|
|
}
|
|
}()
|
|
|
|
// find which users in the channel are set up to always receive mobile notifications
|
|
for _, profile := range profileMap {
|
|
if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL ||
|
|
channelMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CHANNEL_NOTIFY_ALL) &&
|
|
(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
|
|
!post.IsSystemMessage() {
|
|
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
mentionedUsersList := make([]string, 0, len(mentions.Mentions))
|
|
updateMentionChans := []chan *model.AppError{}
|
|
mentionAutofollowChans := []chan *model.AppError{}
|
|
threadParticipants := map[string]bool{post.UserId: true}
|
|
if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" {
|
|
if parentPostList != nil {
|
|
threadParticipants[parentPostList.Posts[parentPostList.Order[0]].UserId] = true
|
|
}
|
|
for id := range mentions.Mentions {
|
|
threadParticipants[id] = true
|
|
}
|
|
// for each mention, make sure to update thread autofollow (if enabled) and update increment mention count
|
|
for id := range threadParticipants {
|
|
mac := make(chan *model.AppError, 1)
|
|
go func(userID string) {
|
|
defer close(mac)
|
|
incrementMentions := false
|
|
for mid := range mentions.Mentions {
|
|
if userID == mid {
|
|
incrementMentions = true
|
|
break
|
|
}
|
|
}
|
|
nErr := a.Srv().Store.Thread().CreateMembershipIfNeeded(userID, post.RootId, true, incrementMentions, *a.Config().ServiceSettings.ThreadAutoFollow)
|
|
if nErr != nil {
|
|
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
mac <- nil
|
|
}(id)
|
|
mentionAutofollowChans = append(mentionAutofollowChans, mac)
|
|
}
|
|
}
|
|
for id := range mentions.Mentions {
|
|
mentionedUsersList = append(mentionedUsersList, id)
|
|
|
|
umc := make(chan *model.AppError, 1)
|
|
go func(userID string) {
|
|
defer close(umc)
|
|
nErr := a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userID, *a.Config().ServiceSettings.ThreadAutoFollow)
|
|
if nErr != nil {
|
|
umc <- model.NewAppError("SendNotifications", "app.channel.increment_mention_count.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
umc <- nil
|
|
}(id)
|
|
updateMentionChans = append(updateMentionChans, umc)
|
|
}
|
|
|
|
notification := &PostNotification{
|
|
Post: post.Clone(),
|
|
Channel: channel,
|
|
ProfileMap: profileMap,
|
|
Sender: sender,
|
|
}
|
|
|
|
if *a.Config().EmailSettings.SendEmailNotifications {
|
|
for _, id := range mentionedUsersList {
|
|
if profileMap[id] == nil {
|
|
continue
|
|
}
|
|
|
|
//If email verification is required and user email is not verified don't send email.
|
|
if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
|
|
mlog.Debug("Skipped sending notification email, address not verified.", mlog.String("user_email", profileMap[id].Email), mlog.String("user_id", id))
|
|
continue
|
|
}
|
|
|
|
if a.userAllowsEmail(profileMap[id], channelMemberNotifyPropsMap[id], post) {
|
|
a.sendNotificationEmail(notification, profileMap[id], team)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for channel-wide mentions in channels that have too many members for those to work
|
|
if int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
|
|
T := utils.GetUserTranslations(sender.Locale)
|
|
|
|
if mentions.HereMentioned {
|
|
a.SendEphemeralPost(
|
|
post.UserId,
|
|
&model.Post{
|
|
ChannelId: post.ChannelId,
|
|
Message: T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
|
|
CreateAt: post.CreateAt + 1,
|
|
},
|
|
)
|
|
}
|
|
|
|
if mentions.ChannelMentioned {
|
|
a.SendEphemeralPost(
|
|
post.UserId,
|
|
&model.Post{
|
|
ChannelId: post.ChannelId,
|
|
Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
|
|
CreateAt: post.CreateAt + 1,
|
|
},
|
|
)
|
|
}
|
|
|
|
if mentions.AllMentioned {
|
|
a.SendEphemeralPost(
|
|
post.UserId,
|
|
&model.Post{
|
|
ChannelId: post.ChannelId,
|
|
Message: T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
|
|
CreateAt: post.CreateAt + 1,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// Make sure all mention updates are complete to prevent race
|
|
// Probably better to batch these DB updates in the future
|
|
// MUST be completed before push notifications send
|
|
for _, umc := range updateMentionChans {
|
|
if err := <-umc; err != nil {
|
|
mlog.Warn(
|
|
"Failed to update mention count",
|
|
mlog.String("post_id", post.Id),
|
|
mlog.String("channel_id", post.ChannelId),
|
|
mlog.Err(err),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Log the problems that might have occurred while auto following the thread
|
|
for _, mac := range mentionAutofollowChans {
|
|
if err := <-mac; err != nil {
|
|
mlog.Warn(
|
|
"Failed to update thread autofollow from mention",
|
|
mlog.String("post_id", post.Id),
|
|
mlog.String("channel_id", post.ChannelId),
|
|
mlog.Err(err),
|
|
)
|
|
}
|
|
}
|
|
sendPushNotifications := false
|
|
if *a.Config().EmailSettings.SendPushNotifications {
|
|
pushServer := *a.Config().EmailSettings.PushNotificationServer
|
|
if license := a.Srv().License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
|
|
mlog.Warn("Push notifications are disabled. Go to System Console > Notifications > Mobile Push to enable them.")
|
|
sendPushNotifications = false
|
|
} else {
|
|
sendPushNotifications = true
|
|
}
|
|
}
|
|
|
|
if sendPushNotifications {
|
|
for _, id := range mentionedUsersList {
|
|
if profileMap[id] == nil {
|
|
continue
|
|
}
|
|
|
|
var status *model.Status
|
|
var err *model.AppError
|
|
if status, err = a.GetStatus(id); err != nil {
|
|
status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
|
}
|
|
|
|
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
|
|
mentionType := mentions.Mentions[id]
|
|
|
|
replyToThreadType := ""
|
|
if mentionType == ThreadMention {
|
|
replyToThreadType = model.COMMENTS_NOTIFY_ANY
|
|
} else if mentionType == CommentMention {
|
|
replyToThreadType = model.COMMENTS_NOTIFY_ROOT
|
|
}
|
|
|
|
a.sendPushNotification(
|
|
notification,
|
|
profileMap[id],
|
|
mentionType == KeywordMention || mentionType == ChannelMention || mentionType == DMMention,
|
|
mentionType == ChannelMention,
|
|
replyToThreadType,
|
|
)
|
|
} else {
|
|
// register that a notification was not sent
|
|
a.NotificationsLog().Warn("Notification not sent",
|
|
mlog.String("ackId", ""),
|
|
mlog.String("type", model.PUSH_TYPE_MESSAGE),
|
|
mlog.String("userId", id),
|
|
mlog.String("postId", post.Id),
|
|
mlog.String("status", model.PUSH_NOT_SENT),
|
|
)
|
|
}
|
|
}
|
|
|
|
for _, id := range allActivityPushUserIds {
|
|
if profileMap[id] == nil {
|
|
continue
|
|
}
|
|
|
|
if _, ok := mentions.Mentions[id]; !ok {
|
|
var status *model.Status
|
|
var err *model.AppError
|
|
if status, err = a.GetStatus(id); err != nil {
|
|
status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
|
}
|
|
|
|
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
|
|
a.sendPushNotification(
|
|
notification,
|
|
profileMap[id],
|
|
false,
|
|
false,
|
|
"",
|
|
)
|
|
} else {
|
|
// register that a notification was not sent
|
|
a.NotificationsLog().Warn("Notification not sent",
|
|
mlog.String("ackId", ""),
|
|
mlog.String("type", model.PUSH_TYPE_MESSAGE),
|
|
mlog.String("userId", id),
|
|
mlog.String("postId", post.Id),
|
|
mlog.String("status", model.PUSH_NOT_SENT),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
|
|
|
|
// Note that PreparePostForClient should've already been called by this point
|
|
message.Add("post", post.ToJson())
|
|
|
|
message.Add("channel_type", channel.Type)
|
|
message.Add("channel_display_name", notification.GetChannelName(model.SHOW_USERNAME, ""))
|
|
message.Add("channel_name", channel.Name)
|
|
message.Add("sender_name", notification.GetSenderName(model.SHOW_USERNAME, *a.Config().ServiceSettings.EnablePostUsernameOverride))
|
|
message.Add("team_id", team.Id)
|
|
message.Add("set_online", setOnline)
|
|
|
|
if len(post.FileIds) != 0 && fchan != nil {
|
|
message.Add("otherFile", "true")
|
|
|
|
var infos []*model.FileInfo
|
|
if result := <-fchan; result.NErr != nil {
|
|
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(result.NErr))
|
|
} else {
|
|
infos = result.Data.([]*model.FileInfo)
|
|
}
|
|
|
|
for _, info := range infos {
|
|
if info.IsImage() {
|
|
message.Add("image", "true")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(mentionedUsersList) != 0 {
|
|
message.Add("mentions", model.ArrayToJson(mentionedUsersList))
|
|
}
|
|
|
|
a.Publish(message)
|
|
// If this is a reply in a thread, notify participants
|
|
if a.Config().FeatureFlags.CollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.COLLAPSED_THREADS_DISABLED && post.RootId != "" {
|
|
thread, err := a.Srv().Store.Thread().Get(post.RootId)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "cannot get thread %q", post.RootId)
|
|
}
|
|
payload := thread.ToJson()
|
|
for _, uid := range thread.Participants {
|
|
sendEvent := *a.Config().ServiceSettings.CollapsedThreads == model.COLLAPSED_THREADS_DEFAULT_ON
|
|
// check if a participant has overridden collapsed threads settings
|
|
if preference, err := a.Srv().Store.Preference().Get(uid, model.PREFERENCE_CATEGORY_COLLAPSED_THREADS_SETTINGS, model.PREFERENCE_NAME_COLLAPSED_THREADS_ENABLED); err == nil {
|
|
sendEvent, _ = strconv.ParseBool(preference.Value)
|
|
}
|
|
if sendEvent {
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_UPDATED, "", "", uid, nil)
|
|
message.Add("thread", payload)
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
|
|
}
|
|
return mentionedUsersList, nil
|
|
}
|
|
|
|
func (a *App) userAllowsEmail(user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool {
|
|
userAllowsEmails := user.NotifyProps[model.EMAIL_NOTIFY_PROP] != "false"
|
|
if channelEmail, ok := channelMemberNotificationProps[model.EMAIL_NOTIFY_PROP]; ok {
|
|
if channelEmail != model.CHANNEL_NOTIFY_DEFAULT {
|
|
userAllowsEmails = channelEmail != "false"
|
|
}
|
|
}
|
|
|
|
// Remove the user as recipient when the user has muted the channel.
|
|
if channelMuted, ok := channelMemberNotificationProps[model.MARK_UNREAD_NOTIFY_PROP]; ok {
|
|
if channelMuted == model.CHANNEL_MARK_UNREAD_MENTION {
|
|
mlog.Debug("Channel muted for user", mlog.String("user_id", user.Id), mlog.String("channel_mute", channelMuted))
|
|
userAllowsEmails = false
|
|
}
|
|
}
|
|
|
|
var status *model.Status
|
|
var err *model.AppError
|
|
if status, err = a.GetStatus(user.Id); err != nil {
|
|
status = &model.Status{
|
|
UserId: user.Id,
|
|
Status: model.STATUS_OFFLINE,
|
|
Manual: false,
|
|
LastActivityAt: 0,
|
|
ActiveChannel: "",
|
|
}
|
|
}
|
|
|
|
autoResponderRelated := status.Status == model.STATUS_OUT_OF_OFFICE || post.Type == model.POST_AUTO_RESPONDER
|
|
emailNotificationsAllowedForStatus := status.Status != model.STATUS_ONLINE && status.Status != model.STATUS_DND
|
|
|
|
return userAllowsEmails && emailNotificationsAllowedForStatus && user.DeleteAt == 0 && !autoResponderRelated
|
|
}
|
|
|
|
func (a *App) sendNoUsersNotifiedByGroupInChannel(sender *model.User, post *model.Post, channel *model.Channel, group *model.Group) {
|
|
T := utils.GetUserTranslations(sender.Locale)
|
|
ephemeralPost := &model.Post{
|
|
UserId: sender.Id,
|
|
RootId: post.RootId,
|
|
ParentId: post.ParentId,
|
|
ChannelId: channel.Id,
|
|
Message: T("api.post.check_for_out_of_channel_group_users.message.none", model.StringInterface{"GroupName": group.Name}),
|
|
}
|
|
a.SendEphemeralPost(post.UserId, ephemeralPost)
|
|
}
|
|
|
|
// 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.
|
|
func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) {
|
|
outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(sender, post, channel, potentialMentions)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
a.SendEphemeralPost(post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers))
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (a *App) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
|
|
result := []*model.User{}
|
|
for _, user := range otherUsers {
|
|
canSee, err := a.UserCanSeeOtherUser(viewer.Id, user.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if canSee {
|
|
result = append(result, user)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) filterOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, error) {
|
|
if post.IsSystemMessage() {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
if channel.TeamId == "" || channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
if len(potentialMentions) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
users, err := a.Srv().Store.User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Filter out inactive users and bots
|
|
allUsers := model.UserSlice(users).FilterByActive(true)
|
|
allUsers = allUsers.FilterWithoutBots()
|
|
allUsers, appErr := a.FilterUsersByVisible(sender, allUsers)
|
|
if appErr != nil {
|
|
return nil, nil, appErr
|
|
}
|
|
|
|
if len(allUsers) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// Differentiate between users who can and can't be added to the channel
|
|
var outOfChannelUsers model.UserSlice
|
|
var outOfGroupsUsers model.UserSlice
|
|
if channel.IsGroupConstrained() {
|
|
nonMemberIDs, err := a.FilterNonGroupChannelMembers(allUsers.IDs(), channel)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
outOfChannelUsers = allUsers.FilterWithoutID(nonMemberIDs)
|
|
outOfGroupsUsers = allUsers.FilterByID(nonMemberIDs)
|
|
} else {
|
|
outOfChannelUsers = allUsers
|
|
}
|
|
|
|
return outOfChannelUsers, outOfGroupsUsers, nil
|
|
}
|
|
|
|
func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post {
|
|
allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
|
|
|
|
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(outOfChannelUsers) == 1 {
|
|
message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
|
|
"Username": ocUsernames[0],
|
|
})
|
|
} 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(preliminary, ", @"),
|
|
"LastUsername": final,
|
|
})
|
|
}
|
|
|
|
if len(outOfGroupsUsers) == 1 {
|
|
if message != "" {
|
|
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 message != "" {
|
|
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": 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(),
|
|
},
|
|
}
|
|
|
|
return &model.Post{
|
|
Id: ephemeralPostId,
|
|
RootId: post.RootId,
|
|
ChannelId: post.ChannelId,
|
|
Message: message,
|
|
CreateAt: post.CreateAt + 1,
|
|
Props: props,
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// Mentions contains the ID of each user that was mentioned and how they were mentioned.
|
|
Mentions map[string]MentionType
|
|
|
|
// Contains a map of groups that were mentioned
|
|
GroupMentions map[string]*model.Group
|
|
|
|
// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
|
|
// a corresponding keyword.
|
|
OtherPotentialMentions []string
|
|
|
|
// HereMentioned is true if the message contained @here.
|
|
HereMentioned bool
|
|
|
|
// AllMentioned is true if the message contained @all.
|
|
AllMentioned bool
|
|
|
|
// ChannelMentioned is true if the message contained @channel.
|
|
ChannelMentioned bool
|
|
}
|
|
|
|
type MentionType int
|
|
|
|
const (
|
|
// Different types of mentions ordered by their priority from lowest to highest
|
|
|
|
// A placeholder that should never be used in practice
|
|
NoMention MentionType = iota
|
|
|
|
// The post is in a thread that the user has commented on
|
|
ThreadMention
|
|
|
|
// The post is a comment on a thread started by the user
|
|
CommentMention
|
|
|
|
// The post contains an at-channel, at-all, or at-here
|
|
ChannelMention
|
|
|
|
// The post is a DM
|
|
DMMention
|
|
|
|
// The post contains an at-mention for the user
|
|
KeywordMention
|
|
|
|
// The post contains a group mention for the user
|
|
GroupMention
|
|
)
|
|
|
|
func (m *ExplicitMentions) addMention(userID string, mentionType MentionType) {
|
|
if m.Mentions == nil {
|
|
m.Mentions = make(map[string]MentionType)
|
|
}
|
|
|
|
if currentType, ok := m.Mentions[userID]; ok && currentType >= mentionType {
|
|
return
|
|
}
|
|
|
|
m.Mentions[userID] = mentionType
|
|
}
|
|
|
|
func (m *ExplicitMentions) addGroupMention(word string, groups map[string]*model.Group) bool {
|
|
if strings.HasPrefix(word, "@") {
|
|
word = word[1:]
|
|
} else {
|
|
// Only allow group mentions when mentioned directly with @group-name
|
|
return false
|
|
}
|
|
|
|
group, groupFound := groups[word]
|
|
if !groupFound {
|
|
group = groups[strings.ToLower(word)]
|
|
}
|
|
|
|
if group == nil {
|
|
return false
|
|
}
|
|
|
|
if m.GroupMentions == nil {
|
|
m.GroupMentions = make(map[string]*model.Group)
|
|
}
|
|
|
|
if group.Name != nil {
|
|
m.GroupMentions[*group.Name] = group
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (m *ExplicitMentions) addMentions(userIDs []string, mentionType MentionType) {
|
|
for _, userID := range userIDs {
|
|
m.addMention(userID, mentionType)
|
|
}
|
|
}
|
|
|
|
func (m *ExplicitMentions) removeMention(userID string) {
|
|
delete(m.Mentions, userID)
|
|
}
|
|
|
|
// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
|
|
// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
|
|
func getExplicitMentions(post *model.Post, keywords map[string][]string, groups map[string]*model.Group) *ExplicitMentions {
|
|
ret := &ExplicitMentions{}
|
|
|
|
buf := ""
|
|
mentionsEnabledFields := getMentionsEnabledFields(post)
|
|
for _, message := range mentionsEnabledFields {
|
|
markdown.Inspect(message, func(node interface{}) bool {
|
|
text, ok := node.(*markdown.Text)
|
|
if !ok {
|
|
ret.processText(buf, keywords, groups)
|
|
buf = ""
|
|
return true
|
|
}
|
|
buf += text.Text
|
|
return false
|
|
})
|
|
}
|
|
ret.processText(buf, keywords, groups)
|
|
|
|
return ret
|
|
}
|
|
|
|
// Given a post returns the values of the fields in which mentions are possible.
|
|
// post.message, preText and text in the attachment are enabled.
|
|
func getMentionsEnabledFields(post *model.Post) model.StringArray {
|
|
ret := []string{}
|
|
|
|
ret = append(ret, post.Message)
|
|
for _, attachment := range post.Attachments() {
|
|
|
|
if attachment.Pretext != "" {
|
|
ret = append(ret, attachment.Pretext)
|
|
}
|
|
if attachment.Text != "" {
|
|
ret = append(ret, attachment.Text)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// allowChannelMentions returns whether or not the channel mentions are allowed for the given post.
|
|
func (a *App) allowChannelMentions(post *model.Post, numProfiles int) bool {
|
|
if !a.HasPermissionToChannel(post.UserId, post.ChannelId, model.PERMISSION_USE_CHANNEL_MENTIONS) {
|
|
return false
|
|
}
|
|
|
|
if post.Type == model.POST_HEADER_CHANGE || post.Type == model.POST_PURPOSE_CHANGE {
|
|
return false
|
|
}
|
|
|
|
if int64(numProfiles) >= *a.Config().TeamSettings.MaxNotificationsPerChannel {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// allowGroupMentions returns whether or not the group mentions are allowed for the given post.
|
|
func (a *App) allowGroupMentions(post *model.Post) bool {
|
|
if license := a.Srv().License(); license == nil || !*license.Features.LDAPGroups {
|
|
return false
|
|
}
|
|
|
|
if !a.HasPermissionToChannel(post.UserId, post.ChannelId, model.PERMISSION_USE_GROUP_MENTIONS) {
|
|
return false
|
|
}
|
|
|
|
if post.Type == model.POST_HEADER_CHANGE || post.Type == model.POST_PURPOSE_CHANGE {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// getGroupsAllowedForReferenceInChannel returns a map of groups allowed for reference in a given channel and team.
|
|
func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team *model.Team) (map[string]*model.Group, error) {
|
|
var err error
|
|
groupsMap := make(map[string]*model.Group)
|
|
opts := model.GroupSearchOpts{FilterAllowReference: true}
|
|
|
|
if channel.IsGroupConstrained() || (team != nil && team.IsGroupConstrained()) {
|
|
var groups []*model.GroupWithSchemeAdmin
|
|
if channel.IsGroupConstrained() {
|
|
groups, err = a.Srv().Store.Group().GetGroupsByChannel(channel.Id, opts)
|
|
} else {
|
|
groups, err = a.Srv().Store.Group().GetGroupsByTeam(team.Id, opts)
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to get groups")
|
|
}
|
|
for _, group := range groups {
|
|
if group.Group.Name != nil {
|
|
groupsMap[*group.Group.Name] = &group.Group
|
|
}
|
|
}
|
|
return groupsMap, nil
|
|
}
|
|
|
|
groups, err := a.Srv().Store.Group().GetGroups(0, 0, opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to get groups")
|
|
}
|
|
for _, group := range groups {
|
|
if group.Name != nil {
|
|
groupsMap[*group.Name] = group
|
|
}
|
|
}
|
|
|
|
return groupsMap, nil
|
|
}
|
|
|
|
// Given a map of user IDs to profiles, returns a list of mention
|
|
// keywords for all users in the channel.
|
|
func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
|
|
keywords := make(map[string][]string)
|
|
|
|
for _, profile := range profiles {
|
|
addMentionKeywordsForUser(
|
|
keywords,
|
|
profile,
|
|
channelMemberNotifyPropsMap[profile.Id],
|
|
a.GetStatusFromCache(profile.Id),
|
|
allowChannelMentions,
|
|
)
|
|
}
|
|
|
|
return keywords
|
|
}
|
|
|
|
// insertGroupMentions adds group members in the channel to Mentions, adds group members not in the channel to OtherPotentialMentions
|
|
// returns false if no group members present in the team that the channel belongs to
|
|
func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *ExplicitMentions) (bool, *model.AppError) {
|
|
var err error
|
|
var groupMembers []*model.User
|
|
outOfChannelGroupMembers := []*model.User{}
|
|
isGroupOrDirect := channel.IsGroupOrDirect()
|
|
|
|
if isGroupOrDirect {
|
|
groupMembers, err = a.Srv().Store.Group().GetMemberUsers(group.Id)
|
|
} else {
|
|
groupMembers, err = a.Srv().Store.Group().GetMemberUsersInTeam(group.Id, channel.TeamId)
|
|
}
|
|
|
|
if err != nil {
|
|
return false, model.NewAppError("insertGroupMentions", "app.select_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if mentions.Mentions == nil {
|
|
mentions.Mentions = make(map[string]MentionType)
|
|
}
|
|
|
|
for _, member := range groupMembers {
|
|
if _, ok := profileMap[member.Id]; ok {
|
|
mentions.Mentions[member.Id] = GroupMention
|
|
} else {
|
|
outOfChannelGroupMembers = append(outOfChannelGroupMembers, member)
|
|
}
|
|
}
|
|
|
|
potentialGroupMembersMentioned := []string{}
|
|
for _, user := range outOfChannelGroupMembers {
|
|
potentialGroupMembersMentioned = append(potentialGroupMembersMentioned, user.Username)
|
|
}
|
|
if mentions.OtherPotentialMentions == nil {
|
|
mentions.OtherPotentialMentions = potentialGroupMembersMentioned
|
|
} else {
|
|
mentions.OtherPotentialMentions = append(mentions.OtherPotentialMentions, potentialGroupMembersMentioned...)
|
|
}
|
|
|
|
return isGroupOrDirect || len(groupMembers) > 0, nil
|
|
}
|
|
|
|
// addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
|
|
func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) map[string][]string {
|
|
userMention := "@" + strings.ToLower(profile.Username)
|
|
keywords[userMention] = append(keywords[userMention], profile.Id)
|
|
|
|
// Add all the user's mention keys
|
|
for _, k := range profile.GetMentionKeys() {
|
|
// note that these are made lower case so that we can do a case insensitive check for them
|
|
key := strings.ToLower(k)
|
|
|
|
if key != "" {
|
|
keywords[key] = append(keywords[key], profile.Id)
|
|
}
|
|
}
|
|
|
|
// If turned on, add the user's case sensitive first name
|
|
if profile.NotifyProps[model.FIRST_NAME_NOTIFY_PROP] == "true" && profile.FirstName != "" {
|
|
keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
|
|
}
|
|
|
|
// Add @channel and @all to keywords if user has them turned on and the server allows them
|
|
if allowChannelMentions {
|
|
// Ignore channel mentions if channel is muted and channel mention setting is default
|
|
ignoreChannelMentions := channelNotifyProps[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP] == model.IGNORE_CHANNEL_MENTIONS_ON || (channelNotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.USER_NOTIFY_MENTION && channelNotifyProps[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP] == model.IGNORE_CHANNEL_MENTIONS_DEFAULT)
|
|
|
|
if profile.NotifyProps[model.CHANNEL_MENTIONS_NOTIFY_PROP] == "true" && !ignoreChannelMentions {
|
|
keywords["@channel"] = append(keywords["@channel"], profile.Id)
|
|
keywords["@all"] = append(keywords["@all"], profile.Id)
|
|
|
|
if status != nil && status.Status == model.STATUS_ONLINE {
|
|
keywords["@here"] = append(keywords["@here"], profile.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
return keywords
|
|
}
|
|
|
|
// Represents either an email or push notification and contains the fields required to send it to any user.
|
|
type PostNotification struct {
|
|
Channel *model.Channel
|
|
Post *model.Post
|
|
ProfileMap map[string]*model.User
|
|
Sender *model.User
|
|
}
|
|
|
|
// Returns the name of the channel for this notification. For direct messages, this is the sender's name
|
|
// preceded by an at sign. For group messages, this is a comma-separated list of the members of the
|
|
// channel, with an option to exclude the recipient of the message from that list.
|
|
func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string {
|
|
switch n.Channel.Type {
|
|
case model.CHANNEL_DIRECT:
|
|
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
|
|
case model.CHANNEL_GROUP:
|
|
names := []string{}
|
|
for _, user := range n.ProfileMap {
|
|
if user.Id != excludeId {
|
|
names = append(names, user.GetDisplayName(userNameFormat))
|
|
}
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
return strings.Join(names, ", ")
|
|
default:
|
|
return n.Channel.DisplayName
|
|
}
|
|
}
|
|
|
|
// Returns the name of the sender of this notification, accounting for things like system messages
|
|
// and whether or not the username has been overridden by an integration.
|
|
func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
|
|
if n.Post.IsSystemMessage() {
|
|
return utils.T("system.message.name")
|
|
}
|
|
|
|
if overridesAllowed && n.Channel.Type != model.CHANNEL_DIRECT {
|
|
if value, ok := n.Post.GetProps()["override_username"]; ok && n.Post.GetProp("from_webhook") == "true" {
|
|
return value.(string)
|
|
}
|
|
}
|
|
|
|
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
|
|
}
|
|
|
|
// checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
|
|
func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string, groups map[string]*model.Group) bool {
|
|
var mentionType MentionType
|
|
|
|
switch strings.ToLower(word) {
|
|
case "@here":
|
|
m.HereMentioned = true
|
|
mentionType = ChannelMention
|
|
case "@channel":
|
|
m.ChannelMentioned = true
|
|
mentionType = ChannelMention
|
|
case "@all":
|
|
m.AllMentioned = true
|
|
mentionType = ChannelMention
|
|
default:
|
|
mentionType = KeywordMention
|
|
}
|
|
|
|
m.addGroupMention(word, groups)
|
|
|
|
if ids, match := keywords[strings.ToLower(word)]; match {
|
|
m.addMentions(ids, mentionType)
|
|
return true
|
|
}
|
|
|
|
// Case-sensitive check for first name
|
|
if ids, match := keywords[word]; match {
|
|
m.addMentions(ids, mentionType)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
|
|
func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
|
|
ids := []string{}
|
|
match := false
|
|
var multibyteKeywords []string
|
|
for keyword := range keywords {
|
|
if len(keyword) != utf8.RuneCountInString(keyword) {
|
|
multibyteKeywords = append(multibyteKeywords, keyword)
|
|
}
|
|
}
|
|
|
|
if len(word) != utf8.RuneCountInString(word) {
|
|
for _, key := range multibyteKeywords {
|
|
if strings.Contains(word, key) {
|
|
ids, match = keywords[key]
|
|
}
|
|
}
|
|
}
|
|
return ids, match
|
|
}
|
|
|
|
// Processes text to filter mentioned users and other potential mentions
|
|
func (m *ExplicitMentions) processText(text string, keywords map[string][]string, groups map[string]*model.Group) {
|
|
systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
|
|
|
|
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
|
|
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
|
|
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
|
|
}) {
|
|
// skip word with format ':word:' with an assumption that it is an emoji format only
|
|
if word[0] == ':' && word[len(word)-1] == ':' {
|
|
continue
|
|
}
|
|
|
|
word = strings.TrimLeft(word, ":.-_")
|
|
|
|
if m.checkForMention(word, keywords, groups) {
|
|
continue
|
|
}
|
|
|
|
foundWithoutSuffix := false
|
|
wordWithoutSuffix := word
|
|
|
|
for wordWithoutSuffix != "" && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
|
|
wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
|
|
|
|
if m.checkForMention(wordWithoutSuffix, keywords, groups) {
|
|
foundWithoutSuffix = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundWithoutSuffix {
|
|
continue
|
|
}
|
|
|
|
if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
|
|
// No need to bother about unicode as we are looking for ASCII characters.
|
|
last := word[len(word)-1]
|
|
switch last {
|
|
// If the word is possibly at the end of a sentence, remove that character.
|
|
case '.', '-', ':':
|
|
word = word[:len(word)-1]
|
|
}
|
|
m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
|
|
} else if strings.ContainsAny(word, ".-:") {
|
|
// This word contains a character that may be the end of a sentence, so split further
|
|
splitWords := strings.FieldsFunc(word, func(c rune) bool {
|
|
return c == '.' || c == '-' || c == ':'
|
|
})
|
|
|
|
for _, splitWord := range splitWords {
|
|
if m.checkForMention(splitWord, keywords, groups) {
|
|
continue
|
|
}
|
|
if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
|
|
m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
|
|
}
|
|
}
|
|
}
|
|
|
|
if ids, match := isKeywordMultibyte(keywords, word); match {
|
|
m.addMentions(ids, KeywordMention)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) GetNotificationNameFormat(user *model.User) string {
|
|
if !*a.Config().PrivacySettings.ShowFullName {
|
|
return model.SHOW_USERNAME
|
|
}
|
|
|
|
data, err := a.Srv().Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT)
|
|
if err != nil {
|
|
return *a.Config().TeamSettings.TeammateNameDisplay
|
|
}
|
|
|
|
return data.Value
|
|
}
|