diff --git a/api4/system.go b/api4/system.go index 8e192bb382..385ab28c63 100644 --- a/api4/system.go +++ b/api4/system.go @@ -410,7 +410,27 @@ func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) { } err := c.App.SendAckToPushProxy(ack) - if err != nil { + if ack.NotificationType == model.PUSH_TYPE_ID_LOADED { + if err != nil { + // Log the error only, then continue to fetch notification message + c.App.NotificationsLog.Error("Notification ack not sent to push proxy", + mlog.String("ackId", ack.Id), + mlog.String("type", ack.NotificationType), + mlog.String("postId", ack.PostId), + mlog.String("status", err.Error()), + ) + } + + msg, appErr := c.App.BuildFetchedPushNotificationMessage(ack.PostId, c.App.Session.UserId) + if appErr != nil { + c.Err = model.NewAppError("pushNotificationAck", "api.push_notification.id_loaded.fetch.app_error", nil, appErr.Error(), http.StatusInternalServerError) + return + } + + w.Write([]byte(msg.ToJson())) + + return + } else if err != nil { c.Err = model.NewAppError("pushNotificationAck", "api.push_notifications_ack.forward.app_error", nil, err.Error(), http.StatusInternalServerError) return } diff --git a/app/notification.go b/app/notification.go index c3b5fa99b9..a69232c12e 100644 --- a/app/notification.go +++ b/app/notification.go @@ -151,7 +151,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod updateMentionChans = append(updateMentionChans, umc) } - notification := &postNotification{ + notification := &PostNotification{ post: post, channel: channel, profileMap: profileMap, @@ -638,7 +638,7 @@ func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, lookF } // Represents either an email or push notification and contains the fields required to send it to any user. -type postNotification struct { +type PostNotification struct { channel *model.Channel post *model.Post profileMap map[string]*model.User @@ -648,7 +648,7 @@ type postNotification struct { // Returns the name of the channel for this notification. For direct messages, this is the sender's name // preceeded by an at sign. For group messages, this is a comma-separated list of the members of the // channel, with an option to exclude the recipient of the message from that list. -func (n *postNotification) GetChannelName(userNameFormat string, excludeId string) string { +func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string { switch n.channel.Type { case model.CHANNEL_DIRECT: return n.sender.GetDisplayNameWithPrefix(userNameFormat, "@") @@ -670,7 +670,7 @@ func (n *postNotification) GetChannelName(userNameFormat string, excludeId strin // Returns the name of the sender of this notification, accounting for things like system messages // and whether or not the username has been overridden by an integration. -func (n *postNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string { +func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string { if n.post.IsSystemMessage() { return utils.T("system.message.name") } diff --git a/app/notification_email.go b/app/notification_email.go index 20af2a7fe3..65c5ff0f8d 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -18,7 +18,7 @@ import ( "github.com/mattermost/mattermost-server/utils" ) -func (a *App) sendNotificationEmail(notification *postNotification, user *model.User, team *model.Team) *model.AppError { +func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) *model.AppError { channel := notification.channel post := notification.post diff --git a/app/notification_push.go b/app/notification_push.go index 32596eb804..9da1fadee8 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -102,7 +102,7 @@ func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, chann return nil } -func (a *App) sendPushNotification(notification *postNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) { +func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) { cfg := a.Config() channel := notification.channel post := notification.post @@ -126,6 +126,22 @@ func (a *App) sendPushNotification(notification *postNotification, user *model.U } } +func (a *App) getFetchedPushNotificationMessage(postMessage, senderName, channelType string, hasFiles bool, userLocale i18n.TranslateFunc) string { + // If the post only has images then push an appropriate message + if len(postMessage) == 0 && hasFiles { + if channelType == model.CHANNEL_DIRECT { + return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ") + } + return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + } + + if channelType == model.CHANNEL_DIRECT { + return model.ClearMentionTags(postMessage) + } + + return senderName + ": " + model.ClearMentionTags(postMessage) +} + func (a *App) getPushNotificationMessage(postMessage string, explicitMention, channelWideMention, hasFiles bool, senderName, channelName, channelType, replyToThreadType string, userLocale i18n.TranslateFunc) string { @@ -427,9 +443,109 @@ func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *mo return false } +func (a *App) BuildFetchedPushNotificationMessage(postId string, userId string) (model.PushNotification, *model.AppError) { + msg := model.PushNotification{ + Type: model.PUSH_TYPE_ID_LOADED, + Category: model.CATEGORY_CAN_REPLY, + Version: model.PUSH_MESSAGE_V2, + } + + post, err := a.GetSinglePost(postId) + if err != nil { + return msg, err + } + + channel, err := a.GetChannel(post.ChannelId) + if err != nil { + return msg, err + } + + user, err := a.GetUser(userId) + if err != nil { + return msg, err + } + + sender, err := a.GetUser(post.UserId) + if err != nil { + return msg, err + } + + msg.PostId = post.Id + msg.RootId = post.RootId + msg.SenderId = post.UserId + msg.ChannelId = channel.Id + msg.TeamId = channel.TeamId + + notification := &PostNotification{ + post: post, + channel: channel, + sender: sender, + } + + cfg := a.Config() + nameFormat := a.GetNotificationNameFormat(user) + channelName := notification.GetChannelName(nameFormat, user.Id) + senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride) + + msg.ChannelName = channelName + + msg.SenderName = senderName + if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { + msg.OverrideUsername = ou + msg.SenderName = ou + } + + if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { + msg.OverrideIconUrl = oi + } + + if fw, ok := post.Props["from_webhook"].(string); ok { + msg.FromWebhook = fw + } + + userLocale := utils.GetUserTranslations(user.Locale) + hasFiles := post.FileIds != nil && len(post.FileIds) > 0 + + msg.Message = a.getFetchedPushNotificationMessage(post.Message, msg.SenderName, channel.Type, hasFiles, userLocale) + + return msg, nil +} + func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string) model.PushNotification { + var msg model.PushNotification + + cfg := a.Config() + contentsConfig := *cfg.EmailSettings.PushNotificationContents + if contentsConfig == model.ID_LOADED_NOTIFICATION { + msg = a.buildIdLoadedPushNotificationMessage(post, user) + } else { + msg = a.buildFullPushNotificationMessage(post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType) + } + + msg.Badge = a.getPushNotificationBadge(user, channel) + + return msg +} + +func (a *App) buildIdLoadedPushNotificationMessage(post *model.Post, user *model.User) model.PushNotification { + userLocale := utils.GetUserTranslations(user.Locale) + msg := model.PushNotification{ + PostId: post.Id, + ChannelId: post.ChannelId, + Category: model.CATEGORY_CAN_REPLY, + Version: model.PUSH_MESSAGE_V2, + Type: model.PUSH_TYPE_ID_LOADED, + Message: userLocale("api.push_notification.id_loaded.default_message"), + } + + return msg +} + +func (a *App) buildFullPushNotificationMessage(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, + explicitMention bool, channelWideMention bool, replyToThreadType string) model.PushNotification { + msg := model.PushNotification{ Category: model.CATEGORY_CAN_REPLY, Version: model.PUSH_MESSAGE_V2, @@ -441,22 +557,6 @@ func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, c SenderId: post.UserId, } - if user.NotifyProps["push"] == "all" { - if unreadCount, err := a.Srv.Store.User().GetAnyUnreadPostCountForChannel(user.Id, channel.Id); err != nil { - msg.Badge = 1 - mlog.Error("We could not get the unread message count for the user", mlog.String("user_id", user.Id), mlog.Err(err)) - } else { - msg.Badge = int(unreadCount) - } - } else { - if unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id); err != nil { - msg.Badge = 1 - mlog.Error("We could not get the unread message count for the user", mlog.String("user_id", user.Id), mlog.Err(err)) - } else { - msg.Badge = int(unreadCount) - } - } - cfg := a.Config() contentsConfig := *cfg.EmailSettings.PushNotificationContents if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT { @@ -484,3 +584,25 @@ func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, c return msg } + +func (a *App) getPushNotificationBadge(user *model.User, channel *model.Channel) int { + var badge int + + if user.NotifyProps["push"] == "all" { + if unreadCount, err := a.Srv.Store.User().GetAnyUnreadPostCountForChannel(user.Id, channel.Id); err != nil { + badge = 1 + mlog.Error("We could not get the unread message count for the user", mlog.String("user_id", user.Id), mlog.Err(err)) + } else { + badge = int(unreadCount) + } + } else { + if unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id); err != nil { + badge = 1 + mlog.Error("We could not get the unread message count for the user", mlog.String("user_id", user.Id), mlog.Err(err)) + } else { + badge = int(unreadCount) + } + } + + return badge +} diff --git a/app/notification_push_test.go b/app/notification_push_test.go index 5efd7228a1..65908a14b1 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -898,7 +898,7 @@ func TestGetPushNotificationMessage(t *testing.T) { } } -func TestBuildPushNotificationMessage(t *testing.T) { +func TestBuildPushNotificationMessageMentions(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() @@ -947,3 +947,64 @@ func TestBuildPushNotificationMessage(t *testing.T) { }) } } + +func TestBuildPushNotificationMessageContents(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + team := th.CreateTeam() + sender := th.CreateUser() + receiver := th.CreateUser() + th.LinkUserToTeam(sender, team) + th.LinkUserToTeam(receiver, team) + channel := th.CreateChannel(team) + th.AddUserToChannel(sender, channel) + th.AddUserToChannel(receiver, channel) + + post := th.CreatePost(channel) + + explicitMention := false + channelWideMention := false + replyToThreadType := "" + + receiverLocale := utils.GetUserTranslations(receiver.Locale) + + for name, tc := range map[string]struct { + contentsConfig string + expectedMsg model.PushNotification + }{ + "only post ID, channel ID, and message included in push notification": { + contentsConfig: model.ID_LOADED_NOTIFICATION, + expectedMsg: model.PushNotification{ + PostId: post.Id, + ChannelId: post.ChannelId, + Category: model.CATEGORY_CAN_REPLY, + Version: model.PUSH_MESSAGE_V2, + Type: model.PUSH_TYPE_ID_LOADED, + Message: receiverLocale("api.push_notification.id_loaded.default_message"), + }, + }, + "full contents included in push notification": { + contentsConfig: model.GENERIC_NOTIFICATION, + expectedMsg: model.PushNotification{ + Category: model.CATEGORY_CAN_REPLY, + Version: model.PUSH_MESSAGE_V2, + Type: model.PUSH_TYPE_MESSAGE, + PostId: post.Id, + TeamId: channel.TeamId, + ChannelId: channel.Id, + ChannelName: channel.Name, + RootId: post.RootId, + SenderId: post.UserId, + SenderName: sender.Username, + Message: fmt.Sprintf("%s posted a message.", sender.Username), + }, + }, + } { + t.Run(name, func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.PushNotificationContents = tc.contentsConfig }) + msg := th.App.BuildPushNotificationMessage(post, receiver, channel, channel.Name, sender.Username, explicitMention, channelWideMention, replyToThreadType) + assert.Equal(t, tc.expectedMsg, msg) + }) + } +} diff --git a/app/notification_test.go b/app/notification_test.go index 5b08bffd2e..131180216b 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -1274,7 +1274,7 @@ func TestPostNotificationGetChannelName(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - notification := &postNotification{ + notification := &PostNotification{ channel: testCase.channel, sender: sender, profileMap: profileMap, @@ -1359,7 +1359,7 @@ func TestPostNotificationGetSenderName(t *testing.T) { post = testCase.post } - notification := &postNotification{ + notification := &PostNotification{ channel: channel, post: post, sender: sender, diff --git a/i18n/en.json b/i18n/en.json index 05bbe3519a..c6ec060d5d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1706,6 +1706,14 @@ "id": "api.push_notification.disabled.app_error", "translation": "Push Notifications are disabled on this server." }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "You've received a new message." + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "An error occurred fetching the ID-loaded push notification" + }, { "id": "api.push_notifications_ack.forward.app_error", "translation": "An error occurred sending the receipt delivery to the push notification service" diff --git a/model/config.go b/model/config.go index a883cd33bb..c34773adf3 100644 --- a/model/config.go +++ b/model/config.go @@ -48,6 +48,7 @@ const ( GENERIC_NOTIFICATION = "generic" GENERIC_NOTIFICATION_SERVER = "https://push-test.mattermost.com" FULL_NOTIFICATION = "full" + ID_LOADED_NOTIFICATION = "id_loaded" DIRECT_MESSAGE_ANY = "any" DIRECT_MESSAGE_TEAM = "team" diff --git a/model/push_notification.go b/model/push_notification.go index 03b49bc897..f7e9e75c66 100644 --- a/model/push_notification.go +++ b/model/push_notification.go @@ -15,9 +15,10 @@ const ( PUSH_NOTIFY_APPLE_REACT_NATIVE = "apple_rn" PUSH_NOTIFY_ANDROID_REACT_NATIVE = "android_rn" - PUSH_TYPE_MESSAGE = "message" - PUSH_TYPE_CLEAR = "clear" - PUSH_MESSAGE_V2 = "v2" + PUSH_TYPE_ID_LOADED = "id_loaded" + PUSH_TYPE_MESSAGE = "message" + PUSH_TYPE_CLEAR = "clear" + PUSH_MESSAGE_V2 = "v2" // The category is set to handle a set of interactive Actions // with the push notifications @@ -36,6 +37,7 @@ type PushNotificationAck struct { ClientReceivedAt int64 `json:"received_at"` ClientPlatform string `json:"platform"` NotificationType string `json:"type"` + PostId string `json:"post_id,omitempty"` } type PushNotification struct { @@ -43,23 +45,23 @@ type PushNotification struct { Platform string `json:"platform"` ServerId string `json:"server_id"` DeviceId string `json:"device_id"` - Category string `json:"category"` - Sound string `json:"sound"` - Message string `json:"message"` - Badge int `json:"badge"` - ContentAvailable int `json:"cont_ava"` - TeamId string `json:"team_id"` - ChannelId string `json:"channel_id"` PostId string `json:"post_id"` - RootId string `json:"root_id"` - ChannelName string `json:"channel_name"` - Type string `json:"type"` - SenderId string `json:"sender_id"` - SenderName string `json:"sender_name"` - OverrideUsername string `json:"override_username"` - OverrideIconUrl string `json:"override_icon_url"` - FromWebhook string `json:"from_webhook"` - Version string `json:"version"` + Category string `json:"category,omitempty"` + Sound string `json:"sound,omitempty"` + Message string `json:"message,omitempty"` + Badge int `json:"badge,omitempty"` + ContentAvailable int `json:"cont_ava,omitempty"` + TeamId string `json:"team_id,omitempty"` + ChannelId string `json:"channel_id,omitempty"` + RootId string `json:"root_id,omitempty"` + ChannelName string `json:"channel_name,omitempty"` + Type string `json:"type,omitempty"` + SenderId string `json:"sender_id,omitempty"` + SenderName string `json:"sender_name,omitempty"` + OverrideUsername string `json:"override_username,omitempty"` + OverrideIconUrl string `json:"override_icon_url,omitempty"` + FromWebhook string `json:"from_webhook,omitempty"` + Version string `json:"version,omitempty"` } func (me *PushNotification) ToJson() string {