Merge pull request #2064 from hmhealey/plt882
PLT-882 Ephemeral Messages and Out-Of-Channel mentions
Этот коммит содержится в:
602
api/post.go
602
api/post.go
@@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -231,6 +232,8 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo
|
||||
tchan := Srv.Store.Team().Get(c.Session.TeamId)
|
||||
cchan := Srv.Store.Channel().Get(post.ChannelId)
|
||||
uchan := Srv.Store.User().Get(post.UserId)
|
||||
pchan := Srv.Store.User().GetProfiles(c.Session.TeamId)
|
||||
mchan := Srv.Store.Channel().GetMembers(post.ChannelId)
|
||||
|
||||
var team *model.Team
|
||||
if result := <-tchan; result.Err != nil {
|
||||
@@ -248,7 +251,24 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
sendNotificationsAndForget(c, post, team, channel)
|
||||
var profiles map[string]*model.User
|
||||
if result := <-pchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.Session.TeamId, result.Err)
|
||||
return
|
||||
} else {
|
||||
profiles = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
var members []model.ChannelMember
|
||||
if result := <-mchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err)
|
||||
return
|
||||
} else {
|
||||
members = result.Data.([]model.ChannelMember)
|
||||
}
|
||||
|
||||
go sendNotifications(c, post, team, channel, profiles, members)
|
||||
go checkForOutOfChannelMentions(c, post, channel, profiles, members)
|
||||
|
||||
var user *model.User
|
||||
if result := <-uchan; result.Err != nil {
|
||||
@@ -413,311 +433,290 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
|
||||
|
||||
}
|
||||
|
||||
func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
|
||||
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) {
|
||||
var channelName string
|
||||
var bodyText string
|
||||
var subjectText string
|
||||
|
||||
go func() {
|
||||
// Get a list of user names (to be used as keywords) and ids for the given team
|
||||
uchan := Srv.Store.User().GetProfiles(c.Session.TeamId)
|
||||
echan := Srv.Store.Channel().GetMembers(post.ChannelId)
|
||||
var mentionedUsers []string
|
||||
|
||||
var channelName string
|
||||
var bodyText string
|
||||
var subjectText string
|
||||
if _, ok := profileMap[post.UserId]; !ok {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
|
||||
return
|
||||
}
|
||||
senderName := profileMap[post.UserId].Username
|
||||
|
||||
var mentionedUsers []string
|
||||
toEmailMap := make(map[string]bool)
|
||||
|
||||
if result := <-uchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.retrive_profiles.error"), c.Session.TeamId, result.Err)
|
||||
return
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
|
||||
var otherUserId string
|
||||
if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
channelName = profileMap[userIds[1]].Username
|
||||
} else {
|
||||
profileMap := result.Data.(map[string]*model.User)
|
||||
otherUserId = userIds[0]
|
||||
channelName = profileMap[userIds[0]].Username
|
||||
}
|
||||
|
||||
if _, ok := profileMap[post.UserId]; !ok {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
|
||||
return
|
||||
otherUser := profileMap[otherUserId]
|
||||
sendEmail := true
|
||||
if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" {
|
||||
sendEmail = false
|
||||
}
|
||||
if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) {
|
||||
toEmailMap[otherUserId] = true
|
||||
}
|
||||
|
||||
} else {
|
||||
// Find out who is a member of the channel, only keep those profiles
|
||||
tempProfileMap := make(map[string]*model.User)
|
||||
for _, member := range members {
|
||||
tempProfileMap[member.UserId] = profileMap[member.UserId]
|
||||
}
|
||||
|
||||
profileMap = tempProfileMap
|
||||
|
||||
// Build map for keywords
|
||||
keywordMap := make(map[string][]string)
|
||||
for _, profile := range profileMap {
|
||||
if len(profile.NotifyProps["mention_keys"]) > 0 {
|
||||
|
||||
// Add all the user's mention keys
|
||||
splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
|
||||
for _, k := range splitKeys {
|
||||
keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id)
|
||||
}
|
||||
}
|
||||
senderName := profileMap[post.UserId].Username
|
||||
|
||||
toEmailMap := make(map[string]bool)
|
||||
// If turned on, add the user's case sensitive first name
|
||||
if profile.NotifyProps["first_name"] == "true" {
|
||||
keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id)
|
||||
}
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
// Add @all to keywords if user has them turned on
|
||||
// if profile.NotifyProps["all"] == "true" {
|
||||
// keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
|
||||
// }
|
||||
|
||||
var otherUserId string
|
||||
if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
channelName = profileMap[userIds[1]].Username
|
||||
} else {
|
||||
otherUserId = userIds[0]
|
||||
channelName = profileMap[userIds[0]].Username
|
||||
}
|
||||
// Add @channel to keywords if user has them turned on
|
||||
if profile.NotifyProps["channel"] == "true" {
|
||||
keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
|
||||
}
|
||||
}
|
||||
|
||||
otherUser := profileMap[otherUserId]
|
||||
sendEmail := true
|
||||
if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" {
|
||||
sendEmail = false
|
||||
}
|
||||
if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) {
|
||||
toEmailMap[otherUserId] = true
|
||||
}
|
||||
// Build a map as a list of unique user_ids that are mentioned in this post
|
||||
splitF := func(c rune) bool {
|
||||
return model.SplitRunes[c]
|
||||
}
|
||||
splitMessage := strings.Fields(post.Message)
|
||||
for _, word := range splitMessage {
|
||||
var userIds []string
|
||||
|
||||
} else {
|
||||
// Non-case-sensitive check for regular keys
|
||||
if ids, match := keywordMap[strings.ToLower(word)]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
|
||||
// Find out who is a member of the channel, only keep those profiles
|
||||
if eResult := <-echan; eResult.Err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.members.error"), post.ChannelId, eResult.Err.Message)
|
||||
return
|
||||
} else {
|
||||
tempProfileMap := make(map[string]*model.User)
|
||||
members := eResult.Data.([]model.ChannelMember)
|
||||
for _, member := range members {
|
||||
tempProfileMap[member.UserId] = profileMap[member.UserId]
|
||||
}
|
||||
// Case-sensitive check for first name
|
||||
if ids, match := keywordMap[word]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
|
||||
profileMap = tempProfileMap
|
||||
}
|
||||
|
||||
// Build map for keywords
|
||||
keywordMap := make(map[string][]string)
|
||||
for _, profile := range profileMap {
|
||||
if len(profile.NotifyProps["mention_keys"]) > 0 {
|
||||
|
||||
// Add all the user's mention keys
|
||||
splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
|
||||
for _, k := range splitKeys {
|
||||
keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// If turned on, add the user's case sensitive first name
|
||||
if profile.NotifyProps["first_name"] == "true" {
|
||||
keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id)
|
||||
}
|
||||
|
||||
// Add @all to keywords if user has them turned on
|
||||
// if profile.NotifyProps["all"] == "true" {
|
||||
// keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
|
||||
// }
|
||||
|
||||
// Add @channel to keywords if user has them turned on
|
||||
if profile.NotifyProps["channel"] == "true" {
|
||||
keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map as a list of unique user_ids that are mentioned in this post
|
||||
splitF := func(c rune) bool {
|
||||
return model.SplitRunes[c]
|
||||
}
|
||||
splitMessage := strings.Fields(post.Message)
|
||||
for _, word := range splitMessage {
|
||||
var userIds []string
|
||||
if len(userIds) == 0 {
|
||||
// No matches were found with the string split just on whitespace so try further splitting
|
||||
// the message on punctuation
|
||||
splitWords := strings.FieldsFunc(word, splitF)
|
||||
|
||||
for _, splitWord := range splitWords {
|
||||
// Non-case-sensitive check for regular keys
|
||||
if ids, match := keywordMap[strings.ToLower(word)]; match {
|
||||
if ids, match := keywordMap[strings.ToLower(splitWord)]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
|
||||
// Case-sensitive check for first name
|
||||
if ids, match := keywordMap[word]; match {
|
||||
if ids, match := keywordMap[splitWord]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
|
||||
if len(userIds) == 0 {
|
||||
// No matches were found with the string split just on whitespace so try further splitting
|
||||
// the message on punctuation
|
||||
splitWords := strings.FieldsFunc(word, splitF)
|
||||
|
||||
for _, splitWord := range splitWords {
|
||||
// Non-case-sensitive check for regular keys
|
||||
if ids, match := keywordMap[strings.ToLower(splitWord)]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
|
||||
// Case-sensitive check for first name
|
||||
if ids, match := keywordMap[splitWord]; match {
|
||||
userIds = append(userIds, ids...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, userId := range userIds {
|
||||
if post.UserId == userId {
|
||||
continue
|
||||
}
|
||||
sendEmail := true
|
||||
if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" {
|
||||
sendEmail = false
|
||||
}
|
||||
if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) {
|
||||
toEmailMap[userId] = true
|
||||
} else {
|
||||
toEmailMap[userId] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id := range toEmailMap {
|
||||
updateMentionCountAndForget(post.ChannelId, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toEmailMap) != 0 {
|
||||
mentionedUsers = make([]string, 0, len(toEmailMap))
|
||||
for k := range toEmailMap {
|
||||
mentionedUsers = append(mentionedUsers, k)
|
||||
for _, userId := range userIds {
|
||||
if post.UserId == userId {
|
||||
continue
|
||||
}
|
||||
sendEmail := true
|
||||
if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" {
|
||||
sendEmail = false
|
||||
}
|
||||
if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) {
|
||||
toEmailMap[userId] = true
|
||||
} else {
|
||||
toEmailMap[userId] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id := range toEmailMap {
|
||||
updateMentionCountAndForget(post.ChannelId, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toEmailMap) != 0 {
|
||||
mentionedUsers = make([]string, 0, len(toEmailMap))
|
||||
for k := range toEmailMap {
|
||||
mentionedUsers = append(mentionedUsers, k)
|
||||
}
|
||||
|
||||
teamURL := c.GetSiteURL() + "/" + team.Name
|
||||
|
||||
// Build and send the emails
|
||||
tm := time.Unix(post.CreateAt/1000, 0)
|
||||
|
||||
for id, doSend := range toEmailMap {
|
||||
|
||||
if !doSend {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if inactive
|
||||
if profileMap[id].DeleteAt > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
userLocale := utils.GetUserTranslations(profileMap[id].Locale)
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
bodyText = userLocale("api.post.send_notifications_and_forget.message_body")
|
||||
subjectText = userLocale("api.post.send_notifications_and_forget.message_subject")
|
||||
} else {
|
||||
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
||||
subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
|
||||
channelName = channel.DisplayName
|
||||
}
|
||||
|
||||
month := userLocale(tm.Month().String())
|
||||
day := fmt.Sprintf("%d", tm.Day())
|
||||
year := fmt.Sprintf("%d", tm.Year())
|
||||
zone, _ := tm.Zone()
|
||||
|
||||
subjectPage := NewServerTemplatePage("post_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
|
||||
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||
"Month": month[:3], "Day": day, "Year": year})
|
||||
|
||||
bodyPage := NewServerTemplatePage("post_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
|
||||
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
|
||||
bodyPage.Props["BodyText"] = bodyText
|
||||
bodyPage.Props["Button"] = userLocale("api.templates.post_body.button")
|
||||
bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info",
|
||||
map[string]interface{}{"ChannelName": channelName, "SenderName": senderName,
|
||||
"Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()),
|
||||
"TimeZone": zone, "Month": month, "Day": day}))
|
||||
|
||||
// attempt to fill in a message body if the post doesn't have any text
|
||||
if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 {
|
||||
// extract the filenames from their paths and determine what type of files are attached
|
||||
filenames := make([]string, len(post.Filenames))
|
||||
onlyImages := true
|
||||
for i, filename := range post.Filenames {
|
||||
var err error
|
||||
if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil {
|
||||
// this should never error since filepath was escaped using url.QueryEscape
|
||||
filenames[i] = filepath.Base(filename)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
onlyImages = onlyImages && model.IsFileExtImage(ext)
|
||||
}
|
||||
filenamesString := strings.Join(filenames, ", ")
|
||||
|
||||
var attachmentPrefix string
|
||||
if onlyImages {
|
||||
attachmentPrefix = "Image"
|
||||
} else {
|
||||
attachmentPrefix = "File"
|
||||
}
|
||||
if len(post.Filenames) > 1 {
|
||||
attachmentPrefix += "s"
|
||||
}
|
||||
|
||||
teamURL := c.GetSiteURL() + "/" + team.Name
|
||||
bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent",
|
||||
map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString})
|
||||
}
|
||||
|
||||
// Build and send the emails
|
||||
tm := time.Unix(post.CreateAt/1000, 0)
|
||||
if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err)
|
||||
}
|
||||
|
||||
for id, doSend := range toEmailMap {
|
||||
if *utils.Cfg.EmailSettings.SendPushNotifications {
|
||||
sessionChan := Srv.Store.Session().GetSessions(id)
|
||||
if result := <-sessionChan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err)
|
||||
} else {
|
||||
sessions := result.Data.([]*model.Session)
|
||||
alreadySeen := make(map[string]string)
|
||||
|
||||
if !doSend {
|
||||
continue
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" &&
|
||||
(strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) {
|
||||
alreadySeen[session.DeviceId] = session.DeviceId
|
||||
|
||||
// skip if inactive
|
||||
if profileMap[id].DeleteAt > 0 {
|
||||
continue
|
||||
}
|
||||
msg := model.PushNotification{}
|
||||
msg.Badge = 1
|
||||
msg.ServerId = utils.CfgDiagnosticId
|
||||
|
||||
userLocale := utils.GetUserTranslations(profileMap[id].Locale)
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
bodyText = userLocale("api.post.send_notifications_and_forget.message_body")
|
||||
subjectText = userLocale("api.post.send_notifications_and_forget.message_subject")
|
||||
} else {
|
||||
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
||||
subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
|
||||
channelName = channel.DisplayName
|
||||
}
|
||||
|
||||
month := userLocale(tm.Month().String())
|
||||
day := fmt.Sprintf("%d", tm.Day())
|
||||
year := fmt.Sprintf("%d", tm.Year())
|
||||
zone, _ := tm.Zone()
|
||||
|
||||
subjectPage := NewServerTemplatePage("post_subject", c.Locale)
|
||||
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
|
||||
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||
"Month": month[:3], "Day": day, "Year": year})
|
||||
|
||||
bodyPage := NewServerTemplatePage("post_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = c.GetSiteURL()
|
||||
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
|
||||
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
|
||||
bodyPage.Props["BodyText"] = bodyText
|
||||
bodyPage.Props["Button"] = userLocale("api.templates.post_body.button")
|
||||
bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info",
|
||||
map[string]interface{}{"ChannelName": channelName, "SenderName": senderName,
|
||||
"Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()),
|
||||
"TimeZone": zone, "Month": month, "Day": day}))
|
||||
|
||||
// attempt to fill in a message body if the post doesn't have any text
|
||||
if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 {
|
||||
// extract the filenames from their paths and determine what type of files are attached
|
||||
filenames := make([]string, len(post.Filenames))
|
||||
onlyImages := true
|
||||
for i, filename := range post.Filenames {
|
||||
var err error
|
||||
if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil {
|
||||
// this should never error since filepath was escaped using url.QueryEscape
|
||||
filenames[i] = filepath.Base(filename)
|
||||
if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") {
|
||||
msg.Platform = model.PUSH_NOTIFY_APPLE
|
||||
msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":")
|
||||
} else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") {
|
||||
msg.Platform = model.PUSH_NOTIFY_ANDROID
|
||||
msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
onlyImages = onlyImages && model.IsFileExtImage(ext)
|
||||
}
|
||||
filenamesString := strings.Join(filenames, ", ")
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
|
||||
} else {
|
||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
|
||||
}
|
||||
|
||||
var attachmentPrefix string
|
||||
if onlyImages {
|
||||
attachmentPrefix = "Image"
|
||||
} else {
|
||||
attachmentPrefix = "File"
|
||||
}
|
||||
if len(post.Filenames) > 1 {
|
||||
attachmentPrefix += "s"
|
||||
}
|
||||
httpClient := http.Client{}
|
||||
request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson()))
|
||||
|
||||
bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent",
|
||||
map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString})
|
||||
}
|
||||
|
||||
if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err)
|
||||
}
|
||||
|
||||
if *utils.Cfg.EmailSettings.SendPushNotifications {
|
||||
sessionChan := Srv.Store.Session().GetSessions(id)
|
||||
if result := <-sessionChan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err)
|
||||
} else {
|
||||
sessions := result.Data.([]*model.Session)
|
||||
alreadySeen := make(map[string]string)
|
||||
|
||||
for _, session := range sessions {
|
||||
if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" &&
|
||||
(strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) {
|
||||
alreadySeen[session.DeviceId] = session.DeviceId
|
||||
|
||||
msg := model.PushNotification{}
|
||||
msg.Badge = 1
|
||||
msg.ServerId = utils.CfgDiagnosticId
|
||||
|
||||
if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") {
|
||||
msg.Platform = model.PUSH_NOTIFY_APPLE
|
||||
msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":")
|
||||
} else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") {
|
||||
msg.Platform = model.PUSH_NOTIFY_ANDROID
|
||||
msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")
|
||||
}
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
|
||||
} else {
|
||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
|
||||
}
|
||||
|
||||
httpClient := http.Client{}
|
||||
request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson()))
|
||||
|
||||
l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message)
|
||||
if _, err := httpClient.Do(request); err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err)
|
||||
}
|
||||
}
|
||||
l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message)
|
||||
if _, err := httpClient.Do(request); err != nil {
|
||||
l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
|
||||
message.Add("post", post.ToJson())
|
||||
message.Add("channel_type", channel.Type)
|
||||
message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
|
||||
message.Add("post", post.ToJson())
|
||||
message.Add("channel_type", channel.Type)
|
||||
|
||||
if len(post.Filenames) != 0 {
|
||||
message.Add("otherFile", "true")
|
||||
if len(post.Filenames) != 0 {
|
||||
message.Add("otherFile", "true")
|
||||
|
||||
for _, filename := range post.Filenames {
|
||||
ext := filepath.Ext(filename)
|
||||
if model.IsFileExtImage(ext) {
|
||||
message.Add("image", "true")
|
||||
break
|
||||
}
|
||||
for _, filename := range post.Filenames {
|
||||
ext := filepath.Ext(filename)
|
||||
if model.IsFileExtImage(ext) {
|
||||
message.Add("image", "true")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mentionedUsers) != 0 {
|
||||
message.Add("mentions", model.ArrayToJson(mentionedUsers))
|
||||
}
|
||||
if len(mentionedUsers) != 0 {
|
||||
message.Add("mentions", model.ArrayToJson(mentionedUsers))
|
||||
}
|
||||
|
||||
PublishAndForget(message)
|
||||
}()
|
||||
PublishAndForget(message)
|
||||
}
|
||||
|
||||
func updateMentionCountAndForget(channelId, userId string) {
|
||||
@@ -728,6 +727,95 @@ func updateMentionCountAndForget(channelId, userId string) {
|
||||
}()
|
||||
}
|
||||
|
||||
func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel, allProfiles map[string]*model.User, members []model.ChannelMember) {
|
||||
// don't check for out of channel mentions in direct channels
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
return
|
||||
}
|
||||
|
||||
mentioned := getOutOfChannelMentions(post, allProfiles, members)
|
||||
if len(mentioned) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
usernames := make([]string, len(mentioned))
|
||||
for i, user := range mentioned {
|
||||
usernames[i] = user.Username
|
||||
}
|
||||
sort.Strings(usernames)
|
||||
|
||||
var message string
|
||||
if len(usernames) == 1 {
|
||||
message = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
|
||||
"Username": usernames[0],
|
||||
})
|
||||
} else {
|
||||
message = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
|
||||
"Usernames": strings.Join(usernames[:len(usernames)-1], ", "),
|
||||
"LastUsername": usernames[len(usernames)-1],
|
||||
})
|
||||
}
|
||||
|
||||
SendEphemeralPost(
|
||||
c.Session.TeamId,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: message,
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Gets a list of users that were mentioned in a given post that aren't in the channel that the post was made in
|
||||
func getOutOfChannelMentions(post *model.Post, allProfiles map[string]*model.User, members []model.ChannelMember) []*model.User {
|
||||
// copy the profiles map since we'll be removing items from it
|
||||
profiles := make(map[string]*model.User)
|
||||
for id, profile := range allProfiles {
|
||||
profiles[id] = profile
|
||||
}
|
||||
|
||||
// only keep profiles which aren't in the current channel
|
||||
for _, member := range members {
|
||||
delete(profiles, member.UserId)
|
||||
}
|
||||
|
||||
var mentioned []*model.User
|
||||
|
||||
for _, profile := range profiles {
|
||||
if pattern, err := regexp.Compile(`(\W|^)@` + regexp.QuoteMeta(profile.Username) + `(\W|$)`); err != nil {
|
||||
l4g.Error(utils.T("api.post.get_out_of_channel_mentions.regex.error"), profile.Id, err)
|
||||
} else if pattern.MatchString(post.Message) {
|
||||
mentioned = append(mentioned, profile)
|
||||
}
|
||||
}
|
||||
|
||||
return mentioned
|
||||
}
|
||||
|
||||
func SendEphemeralPost(teamId, userId string, post *model.Post) {
|
||||
post.Type = model.POST_EPHEMERAL
|
||||
|
||||
// fill in fields which haven't been specified which have sensible defaults
|
||||
if post.Id == "" {
|
||||
post.Id = model.NewId()
|
||||
}
|
||||
if post.CreateAt == 0 {
|
||||
post.CreateAt = model.GetMillis()
|
||||
}
|
||||
if post.Props == nil {
|
||||
post.Props = model.StringInterface{}
|
||||
}
|
||||
if post.Filenames == nil {
|
||||
post.Filenames = []string{}
|
||||
}
|
||||
|
||||
message := model.NewMessage(teamId, post.ChannelId, userId, model.ACTION_EPHEMERAL_MESSAGE)
|
||||
message.Add("post", post.ToJson())
|
||||
|
||||
PublishAndForget(message)
|
||||
}
|
||||
|
||||
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
post := model.PostFromJson(r.Body)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mattermost/platform/store"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -857,3 +858,97 @@ func TestMakeDirectChannelVisible(t *testing.T) {
|
||||
t.Fatal("Failed to set direct channel to be visible for user2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOutOfChannelMentions(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team1 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN}
|
||||
team1 = Client.Must(Client.CreateTeam(team1)).Data.(*model.Team)
|
||||
|
||||
user1 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user1"}
|
||||
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
|
||||
|
||||
user2 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user2"}
|
||||
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
|
||||
|
||||
user3 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user3"}
|
||||
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user3.Id))
|
||||
|
||||
Client.Must(Client.LoginByEmail(team1.Name, user1.Email, "pwd"))
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team1.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
var allProfiles map[string]*model.User
|
||||
if result := <-Srv.Store.User().GetProfiles(team1.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
allProfiles = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
var members []model.ChannelMember
|
||||
if result := <-Srv.Store.Channel().GetMembers(channel1.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
members = result.Data.([]model.ChannelMember)
|
||||
}
|
||||
|
||||
// test a post that doesn't @mention anybody
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "user1 user2 user3"}
|
||||
if mentioned := getOutOfChannelMentions(post1, allProfiles, members); len(mentioned) != 0 {
|
||||
t.Fatalf("getOutOfChannelMentions returned %v when no users were mentioned", mentioned)
|
||||
}
|
||||
|
||||
// test a post that @mentions someone in the channel
|
||||
post2 := &model.Post{ChannelId: channel1.Id, Message: "@user1 is user1"}
|
||||
if mentioned := getOutOfChannelMentions(post2, allProfiles, members); len(mentioned) != 0 {
|
||||
t.Fatalf("getOutOfChannelMentions returned %v when only users in the channel were mentioned", mentioned)
|
||||
}
|
||||
|
||||
// test a post that @mentions someone not in the channel
|
||||
post3 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user3 aren't in the channel"}
|
||||
if mentioned := getOutOfChannelMentions(post3, allProfiles, members); len(mentioned) != 2 || (mentioned[0].Id != user2.Id && mentioned[0].Id != user3.Id) || (mentioned[1].Id != user2.Id && mentioned[1].Id != user3.Id) {
|
||||
t.Fatalf("getOutOfChannelMentions returned %v when two users outside the channel were mentioned", mentioned)
|
||||
}
|
||||
|
||||
// test a post that @mentions someone not in the channel as well as someone in the channel
|
||||
post4 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user1 might be in the channel"}
|
||||
if mentioned := getOutOfChannelMentions(post4, allProfiles, members); len(mentioned) != 1 || mentioned[0].Id != user2.Id {
|
||||
t.Fatalf("getOutOfChannelMentions returned %v when someone in the channel and someone outside the channel were mentioned", mentioned)
|
||||
}
|
||||
|
||||
Client.Must(Client.Logout())
|
||||
|
||||
team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN}
|
||||
team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
|
||||
|
||||
user4 := &model.User{TeamId: team2.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user4"}
|
||||
user4 = Client.Must(Client.CreateUser(user4, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user4.Id))
|
||||
|
||||
Client.Must(Client.LoginByEmail(team2.Name, user4.Email, "pwd"))
|
||||
|
||||
channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id}
|
||||
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
|
||||
|
||||
if result := <-Srv.Store.User().GetProfiles(team2.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
allProfiles = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Channel().GetMembers(channel2.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
members = result.Data.([]model.ChannelMember)
|
||||
}
|
||||
|
||||
// test a post that @mentions someone on a different team
|
||||
post5 := &model.Post{ChannelId: channel2.Id, Message: "@user2 and @user3 might be in the channel"}
|
||||
if mentioned := getOutOfChannelMentions(post5, allProfiles, members); len(mentioned) != 0 {
|
||||
t.Fatalf("getOutOfChannelMentions returned %v when two users on a different team were mentioned", mentioned)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool {
|
||||
return false
|
||||
} else if msg.Action == model.ACTION_PREFERENCE_CHANGED {
|
||||
return false
|
||||
} else if msg.Action == model.ACTION_EPHEMERAL_MESSAGE {
|
||||
// For now, ephemeral messages are sent directly to individual users
|
||||
return false
|
||||
}
|
||||
|
||||
// Only report events to a user who is the subject of the event, or is in the channel of the event
|
||||
|
||||
Ссылка в новой задаче
Block a user