github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/app/notification_push.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"hash/fnv"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/mattermost/go-i18n/i18n"
    14  	"github.com/vnforks/kid/v5/mlog"
    15  	"github.com/vnforks/kid/v5/model"
    16  	"github.com/vnforks/kid/v5/utils"
    17  )
    18  
    19  type notificationType string
    20  
    21  const (
    22  	notificationTypeClear       notificationType = "clear"
    23  	notificationTypeMessage     notificationType = "message"
    24  	notificationTypeUpdateBadge notificationType = "update_badge"
    25  )
    26  
    27  const PUSH_NOTIFICATION_HUB_WORKERS = 1000
    28  const PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER = 50
    29  
    30  type PushNotificationsHub struct {
    31  	Classes []chan PushNotification
    32  }
    33  
    34  type PushNotification struct {
    35  	notificationType  notificationType
    36  	currentSessionId  string
    37  	userId            string
    38  	classId           string
    39  	post              *model.Post
    40  	user              *model.User
    41  	class             *model.Class
    42  	senderName        string
    43  	className         string
    44  	explicitMention   bool
    45  	classWideMention  bool
    46  	replyToThreadType string
    47  }
    48  
    49  func (hub *PushNotificationsHub) GetGoClassFromUserId(userId string) chan PushNotification {
    50  	h := fnv.New32a()
    51  	h.Write([]byte(userId))
    52  	chanIdx := h.Sum32() % PUSH_NOTIFICATION_HUB_WORKERS
    53  	return hub.Classes[chanIdx]
    54  }
    55  
    56  func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, class *model.Class, className string, senderName string,
    57  	explicitMention bool, classWideMention bool, replyToThreadType string) *model.AppError {
    58  	cfg := a.Config()
    59  	msg, err := a.BuildPushNotificationMessage(
    60  		*cfg.EmailSettings.PushNotificationContents,
    61  		post,
    62  		user,
    63  		class,
    64  		className,
    65  		senderName,
    66  		explicitMention,
    67  		classWideMention,
    68  		replyToThreadType,
    69  	)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	return a.sendPushNotificationToAllSessions(msg, user.Id, "")
    75  }
    76  
    77  func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, userId string, skipSessionId string) *model.AppError {
    78  	sessions, err := a.getMobileAppSessions(userId)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	if msg == nil {
    84  		return model.NewAppError(
    85  			"pushNotification",
    86  			"api.push_notifications.message.parse.app_error",
    87  			nil,
    88  			"",
    89  			http.StatusBadRequest,
    90  		)
    91  	}
    92  
    93  	notification, parseError := model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
    94  	if parseError != nil {
    95  		return model.NewAppError(
    96  			"pushNotification",
    97  			"api.push_notifications.message.parse.app_error",
    98  			nil,
    99  			parseError.Error(),
   100  			http.StatusInternalServerError,
   101  		)
   102  	}
   103  
   104  	for _, session := range sessions {
   105  		// Don't send notifications to this session if it's expired or we want to skip it
   106  		if session.IsExpired() || (skipSessionId != "" && skipSessionId == session.Id) {
   107  			continue
   108  		}
   109  
   110  		// We made a copy to avoid decoding and parsing all the time
   111  		tmpMessage := notification
   112  		tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
   113  		tmpMessage.AckId = model.NewId()
   114  
   115  		err := a.sendToPushProxy(*tmpMessage, session)
   116  		if err != nil {
   117  			a.NotificationsLog().Error("Notification error",
   118  				mlog.String("ackId", tmpMessage.AckId),
   119  				mlog.String("type", tmpMessage.Type),
   120  				mlog.String("userId", session.UserId),
   121  				mlog.String("postId", tmpMessage.PostId),
   122  				mlog.String("classId", tmpMessage.ClassId),
   123  				mlog.String("deviceId", tmpMessage.DeviceId),
   124  				mlog.String("status", err.Error()),
   125  			)
   126  
   127  			continue
   128  		}
   129  
   130  		a.NotificationsLog().Info("Notification sent",
   131  			mlog.String("ackId", tmpMessage.AckId),
   132  			mlog.String("type", tmpMessage.Type),
   133  			mlog.String("userId", session.UserId),
   134  			mlog.String("postId", tmpMessage.PostId),
   135  			mlog.String("classId", tmpMessage.ClassId),
   136  			mlog.String("deviceId", tmpMessage.DeviceId),
   137  			mlog.String("status", model.PUSH_SEND_SUCCESS),
   138  		)
   139  
   140  		if a.Metrics() != nil {
   141  			a.Metrics().IncrementPostSentPush()
   142  		}
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, classWideMention bool, replyToThreadType string) {
   149  	cfg := a.Config()
   150  	class := notification.Class
   151  	post := notification.Post
   152  
   153  	nameFormat := a.GetNotificationNameFormat(user)
   154  
   155  	className := notification.GetClassName(nameFormat, user.Id)
   156  	senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride)
   157  
   158  	c := a.Srv().PushNotificationsHub.GetGoClassFromUserId(user.Id)
   159  	c <- PushNotification{
   160  		notificationType:  notificationTypeMessage,
   161  		post:              post,
   162  		user:              user,
   163  		class:             class,
   164  		senderName:        senderName,
   165  		className:         className,
   166  		explicitMention:   explicitMention,
   167  		classWideMention:  classWideMention,
   168  		replyToThreadType: replyToThreadType,
   169  	}
   170  }
   171  
   172  func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, explicitMention, classWideMention, hasFiles bool,
   173  	senderName, className, replyToThreadType string, userLocale i18n.TranslateFunc) string {
   174  
   175  	// If the post only has images then push an appropriate message
   176  	if len(postMessage) == 0 && hasFiles {
   177  		return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only")
   178  	}
   179  
   180  	if contentsConfig == model.FULL_NOTIFICATION {
   181  		return senderName + ": " + model.ClearMentionTags(postMessage)
   182  	}
   183  
   184  	if classWideMention {
   185  		return senderName + userLocale("api.post.send_notification_and_forget.push_class_mention")
   186  	}
   187  
   188  	if explicitMention {
   189  		return senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention")
   190  	}
   191  
   192  	if replyToThreadType == model.COMMENTS_NOTIFY_ROOT {
   193  		return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post")
   194  	}
   195  
   196  	if replyToThreadType == model.COMMENTS_NOTIFY_ANY {
   197  		return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread")
   198  	}
   199  
   200  	return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message")
   201  }
   202  
   203  func (a *App) clearPushNotificationSync(currentSessionId, userId, classId string) *model.AppError {
   204  	msg := &model.PushNotification{
   205  		Type:             model.PUSH_TYPE_CLEAR,
   206  		Version:          model.PUSH_MESSAGE_V2,
   207  		ClassId:          classId,
   208  		ContentAvailable: 1,
   209  	}
   210  
   211  	unreadCount, err := a.Srv().Store.User().GetUnreadCount(userId)
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	msg.Badge = int(unreadCount)
   217  
   218  	return a.sendPushNotificationToAllSessions(msg, userId, currentSessionId)
   219  }
   220  
   221  func (a *App) clearPushNotification(currentSessionId, userId, classId string) {
   222  	class := a.Srv().PushNotificationsHub.GetGoClassFromUserId(userId)
   223  	class <- PushNotification{
   224  		notificationType: notificationTypeClear,
   225  		currentSessionId: currentSessionId,
   226  		userId:           userId,
   227  		classId:          classId,
   228  	}
   229  }
   230  
   231  func (a *App) updateMobileAppBadgeSync(userId string) *model.AppError {
   232  	msg := &model.PushNotification{
   233  		Type:             model.PUSH_TYPE_UPDATE_BADGE,
   234  		Version:          model.PUSH_MESSAGE_V2,
   235  		Sound:            "none",
   236  		ContentAvailable: 1,
   237  	}
   238  
   239  	unreadCount, err := a.Srv().Store.User().GetUnreadCount(userId)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	msg.Badge = int(unreadCount)
   245  
   246  	return a.sendPushNotificationToAllSessions(msg, userId, "")
   247  }
   248  
   249  func (a *App) UpdateMobileAppBadge(userId string) {
   250  	class := a.Srv().PushNotificationsHub.GetGoClassFromUserId(userId)
   251  	class <- PushNotification{
   252  		notificationType: notificationTypeUpdateBadge,
   253  		userId:           userId,
   254  	}
   255  }
   256  
   257  func (a *App) createPushNotificationsHub() {
   258  	hub := PushNotificationsHub{
   259  		Classes: []chan PushNotification{},
   260  	}
   261  	for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ {
   262  		hub.Classes = append(hub.Classes, make(chan PushNotification, PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER))
   263  	}
   264  	a.Srv().PushNotificationsHub = hub
   265  }
   266  
   267  func (a *App) pushNotificationWorker(notifications chan PushNotification) {
   268  	for notification := range notifications {
   269  		var err *model.AppError
   270  		switch notification.notificationType {
   271  		case notificationTypeClear:
   272  			err = a.clearPushNotificationSync(notification.currentSessionId, notification.userId, notification.classId)
   273  		case notificationTypeMessage:
   274  			err = a.sendPushNotificationSync(
   275  				notification.post,
   276  				notification.user,
   277  				notification.class,
   278  				notification.className,
   279  				notification.senderName,
   280  				notification.explicitMention,
   281  				notification.classWideMention,
   282  				notification.replyToThreadType,
   283  			)
   284  		case notificationTypeUpdateBadge:
   285  			err = a.updateMobileAppBadgeSync(notification.userId)
   286  		default:
   287  			mlog.Error("Invalid notification type", mlog.String("notification_type", string(notification.notificationType)))
   288  		}
   289  
   290  		if err != nil {
   291  			mlog.Error("Unable to send push notification", mlog.String("notification_type", string(notification.notificationType)), mlog.Err(err))
   292  		}
   293  	}
   294  }
   295  
   296  func (a *App) StartPushNotificationsHubWorkers() {
   297  	for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ {
   298  		class := a.Srv().PushNotificationsHub.Classes[x]
   299  		a.Srv().Go(func() { a.pushNotificationWorker(class) })
   300  	}
   301  }
   302  
   303  func (a *App) StopPushNotificationsHubWorkers() {
   304  	for _, class := range a.Srv().PushNotificationsHub.Classes {
   305  		close(class)
   306  	}
   307  }
   308  
   309  func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) error {
   310  	msg.ServerId = a.DiagnosticId()
   311  
   312  	a.NotificationsLog().Info("Notification will be sent",
   313  		mlog.String("ackId", msg.AckId),
   314  		mlog.String("type", msg.Type),
   315  		mlog.String("userId", session.UserId),
   316  		mlog.String("postId", msg.PostId),
   317  		mlog.String("status", model.PUSH_SEND_PREPARE),
   318  	)
   319  
   320  	url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.API_URL_SUFFIX_V1 + "/send_push"
   321  	request, err := http.NewRequest("POST", url, strings.NewReader(msg.ToJson()))
   322  	if err != nil {
   323  		return err
   324  	}
   325  
   326  	resp, err := a.Srv().pushNotificationClient.Do(request)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	defer resp.Body.Close()
   331  
   332  	pushResponse := model.PushResponseFromJson(resp.Body)
   333  
   334  	switch pushResponse[model.PUSH_STATUS] {
   335  	case model.PUSH_STATUS_REMOVE:
   336  		a.AttachDeviceId(session.Id, "", session.ExpiresAt)
   337  		a.ClearSessionCacheForUser(session.UserId)
   338  		return errors.New("Device was reported as removed")
   339  	case model.PUSH_STATUS_FAIL:
   340  		return errors.New(pushResponse[model.PUSH_STATUS_ERROR_MSG])
   341  	}
   342  	return nil
   343  }
   344  
   345  func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error {
   346  	if ack == nil {
   347  		return nil
   348  	}
   349  
   350  	a.NotificationsLog().Info("Notification received",
   351  		mlog.String("ackId", ack.Id),
   352  		mlog.String("type", ack.NotificationType),
   353  		mlog.String("deviceType", ack.ClientPlatform),
   354  		mlog.Int64("receivedAt", ack.ClientReceivedAt),
   355  		mlog.String("status", model.PUSH_RECEIVED),
   356  	)
   357  
   358  	request, err := http.NewRequest(
   359  		"POST",
   360  		strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/ack",
   361  		strings.NewReader(ack.ToJson()),
   362  	)
   363  
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	resp, err := a.HTTPService().MakeClient(true).Do(request)
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	resp.Body.Close()
   374  	return nil
   375  
   376  }
   377  
   378  func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
   379  	return a.Srv().Store.Session().GetSessionsWithActiveDeviceIds(userId)
   380  }
   381  
   382  func ShouldSendPushNotification(user *model.User, classNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
   383  	return DoesNotifyPropsAllowPushNotification(user, classNotifyProps, post, wasMentioned) &&
   384  		DoesStatusAllowPushNotification(user.NotifyProps, status, post.ClassId)
   385  }
   386  
   387  func DoesNotifyPropsAllowPushNotification(user *model.User, classNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
   388  	userNotifyProps := user.NotifyProps
   389  	userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP]
   390  	classNotify, ok := classNotifyProps[model.PUSH_NOTIFY_PROP]
   391  	if !ok || classNotify == "" {
   392  		classNotify = model.CLASS_NOTIFY_DEFAULT
   393  	}
   394  
   395  	// If the class is muted do not send push notifications
   396  	if classNotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CLASS_MARK_UNREAD_MENTION {
   397  		return false
   398  	}
   399  
   400  	if post.IsSystemMessage() {
   401  		return false
   402  	}
   403  
   404  	if classNotify == model.USER_NOTIFY_NONE {
   405  		return false
   406  	}
   407  
   408  	if classNotify == model.CLASS_NOTIFY_MENTION && !wasMentioned {
   409  		return false
   410  	}
   411  
   412  	if userNotify == model.USER_NOTIFY_MENTION && classNotify == model.CLASS_NOTIFY_DEFAULT && !wasMentioned {
   413  		return false
   414  	}
   415  
   416  	if (userNotify == model.USER_NOTIFY_ALL || classNotify == model.CLASS_NOTIFY_ALL) &&
   417  		(post.UserId != user.Id || post.GetProp("from_webhook") == "true") {
   418  		return true
   419  	}
   420  
   421  	if userNotify == model.USER_NOTIFY_NONE &&
   422  		classNotify == model.CLASS_NOTIFY_DEFAULT {
   423  		return false
   424  	}
   425  
   426  	return true
   427  }
   428  
   429  func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, classId string) bool {
   430  	// If User status is DND or OOO return false right away
   431  	if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE {
   432  		return false
   433  	}
   434  
   435  	pushStatus, ok := userNotifyProps[model.PUSH_STATUS_NOTIFY_PROP]
   436  	if (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveClass != classId || model.GetMillis()-status.LastActivityAt > model.STATUS_CLASS_TIMEOUT) {
   437  		return true
   438  	}
   439  
   440  	if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
   441  		return true
   442  	}
   443  
   444  	if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
   445  		return true
   446  	}
   447  
   448  	return false
   449  }
   450  
   451  func (a *App) BuildPushNotificationMessage(contentsConfig string, post *model.Post, user *model.User, class *model.Class, className string, senderName string,
   452  	explicitMention bool, classWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
   453  
   454  	var msg *model.PushNotification
   455  
   456  	notificationInterface := a.Srv().Notification
   457  	if (notificationInterface == nil || notificationInterface.CheckLicense() != nil) && contentsConfig == model.ID_LOADED_NOTIFICATION {
   458  		contentsConfig = model.GENERIC_NOTIFICATION
   459  	}
   460  
   461  	if contentsConfig == model.ID_LOADED_NOTIFICATION {
   462  		msg = a.buildIdLoadedPushNotificationMessage(post, user)
   463  	} else {
   464  		msg = a.buildFullPushNotificationMessage(contentsConfig, post, user, class, className, senderName, explicitMention, classWideMention, replyToThreadType)
   465  	}
   466  
   467  	unreadCount, err := a.Srv().Store.User().GetUnreadCount(user.Id)
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  	msg.Badge = int(unreadCount)
   472  
   473  	return msg, nil
   474  }
   475  
   476  func (a *App) buildIdLoadedPushNotificationMessage(post *model.Post, user *model.User) *model.PushNotification {
   477  	userLocale := utils.GetUserTranslations(user.Locale)
   478  	msg := &model.PushNotification{
   479  		PostId:     post.Id,
   480  		ClassId:    post.ClassId,
   481  		Category:   model.CATEGORY_CAN_REPLY,
   482  		Version:    model.PUSH_MESSAGE_V2,
   483  		Type:       model.PUSH_TYPE_MESSAGE,
   484  		IsIdLoaded: true,
   485  		SenderId:   user.Id,
   486  		Message:    userLocale("api.push_notification.id_loaded.default_message"),
   487  	}
   488  
   489  	return msg
   490  }
   491  
   492  func (a *App) buildFullPushNotificationMessage(contentsConfig string, post *model.Post, user *model.User, class *model.Class, className string, senderName string,
   493  	explicitMention bool, classWideMention bool, replyToThreadType string) *model.PushNotification {
   494  
   495  	msg := &model.PushNotification{
   496  		Category:   model.CATEGORY_CAN_REPLY,
   497  		Version:    model.PUSH_MESSAGE_V2,
   498  		Type:       model.PUSH_TYPE_MESSAGE,
   499  		BranchId:   class.BranchId,
   500  		ClassId:    class.Id,
   501  		PostId:     post.Id,
   502  		SenderId:   post.UserId,
   503  		IsIdLoaded: false,
   504  	}
   505  
   506  	cfg := a.Config()
   507  	if contentsConfig != model.GENERIC_NO_CLASS_NOTIFICATION {
   508  		msg.ClassName = className
   509  	}
   510  
   511  	msg.SenderName = senderName
   512  	if ou, ok := post.GetProp("override_username").(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride {
   513  		msg.OverrideUsername = ou
   514  		msg.SenderName = ou
   515  	}
   516  
   517  	if oi, ok := post.GetProp("override_icon_url").(string); ok && *cfg.ServiceSettings.EnablePostIconOverride {
   518  		msg.OverrideIconUrl = oi
   519  	}
   520  
   521  	if fw, ok := post.GetProp("from_webhook").(string); ok {
   522  		msg.FromWebhook = fw
   523  	}
   524  
   525  	userLocale := utils.GetUserTranslations(user.Locale)
   526  	hasFiles := post.FileIds != nil && len(post.FileIds) > 0
   527  
   528  	msg.Message = a.getPushNotificationMessage(contentsConfig, post.Message, explicitMention, classWideMention, hasFiles, msg.SenderName, className, replyToThreadType, userLocale)
   529  
   530  	return msg
   531  }