github.com/spline-fu/mattermost-server@v4.10.10+incompatible/app/notification.go (about)

     1  // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"html"
     9  	"html/template"
    10  	"net/http"
    11  	"net/url"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  	"unicode"
    17  
    18  	"github.com/mattermost/mattermost-server/mlog"
    19  	"github.com/mattermost/mattermost-server/model"
    20  	"github.com/mattermost/mattermost-server/store"
    21  	"github.com/mattermost/mattermost-server/utils"
    22  	"github.com/mattermost/mattermost-server/utils/markdown"
    23  	"github.com/nicksnyder/go-i18n/i18n"
    24  )
    25  
    26  func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, *model.AppError) {
    27  	pchan := a.Srv.Store.User().GetAllProfilesInChannel(channel.Id, true)
    28  	cmnchan := a.Srv.Store.Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
    29  	var fchan store.StoreChannel
    30  
    31  	if len(post.FileIds) != 0 {
    32  		fchan = a.Srv.Store.FileInfo().GetForPost(post.Id, true, true)
    33  	}
    34  
    35  	var profileMap map[string]*model.User
    36  	if result := <-pchan; result.Err != nil {
    37  		return nil, result.Err
    38  	} else {
    39  		profileMap = result.Data.(map[string]*model.User)
    40  	}
    41  
    42  	var channelMemberNotifyPropsMap map[string]model.StringMap
    43  	if result := <-cmnchan; result.Err != nil {
    44  		return nil, result.Err
    45  	} else {
    46  		channelMemberNotifyPropsMap = result.Data.(map[string]model.StringMap)
    47  	}
    48  
    49  	mentionedUserIds := make(map[string]bool)
    50  	allActivityPushUserIds := []string{}
    51  	hereNotification := false
    52  	channelNotification := false
    53  	allNotification := false
    54  	updateMentionChans := []store.StoreChannel{}
    55  
    56  	if channel.Type == model.CHANNEL_DIRECT {
    57  		var otherUserId string
    58  
    59  		userIds := strings.Split(channel.Name, "__")
    60  
    61  		if userIds[0] != userIds[1] {
    62  			if userIds[0] == post.UserId {
    63  				otherUserId = userIds[1]
    64  			} else {
    65  				otherUserId = userIds[0]
    66  			}
    67  		}
    68  
    69  		otherUser, ok := profileMap[otherUserId]
    70  		if ok {
    71  			mentionedUserIds[otherUserId] = true
    72  		}
    73  
    74  		if post.Props["from_webhook"] == "true" {
    75  			mentionedUserIds[post.UserId] = true
    76  		}
    77  
    78  		if post.Type != model.POST_AUTO_RESPONDER {
    79  			a.Go(func() {
    80  				rootId := post.Id
    81  				if post.RootId != "" && post.RootId != post.Id {
    82  					rootId = post.RootId
    83  				}
    84  				a.SendAutoResponse(channel, otherUser, rootId)
    85  			})
    86  		}
    87  
    88  	} else {
    89  		keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE)
    90  
    91  		m := GetExplicitMentions(post.Message, keywords)
    92  
    93  		// Add an implicit mention when a user is added to a channel
    94  		// even if the user has set 'username mentions' to false in account settings.
    95  		if post.Type == model.POST_ADD_TO_CHANNEL {
    96  			val := post.Props[model.POST_PROPS_ADDED_USER_ID]
    97  			if val != nil {
    98  				uid := val.(string)
    99  				m.MentionedUserIds[uid] = true
   100  			}
   101  		}
   102  
   103  		mentionedUserIds, hereNotification, channelNotification, allNotification = m.MentionedUserIds, m.HereMentioned, m.ChannelMentioned, m.AllMentioned
   104  
   105  		// get users that have comment thread mentions enabled
   106  		if len(post.RootId) > 0 && parentPostList != nil {
   107  			for _, threadPost := range parentPostList.Posts {
   108  				profile := profileMap[threadPost.UserId]
   109  				if profile != nil && (profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == parentPostList.Order[0])) {
   110  					mentionedUserIds[threadPost.UserId] = true
   111  				}
   112  			}
   113  		}
   114  
   115  		// prevent the user from mentioning themselves
   116  		if post.Props["from_webhook"] != "true" {
   117  			delete(mentionedUserIds, post.UserId)
   118  		}
   119  
   120  		if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() {
   121  			if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil {
   122  				outOfChannelMentions := result.Data.([]*model.User)
   123  				if channel.Type != model.CHANNEL_GROUP {
   124  					a.Go(func() {
   125  						a.sendOutOfChannelMentions(sender, post, outOfChannelMentions)
   126  					})
   127  				}
   128  			}
   129  		}
   130  
   131  		// find which users in the channel are set up to always receive mobile notifications
   132  		for _, profile := range profileMap {
   133  			if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL ||
   134  				channelMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CHANNEL_NOTIFY_ALL) &&
   135  				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
   136  				!post.IsSystemMessage() {
   137  				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
   138  			}
   139  		}
   140  	}
   141  
   142  	mentionedUsersList := make([]string, 0, len(mentionedUserIds))
   143  	for id := range mentionedUserIds {
   144  		mentionedUsersList = append(mentionedUsersList, id)
   145  		updateMentionChans = append(updateMentionChans, a.Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
   146  	}
   147  
   148  	senderName := ""
   149  	channelName := ""
   150  	if post.IsSystemMessage() {
   151  		senderName = utils.T("system.message.name")
   152  	} else {
   153  		if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
   154  			senderName = value.(string)
   155  		} else {
   156  			senderName = sender.Username
   157  		}
   158  	}
   159  
   160  	if channel.Type == model.CHANNEL_GROUP {
   161  		userList := []*model.User{}
   162  		for _, u := range profileMap {
   163  			if u.Id != sender.Id {
   164  				userList = append(userList, u)
   165  			}
   166  		}
   167  		userList = append(userList, sender)
   168  		channelName = model.GetGroupDisplayNameFromUsers(userList, false)
   169  	} else {
   170  		channelName = channel.DisplayName
   171  	}
   172  
   173  	var senderUsername string
   174  	if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
   175  		senderUsername = value.(string)
   176  	} else {
   177  		senderUsername = sender.Username
   178  	}
   179  
   180  	if a.Config().EmailSettings.SendEmailNotifications {
   181  		for _, id := range mentionedUsersList {
   182  			if profileMap[id] == nil {
   183  				continue
   184  			}
   185  
   186  			userAllowsEmails := profileMap[id].NotifyProps[model.EMAIL_NOTIFY_PROP] != "false"
   187  			if channelEmail, ok := channelMemberNotifyPropsMap[id][model.EMAIL_NOTIFY_PROP]; ok {
   188  				if channelEmail != model.CHANNEL_NOTIFY_DEFAULT {
   189  					userAllowsEmails = channelEmail != "false"
   190  				}
   191  			}
   192  
   193  			// Remove the user as recipient when the user has muted the channel.
   194  			if channelMuted, ok := channelMemberNotifyPropsMap[id][model.MARK_UNREAD_NOTIFY_PROP]; ok {
   195  				if channelMuted == model.CHANNEL_MARK_UNREAD_MENTION {
   196  					mlog.Debug(fmt.Sprintf("Channel muted for user_id %v, channel_mute %v", id, channelMuted))
   197  					userAllowsEmails = false
   198  				}
   199  			}
   200  
   201  			//If email verification is required and user email is not verified don't send email.
   202  			if a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
   203  				mlog.Error(fmt.Sprintf("Skipped sending notification email to %v, address not verified. [details: user_id=%v]", profileMap[id].Email, id))
   204  				continue
   205  			}
   206  
   207  			var status *model.Status
   208  			var err *model.AppError
   209  			if status, err = a.GetStatus(id); err != nil {
   210  				status = &model.Status{
   211  					UserId:         id,
   212  					Status:         model.STATUS_OFFLINE,
   213  					Manual:         false,
   214  					LastActivityAt: 0,
   215  					ActiveChannel:  "",
   216  				}
   217  			}
   218  
   219  			autoResponderRelated := status.Status == model.STATUS_OUT_OF_OFFICE || post.Type == model.POST_AUTO_RESPONDER
   220  
   221  			if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 && !autoResponderRelated {
   222  				a.sendNotificationEmail(post, profileMap[id], channel, team, senderName, sender)
   223  			}
   224  		}
   225  	}
   226  
   227  	T := utils.GetUserTranslations(sender.Locale)
   228  
   229  	// If the channel has more than 1K users then @here is disabled
   230  	if hereNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   231  		hereNotification = false
   232  		a.SendEphemeralPost(
   233  			post.UserId,
   234  			&model.Post{
   235  				ChannelId: post.ChannelId,
   236  				Message:   T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   237  				CreateAt:  post.CreateAt + 1,
   238  			},
   239  		)
   240  	}
   241  
   242  	// If the channel has more than 1K users then @channel is disabled
   243  	if channelNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   244  		a.SendEphemeralPost(
   245  			post.UserId,
   246  			&model.Post{
   247  				ChannelId: post.ChannelId,
   248  				Message:   T("api.post.disabled_channel", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   249  				CreateAt:  post.CreateAt + 1,
   250  			},
   251  		)
   252  	}
   253  
   254  	// If the channel has more than 1K users then @all is disabled
   255  	if allNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   256  		a.SendEphemeralPost(
   257  			post.UserId,
   258  			&model.Post{
   259  				ChannelId: post.ChannelId,
   260  				Message:   T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   261  				CreateAt:  post.CreateAt + 1,
   262  			},
   263  		)
   264  	}
   265  
   266  	// Make sure all mention updates are complete to prevent race
   267  	// Probably better to batch these DB updates in the future
   268  	// MUST be completed before push notifications send
   269  	for _, uchan := range updateMentionChans {
   270  		if result := <-uchan; result.Err != nil {
   271  			mlog.Warn(fmt.Sprintf("Failed to update mention count, post_id=%v channel_id=%v err=%v", post.Id, post.ChannelId, result.Err), mlog.String("post_id", post.Id))
   272  		}
   273  	}
   274  
   275  	sendPushNotifications := false
   276  	if *a.Config().EmailSettings.SendPushNotifications {
   277  		pushServer := *a.Config().EmailSettings.PushNotificationServer
   278  		if license := a.License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
   279  			mlog.Warn("api.post.send_notifications_and_forget.push_notification.mhpnsWarn FIXME: NOT FOUND IN TRANSLATIONS FILE")
   280  			sendPushNotifications = false
   281  		} else {
   282  			sendPushNotifications = true
   283  		}
   284  	}
   285  
   286  	if sendPushNotifications {
   287  		for _, id := range mentionedUsersList {
   288  			if profileMap[id] == nil {
   289  				continue
   290  			}
   291  
   292  			var status *model.Status
   293  			var err *model.AppError
   294  			if status, err = a.GetStatus(id); err != nil {
   295  				status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   296  			}
   297  
   298  			if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
   299  				a.sendPushNotification(post, profileMap[id], channel, senderName, channelName, true)
   300  			}
   301  		}
   302  
   303  		for _, id := range allActivityPushUserIds {
   304  			if profileMap[id] == nil {
   305  				continue
   306  			}
   307  
   308  			if _, ok := mentionedUserIds[id]; !ok {
   309  				var status *model.Status
   310  				var err *model.AppError
   311  				if status, err = a.GetStatus(id); err != nil {
   312  					status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   313  				}
   314  
   315  				if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
   316  					a.sendPushNotification(post, profileMap[id], channel, senderName, channelName, false)
   317  				}
   318  			}
   319  		}
   320  	}
   321  
   322  	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
   323  	message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson())
   324  	message.Add("channel_type", channel.Type)
   325  	message.Add("channel_display_name", channelName)
   326  	message.Add("channel_name", channel.Name)
   327  	message.Add("sender_name", senderUsername)
   328  	message.Add("team_id", team.Id)
   329  
   330  	if len(post.FileIds) != 0 && fchan != nil {
   331  		message.Add("otherFile", "true")
   332  
   333  		var infos []*model.FileInfo
   334  		if result := <-fchan; result.Err != nil {
   335  			mlog.Warn(fmt.Sprint("api.post.send_notifications.files.error FIXME: NOT FOUND IN TRANSLATIONS FILE", post.Id, result.Err), mlog.String("post_id", post.Id))
   336  		} else {
   337  			infos = result.Data.([]*model.FileInfo)
   338  		}
   339  
   340  		for _, info := range infos {
   341  			if info.IsImage() {
   342  				message.Add("image", "true")
   343  				break
   344  			}
   345  		}
   346  	}
   347  
   348  	if len(mentionedUsersList) != 0 {
   349  		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
   350  	}
   351  
   352  	a.Publish(message)
   353  	return mentionedUsersList, nil
   354  }
   355  
   356  func (a *App) sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
   357  	if channel.IsGroupOrDirect() {
   358  		if result := <-a.Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
   359  			return result.Err
   360  		} else {
   361  			// if the recipient isn't in the current user's team, just pick one
   362  			teams := result.Data.([]*model.Team)
   363  			found := false
   364  
   365  			for i := range teams {
   366  				if teams[i].Id == team.Id {
   367  					found = true
   368  					break
   369  				}
   370  			}
   371  
   372  			if !found && len(teams) > 0 {
   373  				team = teams[0]
   374  			} else {
   375  				// in case the user hasn't joined any teams we send them to the select_team page
   376  				team = &model.Team{Name: "select_team", DisplayName: a.Config().TeamSettings.SiteName}
   377  			}
   378  		}
   379  	}
   380  	if *a.Config().EmailSettings.EnableEmailBatching {
   381  		var sendBatched bool
   382  		if result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil {
   383  			// if the call fails, assume that the interval has not been explicitly set and batch the notifications
   384  			sendBatched = true
   385  		} else {
   386  			// if the user has chosen to receive notifications immediately, don't batch them
   387  			sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS
   388  		}
   389  
   390  		if sendBatched {
   391  			if err := a.AddNotificationEmailToBatch(user, post, team); err == nil {
   392  				return nil
   393  			}
   394  		}
   395  
   396  		// fall back to sending a single email if we can't batch it for some reason
   397  	}
   398  
   399  	translateFunc := utils.GetUserTranslations(user.Locale)
   400  
   401  	var subjectText string
   402  	if channel.Type == model.CHANNEL_DIRECT {
   403  		subjectText = getDirectMessageNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, senderName)
   404  	} else if *a.Config().EmailSettings.UseChannelInEmailNotifications {
   405  		subjectText = getNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channel.DisplayName+")")
   406  	} else {
   407  		subjectText = getNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName)
   408  	}
   409  
   410  	emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
   411  	if license := a.License(); license != nil && *license.Features.EmailNotificationContents {
   412  		emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
   413  	}
   414  
   415  	teamURL := a.GetSiteURL() + "/" + team.Name
   416  	var bodyText = a.getNotificationEmailBody(user, post, channel, senderName, team.Name, teamURL, emailNotificationContentsType, translateFunc)
   417  
   418  	a.Go(func() {
   419  		if err := a.SendMail(user.Email, html.UnescapeString(subjectText), bodyText); err != nil {
   420  			mlog.Error(fmt.Sprint("api.post.send_notifications_and_forget.send.error FIXME: NOT FOUND IN TRANSLATIONS FILE", user.Email, err))
   421  		}
   422  	})
   423  
   424  	if a.Metrics != nil {
   425  		a.Metrics.IncrementPostSentEmail()
   426  	}
   427  
   428  	return nil
   429  }
   430  
   431  /**
   432   * Computes the subject line for direct notification email messages
   433   */
   434  func getDirectMessageNotificationEmailSubject(post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string) string {
   435  	t := getFormattedPostTime(post, translateFunc)
   436  	var subjectParameters = map[string]interface{}{
   437  		"SiteName":          siteName,
   438  		"SenderDisplayName": senderName,
   439  		"Month":             t.Month,
   440  		"Day":               t.Day,
   441  		"Year":              t.Year,
   442  	}
   443  	return translateFunc("app.notification.subject.direct.full", subjectParameters)
   444  }
   445  
   446  /**
   447   * Computes the subject line for group, public, and private email messages
   448   */
   449  func getNotificationEmailSubject(post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string) string {
   450  	t := getFormattedPostTime(post, translateFunc)
   451  	var subjectParameters = map[string]interface{}{
   452  		"SiteName": siteName,
   453  		"TeamName": teamName,
   454  		"Month":    t.Month,
   455  		"Day":      t.Day,
   456  		"Year":     t.Year,
   457  	}
   458  	return translateFunc("app.notification.subject.notification.full", subjectParameters)
   459  }
   460  
   461  /**
   462   * Computes the email body for notification messages
   463   */
   464  func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, senderName string, teamName string, teamURL string, emailNotificationContentsType string, translateFunc i18n.TranslateFunc) string {
   465  	// only include message contents in notification email if email notification contents type is set to full
   466  	var bodyPage *utils.HTMLTemplate
   467  	if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   468  		bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale)
   469  		bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc)
   470  	} else {
   471  		bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale)
   472  	}
   473  
   474  	bodyPage.Props["SiteURL"] = a.GetSiteURL()
   475  	if teamName != "select_team" {
   476  		bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
   477  	} else {
   478  		bodyPage.Props["TeamLink"] = teamURL
   479  	}
   480  
   481  	var channelName = channel.DisplayName
   482  	if channel.Type == model.CHANNEL_GROUP {
   483  		channelName = translateFunc("api.templates.channel_name.group")
   484  	}
   485  	t := getFormattedPostTime(post, translateFunc)
   486  
   487  	var bodyText string
   488  	var info template.HTML
   489  	if channel.Type == model.CHANNEL_DIRECT {
   490  		if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   491  			bodyText = translateFunc("app.notification.body.intro.direct.full")
   492  			info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.full",
   493  				map[string]interface{}{
   494  					"SenderName": senderName,
   495  					"Hour":       t.Hour,
   496  					"Minute":     t.Minute,
   497  					"TimeZone":   t.TimeZone,
   498  					"Month":      t.Month,
   499  					"Day":        t.Day,
   500  				})
   501  		} else {
   502  			bodyText = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{
   503  				"SenderName": senderName,
   504  			})
   505  			info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.generic",
   506  				map[string]interface{}{
   507  					"Hour":     t.Hour,
   508  					"Minute":   t.Minute,
   509  					"TimeZone": t.TimeZone,
   510  					"Month":    t.Month,
   511  					"Day":      t.Day,
   512  				})
   513  		}
   514  	} else {
   515  		if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   516  			bodyText = translateFunc("app.notification.body.intro.notification.full")
   517  			info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.full",
   518  				map[string]interface{}{
   519  					"ChannelName": channelName,
   520  					"SenderName":  senderName,
   521  					"Hour":        t.Hour,
   522  					"Minute":      t.Minute,
   523  					"TimeZone":    t.TimeZone,
   524  					"Month":       t.Month,
   525  					"Day":         t.Day,
   526  				})
   527  		} else {
   528  			bodyText = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{
   529  				"SenderName": senderName,
   530  			})
   531  			info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.generic",
   532  				map[string]interface{}{
   533  					"Hour":     t.Hour,
   534  					"Minute":   t.Minute,
   535  					"TimeZone": t.TimeZone,
   536  					"Month":    t.Month,
   537  					"Day":      t.Day,
   538  				})
   539  		}
   540  	}
   541  
   542  	bodyPage.Props["BodyText"] = bodyText
   543  	bodyPage.Html["Info"] = info
   544  	bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button")
   545  
   546  	return bodyPage.Render()
   547  }
   548  
   549  type formattedPostTime struct {
   550  	Time     time.Time
   551  	Year     string
   552  	Month    string
   553  	Day      string
   554  	Hour     string
   555  	Minute   string
   556  	TimeZone string
   557  }
   558  
   559  func getFormattedPostTime(post *model.Post, translateFunc i18n.TranslateFunc) formattedPostTime {
   560  	tm := time.Unix(post.CreateAt/1000, 0)
   561  	zone, _ := tm.Zone()
   562  
   563  	return formattedPostTime{
   564  		Time:     tm,
   565  		Year:     fmt.Sprintf("%d", tm.Year()),
   566  		Month:    translateFunc(tm.Month().String()),
   567  		Day:      fmt.Sprintf("%d", tm.Day()),
   568  		Hour:     fmt.Sprintf("%02d", tm.Hour()),
   569  		Minute:   fmt.Sprintf("%02d", tm.Minute()),
   570  		TimeZone: zone,
   571  	}
   572  }
   573  
   574  func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
   575  	if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 {
   576  		return post.Message
   577  	}
   578  
   579  	// extract the filenames from their paths and determine what type of files are attached
   580  	var infos []*model.FileInfo
   581  	if result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, true); result.Err != nil {
   582  		mlog.Warn(fmt.Sprintf("Encountered error when getting files for notification message, post_id=%v, err=%v", post.Id, result.Err), mlog.String("post_id", post.Id))
   583  	} else {
   584  		infos = result.Data.([]*model.FileInfo)
   585  	}
   586  
   587  	filenames := make([]string, len(infos))
   588  	onlyImages := true
   589  	for i, info := range infos {
   590  		if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
   591  			// this should never error since filepath was escaped using url.QueryEscape
   592  			filenames[i] = escaped
   593  		} else {
   594  			filenames[i] = info.Name
   595  		}
   596  
   597  		onlyImages = onlyImages && info.IsImage()
   598  	}
   599  
   600  	props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
   601  
   602  	if onlyImages {
   603  		return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
   604  	} else {
   605  		return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
   606  	}
   607  }
   608  
   609  func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName, channelName string, wasMentioned bool) *model.AppError {
   610  	sessions, err := a.getMobileAppSessions(user.Id)
   611  	if err != nil {
   612  		return err
   613  	}
   614  
   615  	if channel.Type == model.CHANNEL_DIRECT {
   616  		channelName = senderName
   617  	}
   618  
   619  	msg := model.PushNotification{}
   620  	if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
   621  		msg.Badge = 1
   622  		mlog.Error(fmt.Sprint("We could not get the unread message count for the user", user.Id, badge.Err), mlog.String("user_id", user.Id))
   623  	} else {
   624  		msg.Badge = int(badge.Data.(int64))
   625  	}
   626  
   627  	msg.Type = model.PUSH_TYPE_MESSAGE
   628  	msg.TeamId = channel.TeamId
   629  	msg.ChannelId = channel.Id
   630  	msg.PostId = post.Id
   631  	msg.RootId = post.RootId
   632  	msg.ChannelName = channel.Name
   633  	msg.SenderId = post.UserId
   634  
   635  	if ou, ok := post.Props["override_username"].(string); ok {
   636  		msg.OverrideUsername = ou
   637  	}
   638  
   639  	if oi, ok := post.Props["override_icon_url"].(string); ok {
   640  		msg.OverrideIconUrl = oi
   641  	}
   642  
   643  	if fw, ok := post.Props["from_webhook"].(string); ok {
   644  		msg.FromWebhook = fw
   645  	}
   646  
   647  	userLocale := utils.GetUserTranslations(user.Locale)
   648  	hasFiles := post.FileIds != nil && len(post.FileIds) > 0
   649  
   650  	msg.Message, msg.Category = a.getPushNotificationMessage(post.Message, wasMentioned, hasFiles, senderName, channelName, channel.Type, userLocale)
   651  
   652  	for _, session := range sessions {
   653  		tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   654  		tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   655  
   656  		mlog.Debug(fmt.Sprintf("Sending push notification to device %v for user %v with msg of '%v'", tmpMessage.DeviceId, user.Id, msg.Message), mlog.String("user_id", user.Id))
   657  
   658  		a.Go(func(session *model.Session) func() {
   659  			return func() {
   660  				a.sendToPushProxy(tmpMessage, session)
   661  			}
   662  		}(session))
   663  
   664  		if a.Metrics != nil {
   665  			a.Metrics.IncrementPostSentPush()
   666  		}
   667  	}
   668  
   669  	return nil
   670  }
   671  
   672  func (a *App) getPushNotificationMessage(postMessage string, wasMentioned bool, hasFiles bool, senderName string, channelName string, channelType string, userLocale i18n.TranslateFunc) (string, string) {
   673  	message := ""
   674  	category := ""
   675  
   676  	contentsConfig := *a.Config().EmailSettings.PushNotificationContents
   677  
   678  	if contentsConfig == model.FULL_NOTIFICATION {
   679  		category = model.CATEGORY_CAN_REPLY
   680  
   681  		if channelType == model.CHANNEL_DIRECT {
   682  			message = senderName + ": " + model.ClearMentionTags(postMessage)
   683  		} else {
   684  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(postMessage)
   685  		}
   686  	} else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
   687  		if channelType == model.CHANNEL_DIRECT {
   688  			category = model.CATEGORY_CAN_REPLY
   689  
   690  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
   691  		} else if wasMentioned {
   692  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
   693  		} else {
   694  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
   695  		}
   696  	} else {
   697  		if channelType == model.CHANNEL_DIRECT {
   698  			category = model.CATEGORY_CAN_REPLY
   699  
   700  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
   701  		} else if wasMentioned {
   702  			category = model.CATEGORY_CAN_REPLY
   703  
   704  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
   705  		} else {
   706  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
   707  		}
   708  	}
   709  
   710  	// If the post only has images then push an appropriate message
   711  	if len(postMessage) == 0 && hasFiles {
   712  		if channelType == model.CHANNEL_DIRECT {
   713  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
   714  		} else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
   715  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_no_channel")
   716  		} else {
   717  			message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
   718  		}
   719  	}
   720  
   721  	return message, category
   722  }
   723  
   724  func (a *App) ClearPushNotification(userId string, channelId string) {
   725  	a.Go(func() {
   726  		// Sleep is to allow the read replicas a chance to fully sync
   727  		// the unread count for sending an accurate count.
   728  		// Delaying a little doesn't hurt anything and is cheaper than
   729  		// attempting to read from master.
   730  		time.Sleep(time.Second * 5)
   731  
   732  		sessions, err := a.getMobileAppSessions(userId)
   733  		if err != nil {
   734  			mlog.Error(err.Error())
   735  			return
   736  		}
   737  
   738  		msg := model.PushNotification{}
   739  		msg.Type = model.PUSH_TYPE_CLEAR
   740  		msg.ChannelId = channelId
   741  		msg.ContentAvailable = 0
   742  		if badge := <-a.Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
   743  			msg.Badge = 0
   744  			mlog.Error(fmt.Sprint("We could not get the unread message count for the user", userId, badge.Err), mlog.String("user_id", userId))
   745  		} else {
   746  			msg.Badge = int(badge.Data.(int64))
   747  		}
   748  
   749  		mlog.Debug(fmt.Sprintf("Clearing push notification to %v with channel_id %v", msg.DeviceId, msg.ChannelId))
   750  
   751  		for _, session := range sessions {
   752  			tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   753  			tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   754  			a.Go(func() {
   755  				a.sendToPushProxy(tmpMessage, session)
   756  			})
   757  		}
   758  	})
   759  }
   760  
   761  func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) {
   762  	msg.ServerId = a.DiagnosticId()
   763  
   764  	request, _ := http.NewRequest("POST", strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
   765  
   766  	if resp, err := a.HTTPClient(true).Do(request); err != nil {
   767  		mlog.Error(fmt.Sprintf("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error()), mlog.String("user_id", session.UserId))
   768  	} else {
   769  		defer resp.Body.Close()
   770  
   771  		pushResponse := model.PushResponseFromJson(resp.Body)
   772  
   773  		if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE {
   774  			mlog.Info(fmt.Sprintf("Device was reported as removed for UserId=%v SessionId=%v removing push for this session", session.UserId, session.Id), mlog.String("user_id", session.UserId))
   775  			a.AttachDeviceId(session.Id, "", session.ExpiresAt)
   776  			a.ClearSessionCacheForUser(session.UserId)
   777  		}
   778  
   779  		if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL {
   780  			mlog.Error(fmt.Sprintf("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, pushResponse[model.PUSH_STATUS_ERROR_MSG]), mlog.String("user_id", session.UserId))
   781  		}
   782  	}
   783  }
   784  
   785  func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
   786  	if result := <-a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil {
   787  		return nil, result.Err
   788  	} else {
   789  		return result.Data.([]*model.Session), nil
   790  	}
   791  }
   792  
   793  func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, users []*model.User) *model.AppError {
   794  	if len(users) == 0 {
   795  		return nil
   796  	}
   797  
   798  	var usernames []string
   799  	for _, user := range users {
   800  		usernames = append(usernames, user.Username)
   801  	}
   802  	sort.Strings(usernames)
   803  
   804  	var userIds []string
   805  	for _, user := range users {
   806  		userIds = append(userIds, user.Id)
   807  	}
   808  
   809  	T := utils.GetUserTranslations(sender.Locale)
   810  
   811  	ephemeralPostId := model.NewId()
   812  	var message string
   813  	if len(users) == 1 {
   814  		message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
   815  			"Username": usernames[0],
   816  		})
   817  	} else {
   818  		message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
   819  			"Usernames":    strings.Join(usernames[:len(usernames)-1], ", @"),
   820  			"LastUsername": usernames[len(usernames)-1],
   821  		})
   822  	}
   823  
   824  	props := model.StringInterface{
   825  		model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{
   826  			"post_id":   ephemeralPostId,
   827  			"usernames": usernames,
   828  			"user_ids":  userIds,
   829  		},
   830  	}
   831  
   832  	a.SendEphemeralPost(
   833  		post.UserId,
   834  		&model.Post{
   835  			Id:        ephemeralPostId,
   836  			RootId:    post.RootId,
   837  			ChannelId: post.ChannelId,
   838  			Message:   message,
   839  			CreateAt:  post.CreateAt + 1,
   840  			Props:     props,
   841  		},
   842  	)
   843  
   844  	return nil
   845  }
   846  
   847  type ExplicitMentions struct {
   848  	// MentionedUserIds contains a key for each user mentioned by keyword.
   849  	MentionedUserIds map[string]bool
   850  
   851  	// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
   852  	// a corresponding keyword.
   853  	OtherPotentialMentions []string
   854  
   855  	// HereMentioned is true if the message contained @here.
   856  	HereMentioned bool
   857  
   858  	// AllMentioned is true if the message contained @all.
   859  	AllMentioned bool
   860  
   861  	// ChannelMentioned is true if the message contained @channel.
   862  	ChannelMentioned bool
   863  }
   864  
   865  // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
   866  // users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
   867  func GetExplicitMentions(message string, keywords map[string][]string) *ExplicitMentions {
   868  	ret := &ExplicitMentions{
   869  		MentionedUserIds: make(map[string]bool),
   870  	}
   871  	systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
   872  
   873  	addMentionedUsers := func(ids []string) {
   874  		for _, id := range ids {
   875  			ret.MentionedUserIds[id] = true
   876  		}
   877  	}
   878  	checkForMention := func(word string) bool {
   879  		isMention := false
   880  
   881  		if strings.ToLower(word) == "@here" {
   882  			ret.HereMentioned = true
   883  		}
   884  
   885  		if strings.ToLower(word) == "@channel" {
   886  			ret.ChannelMentioned = true
   887  		}
   888  
   889  		if strings.ToLower(word) == "@all" {
   890  			ret.AllMentioned = true
   891  		}
   892  
   893  		// Non-case-sensitive check for regular keys
   894  		if ids, match := keywords[strings.ToLower(word)]; match {
   895  			addMentionedUsers(ids)
   896  			isMention = true
   897  		}
   898  
   899  		// Case-sensitive check for first name
   900  		if ids, match := keywords[word]; match {
   901  			addMentionedUsers(ids)
   902  			isMention = true
   903  		}
   904  
   905  		return isMention
   906  	}
   907  	processText := func(text string) {
   908  		for _, word := range strings.FieldsFunc(text, func(c rune) bool {
   909  			// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
   910  			return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
   911  		}) {
   912  			// skip word with format ':word:' with an assumption that it is an emoji format only
   913  			if word[0] == ':' && word[len(word)-1] == ':' {
   914  				continue
   915  			}
   916  
   917  			if checkForMention(word) {
   918  				continue
   919  			}
   920  
   921  			// remove trailing '.', as that is the end of a sentence
   922  			foundWithSuffix := false
   923  			for _, suffixPunctuation := range []string{".", ":"} {
   924  				for strings.HasSuffix(word, suffixPunctuation) {
   925  					word = strings.TrimSuffix(word, suffixPunctuation)
   926  					if checkForMention(word) {
   927  						foundWithSuffix = true
   928  						break
   929  					}
   930  				}
   931  			}
   932  
   933  			if foundWithSuffix {
   934  				continue
   935  			}
   936  
   937  			if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
   938  				ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, word[1:])
   939  			} else if strings.ContainsAny(word, ".-:") {
   940  				// This word contains a character that may be the end of a sentence, so split further
   941  				splitWords := strings.FieldsFunc(word, func(c rune) bool {
   942  					return c == '.' || c == '-' || c == ':'
   943  				})
   944  
   945  				for _, splitWord := range splitWords {
   946  					if checkForMention(splitWord) {
   947  						continue
   948  					}
   949  					if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
   950  						ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, splitWord[1:])
   951  					}
   952  				}
   953  			}
   954  		}
   955  	}
   956  
   957  	buf := ""
   958  	markdown.Inspect(message, func(node interface{}) bool {
   959  		text, ok := node.(*markdown.Text)
   960  		if !ok {
   961  			processText(buf)
   962  			buf = ""
   963  			return true
   964  		}
   965  		buf += text.Text
   966  		return false
   967  	})
   968  	processText(buf)
   969  
   970  	return ret
   971  }
   972  
   973  // Given a map of user IDs to profiles, returns a list of mention
   974  // keywords for all users in the channel.
   975  func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool) map[string][]string {
   976  	keywords := make(map[string][]string)
   977  
   978  	for id, profile := range profiles {
   979  		userMention := "@" + strings.ToLower(profile.Username)
   980  		keywords[userMention] = append(keywords[userMention], id)
   981  
   982  		if len(profile.NotifyProps["mention_keys"]) > 0 {
   983  			// Add all the user's mention keys
   984  			splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
   985  			for _, k := range splitKeys {
   986  				// note that these are made lower case so that we can do a case insensitive check for them
   987  				key := strings.ToLower(k)
   988  				keywords[key] = append(keywords[key], id)
   989  			}
   990  		}
   991  
   992  		// If turned on, add the user's case sensitive first name
   993  		if profile.NotifyProps["first_name"] == "true" {
   994  			keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
   995  		}
   996  
   997  		// Add @channel and @all to keywords if user has them turned on
   998  		if lookForSpecialMentions {
   999  			if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" {
  1000  				keywords["@channel"] = append(keywords["@channel"], profile.Id)
  1001  				keywords["@all"] = append(keywords["@all"], profile.Id)
  1002  
  1003  				status := GetStatusFromCache(profile.Id)
  1004  				if status != nil && status.Status == model.STATUS_ONLINE {
  1005  					keywords["@here"] = append(keywords["@here"], profile.Id)
  1006  				}
  1007  			}
  1008  		}
  1009  	}
  1010  
  1011  	return keywords
  1012  }
  1013  
  1014  func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
  1015  	return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
  1016  		DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
  1017  }
  1018  
  1019  func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
  1020  	userNotifyProps := user.NotifyProps
  1021  	userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP]
  1022  	channelNotify, ok := channelNotifyProps[model.PUSH_NOTIFY_PROP]
  1023  
  1024  	// If the channel is muted do not send push notifications
  1025  	if channelMuted, ok := channelNotifyProps[model.MARK_UNREAD_NOTIFY_PROP]; ok {
  1026  		if channelMuted == model.CHANNEL_MARK_UNREAD_MENTION {
  1027  			return false
  1028  		}
  1029  	}
  1030  
  1031  	if post.IsSystemMessage() {
  1032  		return false
  1033  	}
  1034  
  1035  	if channelNotify == model.USER_NOTIFY_NONE {
  1036  		return false
  1037  	}
  1038  
  1039  	if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned {
  1040  		return false
  1041  	}
  1042  
  1043  	if userNotify == model.USER_NOTIFY_MENTION && (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) && !wasMentioned {
  1044  		return false
  1045  	}
  1046  
  1047  	if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) &&
  1048  		(post.UserId != user.Id || post.Props["from_webhook"] == "true") {
  1049  		return true
  1050  	}
  1051  
  1052  	if userNotify == model.USER_NOTIFY_NONE &&
  1053  		(!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) {
  1054  		return false
  1055  	}
  1056  
  1057  	return true
  1058  }
  1059  
  1060  func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool {
  1061  	// If User status is DND or OOO return false right away
  1062  	if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE {
  1063  		return false
  1064  	}
  1065  
  1066  	if pushStatus, ok := userNotifyProps["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) {
  1067  		return true
  1068  	} else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
  1069  		return true
  1070  	} else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
  1071  		return true
  1072  	}
  1073  
  1074  	return false
  1075  }