github.com/xzl8028/xenia-server@v0.0.0-20190809101854-18450a97da63/app/notification_push.go (about)

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