github.com/psyb0t/mattermost-server@v4.6.1-0.20180125161845-5503a1351abf+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 {
    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 pushServer == model.MHPNS && (!utils.IsLicensed() || !*utils.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 utils.IsLicensed() && *utils.License().Features.EmailNotificationContents {
   362  		emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
   363  	}
   364  
   365  	teamURL := utils.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"] = utils.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  	userLocale := utils.GetUserTranslations(user.Locale)
   570  
   571  	msg := model.PushNotification{}
   572  	if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
   573  		msg.Badge = 1
   574  		l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err)
   575  	} else {
   576  		msg.Badge = int(badge.Data.(int64))
   577  	}
   578  
   579  	msg.Type = model.PUSH_TYPE_MESSAGE
   580  	msg.TeamId = channel.TeamId
   581  	msg.ChannelId = channel.Id
   582  	msg.PostId = post.Id
   583  	msg.RootId = post.RootId
   584  	msg.ChannelName = channel.Name
   585  	msg.SenderId = post.UserId
   586  
   587  	if ou, ok := post.Props["override_username"]; ok && ou != nil {
   588  		msg.OverrideUsername = ou.(string)
   589  	}
   590  
   591  	if oi, ok := post.Props["override_icon_url"]; ok && oi != nil {
   592  		msg.OverrideIconUrl = oi.(string)
   593  	}
   594  
   595  	if fw, ok := post.Props["from_webhook"]; ok && fw != nil {
   596  		msg.FromWebhook = fw.(string)
   597  	}
   598  
   599  	if *a.Config().EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
   600  		msg.Category = model.CATEGORY_CAN_REPLY
   601  		if channel.Type == model.CHANNEL_DIRECT {
   602  			msg.Message = senderName + ": " + model.ClearMentionTags(post.Message)
   603  		} else {
   604  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message)
   605  		}
   606  	} else if *a.Config().EmailSettings.PushNotificationContents == model.GENERIC_NO_CHANNEL_NOTIFICATION {
   607  		if channel.Type == model.CHANNEL_DIRECT {
   608  			msg.Category = model.CATEGORY_CAN_REPLY
   609  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
   610  		} else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
   611  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
   612  		} else {
   613  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
   614  		}
   615  	} else {
   616  		if channel.Type == model.CHANNEL_DIRECT {
   617  			msg.Category = model.CATEGORY_CAN_REPLY
   618  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
   619  		} else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
   620  			msg.Category = model.CATEGORY_CAN_REPLY
   621  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
   622  		} else {
   623  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
   624  		}
   625  	}
   626  
   627  	// If the post only has images then push an appropriate message
   628  	if len(post.Message) == 0 && post.FileIds != nil && len(post.FileIds) > 0 {
   629  		if channel.Type == model.CHANNEL_DIRECT {
   630  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
   631  		} else {
   632  			msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
   633  		}
   634  	}
   635  
   636  	//l4g.Debug("Sending push notification for user %v with msg of '%v'", user.Id, msg.Message)
   637  
   638  	for _, session := range sessions {
   639  		tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   640  		tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   641  
   642  		l4g.Debug("Sending push notification to device %v for user %v with msg of '%v'", tmpMessage.DeviceId, user.Id, msg.Message)
   643  
   644  		a.Go(func(session *model.Session) func() {
   645  			return func() {
   646  				a.sendToPushProxy(tmpMessage, session)
   647  			}
   648  		}(session))
   649  
   650  		if a.Metrics != nil {
   651  			a.Metrics.IncrementPostSentPush()
   652  		}
   653  	}
   654  
   655  	return nil
   656  }
   657  
   658  func (a *App) ClearPushNotification(userId string, channelId string) {
   659  	a.Go(func() {
   660  		// Sleep is to allow the read replicas a chance to fully sync
   661  		// the unread count for sending an accurate count.
   662  		// Delaying a little doesn't hurt anything and is cheaper than
   663  		// attempting to read from master.
   664  		time.Sleep(time.Second * 5)
   665  
   666  		sessions, err := a.getMobileAppSessions(userId)
   667  		if err != nil {
   668  			l4g.Error(err.Error())
   669  			return
   670  		}
   671  
   672  		msg := model.PushNotification{}
   673  		msg.Type = model.PUSH_TYPE_CLEAR
   674  		msg.ChannelId = channelId
   675  		msg.ContentAvailable = 0
   676  		if badge := <-a.Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
   677  			msg.Badge = 0
   678  			l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err)
   679  		} else {
   680  			msg.Badge = int(badge.Data.(int64))
   681  		}
   682  
   683  		l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId)
   684  
   685  		for _, session := range sessions {
   686  			tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   687  			tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   688  			a.Go(func() {
   689  				a.sendToPushProxy(tmpMessage, session)
   690  			})
   691  		}
   692  	})
   693  }
   694  
   695  func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) {
   696  	msg.ServerId = a.DiagnosticId()
   697  
   698  	request, _ := http.NewRequest("POST", *a.Config().EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
   699  
   700  	if resp, err := a.HTTPClient(true).Do(request); err != nil {
   701  		l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error())
   702  	} else {
   703  		pushResponse := model.PushResponseFromJson(resp.Body)
   704  		if resp.Body != nil {
   705  			consumeAndClose(resp)
   706  		}
   707  
   708  		if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE {
   709  			l4g.Info("Device was reported as removed for UserId=%v SessionId=%v removing push for this session", session.UserId, session.Id)
   710  			a.AttachDeviceId(session.Id, "", session.ExpiresAt)
   711  			a.ClearSessionCacheForUser(session.UserId)
   712  		}
   713  
   714  		if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL {
   715  			l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, pushResponse[model.PUSH_STATUS_ERROR_MSG])
   716  		}
   717  	}
   718  }
   719  
   720  func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
   721  	if result := <-a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil {
   722  		return nil, result.Err
   723  	} else {
   724  		return result.Data.([]*model.Session), nil
   725  	}
   726  }
   727  
   728  func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, channelType string, users []*model.User) *model.AppError {
   729  	if len(users) == 0 {
   730  		return nil
   731  	}
   732  
   733  	var usernames []string
   734  	for _, user := range users {
   735  		usernames = append(usernames, user.Username)
   736  	}
   737  	sort.Strings(usernames)
   738  
   739  	var userIds []string
   740  	for _, user := range users {
   741  		userIds = append(userIds, user.Id)
   742  	}
   743  
   744  	T := utils.GetUserTranslations(sender.Locale)
   745  
   746  	var localePhrase string
   747  	if channelType == model.CHANNEL_OPEN {
   748  		localePhrase = T("api.post.check_for_out_of_channel_mentions.link.public")
   749  	} else if channelType == model.CHANNEL_PRIVATE {
   750  		localePhrase = T("api.post.check_for_out_of_channel_mentions.link.private")
   751  	}
   752  
   753  	ephemeralPostId := model.NewId()
   754  	var message string
   755  	if len(users) == 1 {
   756  		message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
   757  			"Username": usernames[0],
   758  			"Phrase":   localePhrase,
   759  		})
   760  	} else {
   761  		message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
   762  			"Usernames":    strings.Join(usernames[:len(usernames)-1], ", @"),
   763  			"LastUsername": usernames[len(usernames)-1],
   764  			"Phrase":       localePhrase,
   765  		})
   766  	}
   767  
   768  	props := model.StringInterface{
   769  		model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{
   770  			"post_id":   ephemeralPostId,
   771  			"usernames": usernames,
   772  			"user_ids":  userIds,
   773  		},
   774  	}
   775  
   776  	a.SendEphemeralPost(
   777  		post.UserId,
   778  		&model.Post{
   779  			Id:        ephemeralPostId,
   780  			RootId:    post.RootId,
   781  			ChannelId: post.ChannelId,
   782  			Message:   message,
   783  			CreateAt:  post.CreateAt + 1,
   784  			Props:     props,
   785  		},
   786  	)
   787  
   788  	return nil
   789  }
   790  
   791  type ExplicitMentions struct {
   792  	// MentionedUserIds contains a key for each user mentioned by keyword.
   793  	MentionedUserIds map[string]bool
   794  
   795  	// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
   796  	// a corresponding keyword.
   797  	OtherPotentialMentions []string
   798  
   799  	// HereMentioned is true if the message contained @here.
   800  	HereMentioned bool
   801  
   802  	// AllMentioned is true if the message contained @all.
   803  	AllMentioned bool
   804  
   805  	// ChannelMentioned is true if the message contained @channel.
   806  	ChannelMentioned bool
   807  }
   808  
   809  // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
   810  // users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
   811  func GetExplicitMentions(message string, keywords map[string][]string) *ExplicitMentions {
   812  	ret := &ExplicitMentions{
   813  		MentionedUserIds: make(map[string]bool),
   814  	}
   815  	systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
   816  
   817  	addMentionedUsers := func(ids []string) {
   818  		for _, id := range ids {
   819  			ret.MentionedUserIds[id] = true
   820  		}
   821  	}
   822  
   823  	processText := func(text string) {
   824  		for _, word := range strings.FieldsFunc(text, func(c rune) bool {
   825  			// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
   826  			return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
   827  		}) {
   828  			isMention := false
   829  
   830  			// skip word with format ':word:' with an assumption that it is an emoji format only
   831  			if word[0] == ':' && word[len(word)-1] == ':' {
   832  				continue
   833  			}
   834  
   835  			if word == "@here" {
   836  				ret.HereMentioned = true
   837  			}
   838  
   839  			if word == "@channel" {
   840  				ret.ChannelMentioned = true
   841  			}
   842  
   843  			if word == "@all" {
   844  				ret.AllMentioned = true
   845  			}
   846  
   847  			// Non-case-sensitive check for regular keys
   848  			if ids, match := keywords[strings.ToLower(word)]; match {
   849  				addMentionedUsers(ids)
   850  				isMention = true
   851  			}
   852  
   853  			// Case-sensitive check for first name
   854  			if ids, match := keywords[word]; match {
   855  				addMentionedUsers(ids)
   856  				isMention = true
   857  			}
   858  
   859  			if isMention {
   860  				continue
   861  			}
   862  
   863  			if strings.ContainsAny(word, ".-:") {
   864  				// This word contains a character that may be the end of a sentence, so split further
   865  				splitWords := strings.FieldsFunc(word, func(c rune) bool {
   866  					return c == '.' || c == '-' || c == ':'
   867  				})
   868  
   869  				for _, splitWord := range splitWords {
   870  					if splitWord == "@here" {
   871  						ret.HereMentioned = true
   872  					}
   873  
   874  					if splitWord == "@all" {
   875  						ret.AllMentioned = true
   876  					}
   877  
   878  					if splitWord == "@channel" {
   879  						ret.ChannelMentioned = true
   880  					}
   881  
   882  					// Non-case-sensitive check for regular keys
   883  					if ids, match := keywords[strings.ToLower(splitWord)]; match {
   884  						addMentionedUsers(ids)
   885  					}
   886  
   887  					// Case-sensitive check for first name
   888  					if ids, match := keywords[splitWord]; match {
   889  						addMentionedUsers(ids)
   890  					} else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
   891  						username := splitWord[1:]
   892  						ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username)
   893  					}
   894  				}
   895  			}
   896  
   897  			if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
   898  				username := word[1:]
   899  				ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username)
   900  			}
   901  		}
   902  	}
   903  
   904  	buf := ""
   905  	markdown.Inspect(message, func(node interface{}) bool {
   906  		text, ok := node.(*markdown.Text)
   907  		if !ok {
   908  			processText(buf)
   909  			buf = ""
   910  			return true
   911  		}
   912  		buf += text.Text
   913  		return false
   914  	})
   915  	processText(buf)
   916  
   917  	return ret
   918  }
   919  
   920  // Given a map of user IDs to profiles, returns a list of mention
   921  // keywords for all users in the channel.
   922  func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool) map[string][]string {
   923  	keywords := make(map[string][]string)
   924  
   925  	for id, profile := range profiles {
   926  		userMention := "@" + strings.ToLower(profile.Username)
   927  		keywords[userMention] = append(keywords[userMention], id)
   928  
   929  		if len(profile.NotifyProps["mention_keys"]) > 0 {
   930  			// Add all the user's mention keys
   931  			splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
   932  			for _, k := range splitKeys {
   933  				// note that these are made lower case so that we can do a case insensitive check for them
   934  				key := strings.ToLower(k)
   935  				keywords[key] = append(keywords[key], id)
   936  			}
   937  		}
   938  
   939  		// If turned on, add the user's case sensitive first name
   940  		if profile.NotifyProps["first_name"] == "true" {
   941  			keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
   942  		}
   943  
   944  		// Add @channel and @all to keywords if user has them turned on
   945  		if lookForSpecialMentions {
   946  			if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" {
   947  				keywords["@channel"] = append(keywords["@channel"], profile.Id)
   948  				keywords["@all"] = append(keywords["@all"], profile.Id)
   949  
   950  				status := GetStatusFromCache(profile.Id)
   951  				if status != nil && status.Status == model.STATUS_ONLINE {
   952  					keywords["@here"] = append(keywords["@here"], profile.Id)
   953  				}
   954  			}
   955  		}
   956  	}
   957  
   958  	return keywords
   959  }
   960  
   961  func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
   962  	return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
   963  		DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
   964  }
   965  
   966  func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
   967  	userNotifyProps := user.NotifyProps
   968  	userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP]
   969  	channelNotify, ok := channelNotifyProps[model.PUSH_NOTIFY_PROP]
   970  
   971  	if post.IsSystemMessage() {
   972  		return false
   973  	}
   974  
   975  	if channelNotify == model.USER_NOTIFY_NONE {
   976  		return false
   977  	}
   978  
   979  	if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned {
   980  		return false
   981  	}
   982  
   983  	if userNotify == model.USER_NOTIFY_MENTION && (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) && !wasMentioned {
   984  		return false
   985  	}
   986  
   987  	if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) &&
   988  		(post.UserId != user.Id || post.Props["from_webhook"] == "true") {
   989  		return true
   990  	}
   991  
   992  	if userNotify == model.USER_NOTIFY_NONE &&
   993  		(!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) {
   994  		return false
   995  	}
   996  
   997  	return true
   998  }
   999  
  1000  func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool {
  1001  	// If User status is DND return false right away
  1002  	if status.Status == model.STATUS_DND {
  1003  		return false
  1004  	}
  1005  
  1006  	if pushStatus, ok := userNotifyProps["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) {
  1007  		return true
  1008  	} else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
  1009  		return true
  1010  	} else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
  1011  		return true
  1012  	}
  1013  
  1014  	return false
  1015  }