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