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