mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
* reproduce panic with test * allow bots in the profile map * explicitly prevent sending notifications to bots * persistent notifications: handle senders not in the channel
402 lines
14 KiB
Go
402 lines
14 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"maps"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ResolvePersistentNotification stops the persistent notifications, if a loggedInUserID(except the post owner) reacts, reply or ack on the post.
|
|
// Post-owner can only delete the original post to stop the notifications.
|
|
func (a *App) ResolvePersistentNotification(rctx request.CTX, post *model.Post, loggedInUserID string) *model.AppError {
|
|
// Ignore the post owner's actions to their own post
|
|
if loggedInUserID == post.UserId {
|
|
return nil
|
|
}
|
|
|
|
if !a.IsPersistentNotificationsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
// Either the notification post is already deleted or was never a notification post
|
|
return nil
|
|
default:
|
|
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if !*a.Config().ServiceSettings.AllowPersistentNotificationsForGuests {
|
|
user, nErr := a.Srv().Store().User().Get(context.Background(), loggedInUserID)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("ResolvePersistentNotification", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("ResolvePersistentNotification", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
if user.IsGuest() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
stopNotifications := false
|
|
if err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
|
|
if mentions.isUserMentioned(loggedInUserID) {
|
|
stopNotifications = true
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Only mentioned users can stop the notifications
|
|
if !stopNotifications {
|
|
return nil
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
|
|
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeletePersistentNotification stops the persistent notifications.
|
|
func (a *App) DeletePersistentNotification(rctx request.CTX, post *model.Post) *model.AppError {
|
|
if !a.IsPersistentNotificationsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
// Either the notification post is already deleted or was never a notification post
|
|
return nil
|
|
default:
|
|
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
|
|
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SendPersistentNotifications() error {
|
|
notificationInterval := time.Duration(*a.Config().ServiceSettings.PersistentNotificationIntervalMinutes) * time.Minute
|
|
notificationMaxCount := int16(*a.Config().ServiceSettings.PersistentNotificationMaxCount)
|
|
|
|
// fetch posts for which the "notificationInterval duration" has passed
|
|
maxTime := time.Now().Add(-notificationInterval).UnixMilli()
|
|
|
|
// Pagination loop
|
|
for {
|
|
notificationPosts, err := a.Srv().Store().PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
|
|
MaxTime: maxTime,
|
|
MaxSentCount: notificationMaxCount,
|
|
PerPage: 500,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get posts for persistent notifications")
|
|
}
|
|
|
|
// No posts left to send persistent notifications
|
|
if len(notificationPosts) == 0 {
|
|
break
|
|
}
|
|
|
|
postIds := make([]string, 0, len(notificationPosts))
|
|
for _, p := range notificationPosts {
|
|
postIds = append(postIds, p.PostId)
|
|
}
|
|
posts, err := a.Srv().Store().Post().GetPostsByIds(postIds)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get posts by IDs")
|
|
}
|
|
|
|
// Send notifications
|
|
if err := a.forEachPersistentNotificationPost(posts, a.sendPersistentNotifications); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().UpdateLastActivity(postIds); err != nil {
|
|
return errors.Wrapf(err, "failed to update lastActivity for notifications: %v", postIds)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().DeleteExpired(notificationMaxCount); err != nil {
|
|
return errors.Wrap(err, "failed to delete expired notifications")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(post *model.Post, channel *model.Channel, team *model.Team, mentions *MentionResults, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error) error {
|
|
channelsMap, teamsMap, err := a.channelTeamMapsForPosts(posts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, err := a.persistentNotificationsAuxiliaryData(channelsMap, teamsMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, post := range posts {
|
|
channel := channelsMap[post.ChannelId]
|
|
team := teamsMap[channel.TeamId]
|
|
// GMs and DMs don't belong to any team
|
|
if channel.IsGroupOrDirect() {
|
|
team = &model.Team{}
|
|
}
|
|
profileMap := channelProfileMap[channel.Id]
|
|
|
|
// Ensure the sender is always in the profile map: for example, system admins can post
|
|
// without being a member.
|
|
if _, ok := profileMap[post.UserId]; !ok {
|
|
var sender *model.User
|
|
sender, err = a.Srv().Store().User().Get(context.Background(), post.UserId)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get profile for sender user %s for post %s", post.UserId, post.Id)
|
|
}
|
|
|
|
profileMap[post.UserId] = sender
|
|
}
|
|
|
|
mentions := &MentionResults{}
|
|
// In DMs, only the "other" user can be mentioned
|
|
if channel.Type == model.ChannelTypeDirect {
|
|
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
|
|
if _, ok := profileMap[otherUserId]; ok {
|
|
mentions.addMention(otherUserId, DMMention)
|
|
}
|
|
} else {
|
|
keywords := channelKeywords[channel.Id]
|
|
keywords.AddGroupsMap(channelGroupMap[channel.Id])
|
|
|
|
mentions = getExplicitMentions(post, keywords)
|
|
for groupID := range mentions.GroupMentions {
|
|
group := channelGroupMap[channel.Id][groupID]
|
|
_, err := a.insertGroupMentions(post.UserId, group, channel, profileMap, mentions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to include mentions from group - %s for channel - %s", group.Id, channel.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := fn(post, channel, team, mentions, profileMap, channelNotifyProps); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model.Channel, teamsMap map[string]*model.Team) (map[string]map[string]*model.Group, map[string]model.UserMap, map[string]MentionKeywords, map[string]map[string]model.StringMap, error) {
|
|
channelGroupMap := make(map[string]map[string]*model.Group, len(channelsMap))
|
|
channelProfileMap := make(map[string]model.UserMap, len(channelsMap))
|
|
channelKeywords := make(map[string]MentionKeywords, len(channelsMap))
|
|
channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
|
|
for _, c := range channelsMap {
|
|
// In DM, notifications can't be send to any 3rd person.
|
|
if c.Type != model.ChannelTypeDirect {
|
|
groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
|
|
if err != nil {
|
|
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
|
|
}
|
|
channelGroupMap[c.Id] = make(map[string]*model.Group, len(groups))
|
|
maps.Copy(channelGroupMap[c.Id], groups)
|
|
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(c.Id, true)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
|
|
}
|
|
channelNotifyProps[c.Id] = props
|
|
}
|
|
|
|
profileMap, err := a.Srv().Store().User().GetAllProfilesInChannel(context.Background(), c.Id, true)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
|
|
}
|
|
|
|
channelKeywords[c.Id] = make(MentionKeywords, len(profileMap))
|
|
for userID, user := range profileMap {
|
|
if !user.IsBot {
|
|
channelKeywords[c.Id].AddUserKeyword(userID, "@"+user.Username)
|
|
}
|
|
}
|
|
channelProfileMap[c.Id] = profileMap
|
|
}
|
|
return channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, nil
|
|
}
|
|
|
|
func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Channel, map[string]*model.Team, error) {
|
|
channelIds := make(model.StringSet)
|
|
for _, p := range posts {
|
|
channelIds.Add(p.ChannelId)
|
|
}
|
|
channels, err := a.Srv().Store().Channel().GetChannelsByIds(channelIds.Val(), false)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
|
|
}
|
|
channelsMap := make(map[string]*model.Channel, len(channels))
|
|
for _, c := range channels {
|
|
channelsMap[c.Id] = c
|
|
}
|
|
|
|
teamIds := make(model.StringSet)
|
|
for _, c := range channels {
|
|
if c.TeamId != "" {
|
|
teamIds.Add(c.TeamId)
|
|
}
|
|
}
|
|
teams := make([]*model.Team, 0, len(teamIds))
|
|
if len(teamIds) > 0 {
|
|
teams, err = a.Srv().Store().Team().GetMany(teamIds.Val())
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
|
|
}
|
|
}
|
|
teamsMap := make(map[string]*model.Team, len(teams))
|
|
for _, t := range teams {
|
|
teamsMap[t.Id] = t
|
|
}
|
|
return channelsMap, teamsMap, nil
|
|
}
|
|
|
|
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *MentionResults, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
|
|
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
|
|
for id, v := range mentions.Mentions {
|
|
// Don't send notification to post owner nor GM mentions
|
|
if id != post.UserId && v > GMMention {
|
|
mentionedUsersList = append(mentionedUsersList, id)
|
|
}
|
|
}
|
|
|
|
sender := profileMap[post.UserId]
|
|
notification := &PostNotification{
|
|
Post: post,
|
|
Channel: channel,
|
|
ProfileMap: profileMap,
|
|
Sender: sender,
|
|
}
|
|
|
|
if int64(len(mentionedUsersList)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
|
|
return errors.Errorf("mentioned users: %d are more than allowed users: %d", len(mentionedUsersList), *a.Config().TeamSettings.MaxNotificationsPerChannel)
|
|
}
|
|
|
|
if a.canSendPushNotifications() {
|
|
for _, userID := range mentionedUsersList {
|
|
user := profileMap[userID]
|
|
if user == nil {
|
|
continue
|
|
}
|
|
rctx := request.EmptyContext(a.Log().With(
|
|
mlog.String("receiver_id", userID),
|
|
))
|
|
|
|
status, err := a.GetStatus(userID)
|
|
if err != nil {
|
|
mlog.Warn("Unable to fetch online status", mlog.Err(err))
|
|
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
|
}
|
|
|
|
isGM := channel.Type == model.ChannelTypeGroup
|
|
if a.ShouldSendPushNotification(rctx, profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post, isGM) {
|
|
a.sendPushNotification(
|
|
notification,
|
|
user,
|
|
true,
|
|
false,
|
|
"",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
desktopUsers := make([]string, 0, len(mentionedUsersList))
|
|
for _, id := range mentionedUsersList {
|
|
user := profileMap[id]
|
|
if user == nil {
|
|
continue
|
|
}
|
|
|
|
if user.NotifyProps[model.DesktopNotifyProp] != model.UserNotifyNone && a.persistentNotificationsAllowedForStatus(id) {
|
|
desktopUsers = append(desktopUsers, id)
|
|
}
|
|
}
|
|
|
|
if len(desktopUsers) != 0 {
|
|
post = a.PreparePostForClient(request.EmptyContext(a.Log()), post, &model.PreparePostForClientOpts{IncludePriority: true})
|
|
postJSON, jsonErr := post.ToJSON()
|
|
if jsonErr != nil {
|
|
return errors.Wrapf(jsonErr, "failed to encode post to JSON")
|
|
}
|
|
|
|
for _, u := range desktopUsers {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPersistentNotificationTriggered, team.Id, post.ChannelId, u, nil, "")
|
|
|
|
message.Add("post", postJSON)
|
|
message.Add("channel_type", channel.Type)
|
|
message.Add("channel_display_name", notification.GetChannelName(model.ShowUsername, ""))
|
|
message.Add("channel_name", channel.Name)
|
|
message.Add("sender_name", notification.GetSenderName(model.ShowUsername, *a.Config().ServiceSettings.EnablePostUsernameOverride))
|
|
message.Add("team_id", team.Id)
|
|
|
|
if len(post.FileIds) != 0 {
|
|
message.Add("otherFile", "true")
|
|
|
|
infos, err := a.Srv().Store().FileInfo().GetForPost(post.Id, false, false, true)
|
|
if err != nil {
|
|
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(err))
|
|
}
|
|
|
|
for _, info := range infos {
|
|
if info.IsImage() {
|
|
message.Add("image", "true")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
message.Add("mentions", model.ArrayToJSON(desktopUsers))
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) persistentNotificationsAllowedForStatus(userID string) bool {
|
|
var status *model.Status
|
|
var err *model.AppError
|
|
if status, err = a.GetStatus(userID); err != nil {
|
|
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
|
}
|
|
|
|
return status.Status != model.StatusDnd && status.Status != model.StatusOutOfOffice
|
|
}
|
|
|
|
func (a *App) IsPersistentNotificationsEnabled() bool {
|
|
return a.IsPostPriorityEnabled() && *a.Config().ServiceSettings.AllowPersistentNotifications
|
|
}
|