github.com/ashishbhate/mattermost-server@v5.11.1+incompatible/app/notification_push.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  	"hash/fnv"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"github.com/mattermost/mattermost-server/mlog"
    13  	"github.com/mattermost/mattermost-server/model"
    14  	"github.com/mattermost/mattermost-server/utils"
    15  	"github.com/nicksnyder/go-i18n/i18n"
    16  )
    17  
    18  type NotificationType string
    19  
    20  const NOTIFICATION_TYPE_CLEAR NotificationType = "clear"
    21  const NOTIFICATION_TYPE_MESSAGE NotificationType = "message"
    22  
    23  const PUSH_NOTIFICATION_HUB_WORKERS = 1000
    24  const PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER = 50
    25  
    26  type PushNotificationsHub struct {
    27  	Channels []chan PushNotification
    28  }
    29  
    30  type PushNotification struct {
    31  	notificationType   NotificationType
    32  	currentSessionId   string
    33  	userId             string
    34  	channelId          string
    35  	post               *model.Post
    36  	user               *model.User
    37  	channel            *model.Channel
    38  	senderName         string
    39  	channelName        string
    40  	explicitMention    bool
    41  	channelWideMention bool
    42  	replyToThreadType  string
    43  }
    44  
    45  func (hub *PushNotificationsHub) GetGoChannelFromUserId(userId string) chan PushNotification {
    46  	h := fnv.New32a()
    47  	h.Write([]byte(userId))
    48  	chanIdx := h.Sum32() % PUSH_NOTIFICATION_HUB_WORKERS
    49  	return hub.Channels[chanIdx]
    50  }
    51  
    52  func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
    53  	explicitMention, channelWideMention bool, replyToThreadType string) *model.AppError {
    54  	cfg := a.Config()
    55  
    56  	sessions, err := a.getMobileAppSessions(user.Id)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	msg := model.PushNotification{}
    62  	if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
    63  		msg.Badge = 1
    64  		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))
    65  	} else {
    66  		msg.Badge = int(badge.Data.(int64))
    67  	}
    68  
    69  	msg.Category = model.CATEGORY_CAN_REPLY
    70  	msg.Version = model.PUSH_MESSAGE_V2
    71  	msg.Type = model.PUSH_TYPE_MESSAGE
    72  	msg.TeamId = channel.TeamId
    73  	msg.ChannelId = channel.Id
    74  	msg.PostId = post.Id
    75  	msg.RootId = post.RootId
    76  	msg.SenderId = post.UserId
    77  
    78  	contentsConfig := *cfg.EmailSettings.PushNotificationContents
    79  	if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT {
    80  		msg.ChannelName = channelName
    81  	}
    82  
    83  	if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride {
    84  		msg.OverrideUsername = ou
    85  	}
    86  
    87  	if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride {
    88  		msg.OverrideIconUrl = oi
    89  	}
    90  
    91  	if fw, ok := post.Props["from_webhook"].(string); ok {
    92  		msg.FromWebhook = fw
    93  	}
    94  
    95  	userLocale := utils.GetUserTranslations(user.Locale)
    96  	hasFiles := post.FileIds != nil && len(post.FileIds) > 0
    97  
    98  	msg.Message = a.getPushNotificationMessage(post.Message, explicitMention, channelWideMention, hasFiles, senderName, channelName, channel.Type, replyToThreadType, userLocale)
    99  
   100  	for _, session := range sessions {
   101  
   102  		if session.IsExpired() {
   103  			continue
   104  		}
   105  
   106  		tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   107  		tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   108  
   109  		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))
   110  
   111  		a.sendToPushProxy(tmpMessage, session)
   112  
   113  		if a.Metrics != nil {
   114  			a.Metrics.IncrementPostSentPush()
   115  		}
   116  	}
   117  
   118  	return nil
   119  }
   120  
   121  func (a *App) sendPushNotification(notification *postNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) {
   122  	cfg := a.Config()
   123  	channel := notification.channel
   124  	post := notification.post
   125  
   126  	var nameFormat string
   127  	if result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT); result.Err != nil {
   128  		nameFormat = *a.Config().TeamSettings.TeammateNameDisplay
   129  	} else {
   130  		nameFormat = result.Data.(model.Preference).Value
   131  	}
   132  
   133  	channelName := notification.GetChannelName(nameFormat, user.Id)
   134  	senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride)
   135  
   136  	c := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(user.Id)
   137  	c <- PushNotification{
   138  		notificationType:   NOTIFICATION_TYPE_MESSAGE,
   139  		post:               post,
   140  		user:               user,
   141  		channel:            channel,
   142  		senderName:         senderName,
   143  		channelName:        channelName,
   144  		explicitMention:    explicitMention,
   145  		channelWideMention: channelWideMention,
   146  		replyToThreadType:  replyToThreadType,
   147  	}
   148  }
   149  
   150  func (a *App) getPushNotificationMessage(postMessage string, explicitMention, channelWideMention, hasFiles bool,
   151  	senderName, channelName, channelType, replyToThreadType string, userLocale i18n.TranslateFunc) string {
   152  
   153  	// If the post only has images then push an appropriate message
   154  	if len(postMessage) == 0 && hasFiles {
   155  		if channelType == model.CHANNEL_DIRECT {
   156  			return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ")
   157  		}
   158  		return "@" + senderName + userLocale("api.post.send_notifications_and_forget.push_image_only")
   159  	}
   160  
   161  	contentsConfig := *a.Config().EmailSettings.PushNotificationContents
   162  
   163  	if contentsConfig == model.FULL_NOTIFICATION {
   164  		if channelType == model.CHANNEL_DIRECT {
   165  			return model.ClearMentionTags(postMessage)
   166  		}
   167  		return "@" + senderName + ": " + model.ClearMentionTags(postMessage)
   168  	}
   169  
   170  	if channelType == model.CHANNEL_DIRECT {
   171  		return userLocale("api.post.send_notifications_and_forget.push_message")
   172  	}
   173  
   174  	if channelWideMention {
   175  		return "@" + senderName + userLocale("api.post.send_notification_and_forget.push_channel_mention")
   176  	}
   177  
   178  	if explicitMention {
   179  		return "@" + senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention")
   180  	}
   181  
   182  	if replyToThreadType == THREAD_ROOT {
   183  		return "@" + senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post")
   184  	}
   185  
   186  	if replyToThreadType == THREAD_ANY {
   187  		return "@" + senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread")
   188  	}
   189  
   190  	return "@" + senderName + userLocale("api.post.send_notifications_and_forget.push_general_message")
   191  }
   192  
   193  func (a *App) ClearPushNotificationSync(currentSessionId, userId, channelId string) {
   194  	sessions, err := a.getMobileAppSessions(userId)
   195  	if err != nil {
   196  		mlog.Error(err.Error())
   197  		return
   198  	}
   199  
   200  	msg := model.PushNotification{}
   201  	msg.Type = model.PUSH_TYPE_CLEAR
   202  	msg.ChannelId = channelId
   203  	msg.ContentAvailable = 0
   204  	if badge := <-a.Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
   205  		msg.Badge = 0
   206  		mlog.Error(fmt.Sprint("We could not get the unread message count for the user", userId, badge.Err), mlog.String("user_id", userId))
   207  	} else {
   208  		msg.Badge = int(badge.Data.(int64))
   209  	}
   210  
   211  	for _, session := range sessions {
   212  		if currentSessionId != session.Id {
   213  			tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
   214  			tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   215  			mlog.Debug(fmt.Sprintf("Clearing push notification to %v with channel_id %v", session.DeviceId, msg.ChannelId))
   216  			a.sendToPushProxy(tmpMessage, session)
   217  		}
   218  	}
   219  }
   220  
   221  func (a *App) ClearPushNotification(currentSessionId, userId, channelId string) {
   222  	channel := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(userId)
   223  	channel <- PushNotification{
   224  		notificationType: NOTIFICATION_TYPE_CLEAR,
   225  		currentSessionId: currentSessionId,
   226  		userId:           userId,
   227  		channelId:        channelId,
   228  	}
   229  }
   230  
   231  func (a *App) CreatePushNotificationsHub() {
   232  	hub := PushNotificationsHub{
   233  		Channels: []chan PushNotification{},
   234  	}
   235  	for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ {
   236  		hub.Channels = append(hub.Channels, make(chan PushNotification, PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER))
   237  	}
   238  	a.Srv.PushNotificationsHub = hub
   239  }
   240  
   241  func (a *App) pushNotificationWorker(notifications chan PushNotification) {
   242  	for notification := range notifications {
   243  		switch notification.notificationType {
   244  		case NOTIFICATION_TYPE_CLEAR:
   245  			a.ClearPushNotificationSync(notification.currentSessionId, notification.userId, notification.channelId)
   246  		case NOTIFICATION_TYPE_MESSAGE:
   247  			a.sendPushNotificationSync(
   248  				notification.post,
   249  				notification.user,
   250  				notification.channel,
   251  				notification.channelName,
   252  				notification.senderName,
   253  				notification.explicitMention,
   254  				notification.channelWideMention,
   255  				notification.replyToThreadType,
   256  			)
   257  		default:
   258  			mlog.Error(fmt.Sprintf("Invalid notification type %v", notification.notificationType))
   259  		}
   260  	}
   261  }
   262  
   263  func (a *App) StartPushNotificationsHubWorkers() {
   264  	for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ {
   265  		channel := a.Srv.PushNotificationsHub.Channels[x]
   266  		a.Srv.Go(func() { a.pushNotificationWorker(channel) })
   267  	}
   268  }
   269  
   270  func (a *App) StopPushNotificationsHubWorkers() {
   271  	for _, channel := range a.Srv.PushNotificationsHub.Channels {
   272  		close(channel)
   273  	}
   274  }
   275  
   276  func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) {
   277  	msg.ServerId = a.DiagnosticId()
   278  
   279  	request, err := http.NewRequest("POST", strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
   280  	if err != nil {
   281  		mlog.Error(fmt.Sprintf("Error sending to push proxy: UserId=%v SessionId=%v message=%v",
   282  			session.UserId, session.Id, err.Error()), mlog.String("user_id", session.UserId))
   283  		return
   284  	}
   285  
   286  	resp, err := a.HTTPService.MakeClient(true).Do(request)
   287  	if err != nil {
   288  		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))
   289  		return
   290  	}
   291  
   292  	defer resp.Body.Close()
   293  
   294  	pushResponse := model.PushResponseFromJson(resp.Body)
   295  
   296  	if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE {
   297  		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))
   298  		a.AttachDeviceId(session.Id, "", session.ExpiresAt)
   299  		a.ClearSessionCacheForUser(session.UserId)
   300  	}
   301  
   302  	if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL {
   303  		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))
   304  	}
   305  }
   306  
   307  func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
   308  	result := <-a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId)
   309  	if result.Err != nil {
   310  		return nil, result.Err
   311  	}
   312  	return result.Data.([]*model.Session), nil
   313  }
   314  
   315  func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
   316  	return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
   317  		DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
   318  }
   319  
   320  func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
   321  	userNotifyProps := user.NotifyProps
   322  	userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP]
   323  	channelNotify, _ := channelNotifyProps[model.PUSH_NOTIFY_PROP]
   324  	if channelNotify == "" {
   325  		channelNotify = model.CHANNEL_NOTIFY_DEFAULT
   326  	}
   327  
   328  	// If the channel is muted do not send push notifications
   329  	if channelNotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_MARK_UNREAD_MENTION {
   330  		return false
   331  	}
   332  
   333  	if post.IsSystemMessage() {
   334  		return false
   335  	}
   336  
   337  	if channelNotify == model.USER_NOTIFY_NONE {
   338  		return false
   339  	}
   340  
   341  	if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned {
   342  		return false
   343  	}
   344  
   345  	if userNotify == model.USER_NOTIFY_MENTION && channelNotify == model.CHANNEL_NOTIFY_DEFAULT && !wasMentioned {
   346  		return false
   347  	}
   348  
   349  	if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) &&
   350  		(post.UserId != user.Id || post.Props["from_webhook"] == "true") {
   351  		return true
   352  	}
   353  
   354  	if userNotify == model.USER_NOTIFY_NONE &&
   355  		channelNotify == model.CHANNEL_NOTIFY_DEFAULT {
   356  		return false
   357  	}
   358  
   359  	return true
   360  }
   361  
   362  func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool {
   363  	// If User status is DND or OOO return false right away
   364  	if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE {
   365  		return false
   366  	}
   367  
   368  	pushStatus, ok := userNotifyProps[model.PUSH_STATUS_NOTIFY_PROP]
   369  	if (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) {
   370  		return true
   371  	}
   372  
   373  	if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
   374  		return true
   375  	}
   376  
   377  	if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
   378  		return true
   379  	}
   380  
   381  	return false
   382  }