diff options
Diffstat (limited to 'api/post.go')
-rw-r--r-- | api/post.go | 1176 |
1 files changed, 31 insertions, 1145 deletions
diff --git a/api/post.go b/api/post.go index 8d255649e..270ab72ca 100644 --- a/api/post.go +++ b/api/post.go @@ -4,33 +4,15 @@ package api import ( - "crypto/tls" - "fmt" - "html" - "html/template" - "io" - "io/ioutil" "net/http" - "net/url" - "path/filepath" - "regexp" - "sort" "strconv" - "strings" - "time" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" - "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" - "github.com/nicksnyder/go-i18n/i18n" -) - -const ( - TRIGGERWORDS_FULL = 0 - TRIGGERWORDS_STARTSWITH = 1 ) func InitPost() { @@ -61,7 +43,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } post.UserId = c.Session.UserId - cchan := Srv.Store.Channel().Get(post.ChannelId, true) + cchan := app.Srv.Store.Channel().Get(post.ChannelId, true) if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_CREATE_POST) { return @@ -82,7 +64,11 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if rp, err := CreatePost(c, post, true); err != nil { + if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + post.CreateAt = 0 + } + + if rp, err := app.CreatePost(post, c.TeamId, true); err != nil { c.Err = err if c.Err.Id == "api.post.create_post.root_id.app_error" || @@ -95,7 +81,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } else { // Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app) if _, ok := post.Props["from_webhook"]; !ok { - if result := <-Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, c.Session.UserId); result.Err != nil { l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, c.Session.UserId, result.Err) } } @@ -104,1106 +90,6 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } } -func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post, *model.AppError) { - var pchan store.StoreChannel - if len(post.RootId) > 0 { - pchan = Srv.Store.Post().Get(post.RootId) - } - - // Verify the parent/child relationships are correct - if pchan != nil { - if presult := <-pchan; presult.Err != nil { - return nil, model.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "") - } else { - list := presult.Data.(*model.PostList) - if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) { - return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "") - } - - if post.ParentId == "" { - post.ParentId = post.RootId - } - - if post.RootId != post.ParentId { - parent := list.Posts[post.ParentId] - if parent == nil { - return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "") - } - } - } - } - - if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { - post.CreateAt = 0 - c.Err = nil - } - - post.Hashtags, _ = model.ParseHashtags(post.Message) - - var rpost *model.Post - if result := <-Srv.Store.Post().Save(post); result.Err != nil { - return nil, result.Err - } else { - rpost = result.Data.(*model.Post) - } - - if einterfaces.GetMetricsInterface() != nil { - einterfaces.GetMetricsInterface().IncrementPostCreate() - } - - if len(post.FileIds) > 0 { - // There's a rare bug where the client sends up duplicate FileIds so protect against that - post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds) - - for _, fileId := range post.FileIds { - if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil { - l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, c.Session.UserId, result.Err) - } - } - - if einterfaces.GetMetricsInterface() != nil { - einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds)) - } - } - - InvalidateCacheForChannel(rpost.ChannelId) - InvalidateCacheForChannelPosts(rpost.ChannelId) - - handlePostEvents(c, rpost, triggerWebhooks) - - return rpost, nil -} - -var linkWithTextRegex *regexp.Regexp = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) - -func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) { - post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType} - post.AddProp("from_webhook", "true") - - if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { - if len(overrideUsername) != 0 { - post.AddProp("override_username", overrideUsername) - } else { - post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) - } - } - - if utils.Cfg.ServiceSettings.EnablePostIconOverride { - if len(overrideIconUrl) != 0 { - post.AddProp("override_icon_url", overrideIconUrl) - } - } - - post.Message = parseSlackLinksToMarkdown(post.Message) - - if len(props) > 0 { - for key, val := range props { - if key == "attachments" { - parseSlackAttachment(post, val) - } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { - post.AddProp(key, val) - } - } - } - - if _, err := CreatePost(c, post, false); err != nil { - return nil, model.NewLocAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message) - } - - return post, nil -} - -func CreateCommandPost(c *Context, post *model.Post, response *model.CommandResponse) { - post.Message = parseSlackLinksToMarkdown(response.Text) - post.UserId = c.Session.UserId - post.CreateAt = model.GetMillis() - - if response.Attachments != nil { - parseSlackAttachment(post, response.Attachments) - } - - switch response.ResponseType { - case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL: - if _, err := CreatePost(c, post, true); err != nil { - c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") - } - case model.COMMAND_RESPONSE_TYPE_EPHEMERAL: - if response.Text == "" { - return - } - - post.ParentId = "" - SendEphemeralPost(c.TeamId, c.Session.UserId, post) - } -} - -// This method only parses and processes the attachments, -// all else should be set in the post which is passed -func parseSlackAttachment(post *model.Post, attachments interface{}) { - post.Type = model.POST_SLACK_ATTACHMENT - - if list, success := attachments.([]interface{}); success { - for i, aInt := range list { - attachment := aInt.(map[string]interface{}) - if aText, ok := attachment["text"].(string); ok { - aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") - attachment["text"] = aText - list[i] = attachment - } - if aText, ok := attachment["pretext"].(string); ok { - aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") - attachment["pretext"] = aText - list[i] = attachment - } - if fVal, ok := attachment["fields"]; ok { - if fields, ok := fVal.([]interface{}); ok { - // parse attachment field links into Markdown format - for j, fInt := range fields { - field := fInt.(map[string]interface{}) - if fValue, ok := field["value"].(string); ok { - fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})") - field["value"] = fValue - fields[j] = field - } - } - attachment["fields"] = fields - list[i] = attachment - } - } - } - post.AddProp("attachments", list) - } -} - -func parseSlackLinksToMarkdown(text string) string { - return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") -} - -func handlePostEvents(c *Context, post *model.Post, triggerWebhooks bool) { - tchan := Srv.Store.Team().Get(c.TeamId) - cchan := Srv.Store.Channel().Get(post.ChannelId, true) - uchan := Srv.Store.User().Get(post.UserId) - - var team *model.Team - if result := <-tchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.team.error"), c.TeamId, result.Err) - return - } else { - team = result.Data.(*model.Team) - } - - var channel *model.Channel - if result := <-cchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.channel.error"), post.ChannelId, result.Err) - return - } else { - channel = result.Data.(*model.Channel) - } - - sendNotifications(c, post, team, channel) - - var user *model.User - if result := <-uchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.user.error"), post.UserId, result.Err) - return - } else { - user = result.Data.(*model.User) - } - - if triggerWebhooks { - go handleWebhookEvents(c, post, team, channel, user) - } - - if channel.Type == model.CHANNEL_DIRECT { - go makeDirectChannelVisible(post.ChannelId) - } -} - -func makeDirectChannelVisible(channelId string) { - var members []model.ChannelMember - if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.get_members.error"), channelId, result.Err.Message) - return - } else { - members = result.Data.([]model.ChannelMember) - } - - if len(members) != 2 { - l4g.Error(utils.T("api.post.make_direct_channel_visible.get_2_members.error"), channelId) - return - } - - // make sure the channel is visible to both members - for i, member := range members { - otherUserId := members[1-i].UserId - - if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil { - // create a new preference since one doesn't exist yet - preference := &model.Preference{ - UserId: member.UserId, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, - Name: otherUserId, - Value: "true", - } - - if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.save_pref.error"), member.UserId, otherUserId, saveResult.Err.Message) - } else { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) - message.Add("preference", preference.ToJson()) - - go Publish(message) - } - } else { - preference := result.Data.(model.Preference) - - if preference.Value != "true" { - // update the existing preference to make the channel visible - preference.Value = "true" - - if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.update_pref.error"), member.UserId, otherUserId, updateResult.Err.Message) - } else { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) - message.Add("preference", preference.ToJson()) - - go Publish(message) - } - } - } - } -} - -func handleWebhookEvents(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) { - if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - return - } - - if channel.Type != model.CHANNEL_OPEN { - return - } - - hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId) - result := <-hchan - if result.Err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.getting.error"), result.Err) - return - } - - hooks := result.Data.([]*model.OutgoingWebhook) - if len(hooks) == 0 { - return - } - - splitWords := strings.Fields(post.Message) - if len(splitWords) == 0 { - return - } - firstWord := splitWords[0] - - relevantHooks := []*model.OutgoingWebhook{} - for _, hook := range hooks { - if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 { - if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 { - relevantHooks = append(relevantHooks, hook) - } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) { - relevantHooks = append(relevantHooks, hook) - } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) { - relevantHooks = append(relevantHooks, hook) - } - } - } - - for _, hook := range relevantHooks { - go func(hook *model.OutgoingWebhook) { - payload := &model.OutgoingWebhookPayload{ - Token: hook.Token, - TeamId: hook.TeamId, - TeamDomain: team.Name, - ChannelId: post.ChannelId, - ChannelName: channel.Name, - Timestamp: post.CreateAt, - UserId: post.UserId, - UserName: user.Username, - PostId: post.Id, - Text: post.Message, - TriggerWord: firstWord, - } - var body io.Reader - var contentType string - if hook.ContentType == "application/json" { - body = strings.NewReader(payload.ToJSON()) - contentType = "application/json" - } else { - body = strings.NewReader(payload.ToFormValues()) - contentType = "application/x-www-form-urlencoded" - } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - client := &http.Client{Transport: tr} - - for _, url := range hook.CallbackURLs { - go func(url string) { - req, _ := http.NewRequest("POST", url, body) - req.Header.Set("Content-Type", contentType) - req.Header.Set("Accept", "application/json") - if resp, err := client.Do(req); err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error()) - } else { - defer func() { - ioutil.ReadAll(resp.Body) - resp.Body.Close() - }() - respProps := model.MapFromJson(resp.Body) - - // copy the context and create a mock session for posting the message - mockSession := model.Session{ - UserId: hook.CreatorId, - TeamMembers: []*model.TeamMember{{TeamId: hook.TeamId, UserId: hook.CreatorId}}, - IsOAuth: false, - } - - newContext := &Context{ - Session: mockSession, - RequestId: model.NewId(), - IpAddress: "", - Path: c.Path, - Err: nil, - teamURLValid: c.teamURLValid, - teamURL: c.teamURL, - siteURL: c.siteURL, - T: c.T, - Locale: c.Locale, - TeamId: hook.TeamId, - } - - if text, ok := respProps["text"]; ok { - if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err) - } - } - } - }(url) - } - - }(hook) - } -} - -// Given a map of user IDs to profiles, returns a list of mention -// keywords for all users in the channel. -func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string { - keywords := make(map[string][]string) - - for id, profile := range profiles { - userMention := "@" + strings.ToLower(profile.Username) - keywords[userMention] = append(keywords[userMention], id) - - 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 { - // note that these are made lower case so that we can do a case insensitive check for them - key := strings.ToLower(k) - keywords[key] = append(keywords[key], id) - } - } - - // If turned on, add the user's case sensitive first name - if profile.NotifyProps["first_name"] == "true" { - keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) - } - - // Add @channel and @all to keywords if user has them turned on - if int64(len(profiles)) < *utils.Cfg.TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" { - keywords["@channel"] = append(keywords["@channel"], profile.Id) - keywords["@all"] = append(keywords["@all"], profile.Id) - } - } - - return keywords -} - -// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned -// users and a slice of potencial mention users not in the channel and whether or not @here was mentioned. -func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) { - mentioned := make(map[string]bool) - potentialOthersMentioned := make([]string, 0) - systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} - hereMentioned := false - allMentioned := false - channelMentioned := false - - addMentionedUsers := func(ids []string) { - for _, id := range ids { - mentioned[id] = true - } - } - - for _, word := range strings.Fields(message) { - isMention := false - - if word == "@here" { - hereMentioned = true - } - - if word == "@channel" { - channelMentioned = true - } - - if word == "@all" { - allMentioned = true - } - - // Non-case-sensitive check for regular keys - if ids, match := keywords[strings.ToLower(word)]; match { - addMentionedUsers(ids) - isMention = true - } - - // Case-sensitive check for first name - if ids, match := keywords[word]; match { - addMentionedUsers(ids) - isMention = true - } - - if !isMention { - // No matches were found with the string split just on whitespace so try further splitting - // the message on punctuation - splitWords := strings.FieldsFunc(word, func(c rune) bool { - return model.SplitRunes[c] - }) - - for _, splitWord := range splitWords { - if splitWord == "@here" { - hereMentioned = true - } - - if splitWord == "@all" { - allMentioned = true - } - - if splitWord == "@channel" { - channelMentioned = true - } - - // Non-case-sensitive check for regular keys - if ids, match := keywords[strings.ToLower(splitWord)]; match { - addMentionedUsers(ids) - } - - // Case-sensitive check for first name - if ids, match := keywords[splitWord]; match { - addMentionedUsers(ids) - } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { - username := word[1:len(splitWord)] - potentialOthersMentioned = append(potentialOthersMentioned, username) - } - } - } - } - - return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned -} - -func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string { - mentionedUsersList := make([]string, 0) - var fchan store.StoreChannel - var senderUsername string - - if post.IsSystemMessage() { - senderUsername = c.T("system.message.name") - } else { - pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) - fchan = Srv.Store.FileInfo().GetForPost(post.Id) - - var profileMap map[string]*model.User - if result := <-pchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) - return nil - } else { - profileMap = result.Data.(map[string]*model.User) - } - - // If the user who made the post is mention don't send a notification - if _, ok := profileMap[post.UserId]; !ok { - l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) - return nil - } - - mentionedUserIds := make(map[string]bool) - allActivityPushUserIds := []string{} - hereNotification := false - channelNotification := false - allNotification := false - updateMentionChans := []store.StoreChannel{} - - if channel.Type == model.CHANNEL_DIRECT { - var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - } else { - otherUserId = userIds[0] - } - - mentionedUserIds[otherUserId] = true - if post.Props["from_webhook"] == "true" { - mentionedUserIds[post.UserId] = true - } - } else { - keywords := getMentionKeywordsInChannel(profileMap) - - var potentialOtherMentions []string - mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = getExplicitMentions(post.Message, keywords) - - // get users that have comment thread mentions enabled - if len(post.RootId) > 0 { - if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) - return nil - } else { - list := result.Data.(*model.PostList) - - for _, threadPost := range list.Posts { - if profile, ok := profileMap[threadPost.UserId]; ok { - if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { - mentionedUserIds[threadPost.UserId] = true - } - } - } - } - } - - // prevent the user from mentioning themselves - if post.Props["from_webhook"] != "true" { - delete(mentionedUserIds, post.UserId) - } - - if len(potentialOtherMentions) > 0 { - if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { - outOfChannelMentions := result.Data.(map[string]*model.User) - go sendOutOfChannelMentions(c, post, outOfChannelMentions) - } - } - - // find which users in the channel are set up to always receive mobile notifications - for _, profile := range profileMap { - if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && - (post.UserId != profile.Id || post.Props["from_webhook"] == "true") { - allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) - } - } - } - - mentionedUsersList = make([]string, 0, len(mentionedUserIds)) - for id := range mentionedUserIds { - mentionedUsersList = append(mentionedUsersList, id) - updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) - } - - var sender *model.User - senderName := make(map[string]string) - for _, id := range mentionedUsersList { - senderName[id] = "" - if profile, ok := profileMap[post.UserId]; ok { - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderName[id] = value.(string) - } else { - //Get the Display name preference from the receiver - if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { - // Show default sender's name if user doesn't set display settings. - senderName[id] = profile.Username - } else { - senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) - } - } - sender = profile - } - } - - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderUsername = value.(string) - } else { - senderUsername = profileMap[post.UserId].Username - } - - if utils.Cfg.EmailSettings.SendEmailNotifications { - for _, id := range mentionedUsersList { - userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" - - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{ - UserId: id, - Status: model.STATUS_OFFLINE, - Manual: false, - LastActivityAt: 0, - ActiveChannel: "", - } - } - - if userAllowsEmails && status.Status != model.STATUS_ONLINE { - sendNotificationEmail(c, post, profileMap[id], channel, team, senderName[id], sender) - } - } - } - - // If the channel has more than 1K users then @here is disabled - if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - hereNotification = false - SendEphemeralPost( - c.TeamId, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: c.T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - // If the channel has more than 1K users then @channel is disabled - if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - SendEphemeralPost( - c.TeamId, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: c.T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - // If the channel has more than 1K users then @all is disabled - if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - SendEphemeralPost( - c.TeamId, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: c.T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - if hereNotification { - statuses := GetAllStatuses() - for _, status := range statuses { - if status.UserId == post.UserId { - continue - } - - _, profileFound := profileMap[status.UserId] - _, alreadyMentioned := mentionedUserIds[status.UserId] - - if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { - mentionedUsersList = append(mentionedUsersList, status.UserId) - updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) - } - } - } - - // Make sure all mention updates are complete to prevent race - // Probably better to batch these DB updates in the future - // MUST be completed before push notifications send - for _, uchan := range updateMentionChans { - if result := <-uchan; result.Err != nil { - l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) - } - } - - sendPushNotifications := false - if *utils.Cfg.EmailSettings.SendPushNotifications { - pushServer := *utils.Cfg.EmailSettings.PushNotificationServer - if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { - l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) - sendPushNotifications = false - } else { - sendPushNotifications = true - } - } - - if sendPushNotifications { - for _, id := range mentionedUsersList { - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} - } - - if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - sendPushNotification(post, profileMap[id], channel, senderName[id], true) - } - } - - for _, id := range allActivityPushUserIds { - if _, ok := mentionedUserIds[id]; !ok { - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} - } - - if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - sendPushNotification(post, profileMap[id], channel, senderName[id], false) - } - } - } - } - } - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) - message.Add("channel_type", channel.Type) - message.Add("channel_display_name", channel.DisplayName) - message.Add("channel_name", channel.Name) - message.Add("sender_name", senderUsername) - message.Add("team_id", team.Id) - - if len(post.FileIds) != 0 && fchan != nil { - message.Add("otherFile", "true") - - var infos []*model.FileInfo - if result := <-fchan; result.Err != nil { - l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) - } else { - infos = result.Data.([]*model.FileInfo) - } - - for _, info := range infos { - if info.IsImage() { - message.Add("image", "true") - break - } - } - } - - if len(mentionedUsersList) != 0 { - message.Add("mentions", model.ArrayToJson(mentionedUsersList)) - } - - Publish(message) - return mentionedUsersList -} - -func sendNotificationEmail(c *Context, post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) { - // skip if inactive - if user.DeleteAt > 0 { - return - } - - if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id { - // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link - if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.get_teams.error"), user.Id, result.Err) - return - } else { - // if the recipient isn't in the current user's team, just pick one - teams := result.Data.([]*model.Team) - found := false - - for i := range teams { - if teams[i].Id == team.Id { - found = true - team = teams[i] - break - } - } - - if !found { - if len(teams) > 0 { - team = teams[0] - } else { - // in case the user hasn't joined any teams we send them to the select_team page - team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName} - } - } - } - } - - if *utils.Cfg.EmailSettings.EnableEmailBatching { - var sendBatched bool - - if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil { - // if the call fails, assume it hasn't been set and use the default - sendBatched = false - } else { - // default to not using batching if the setting is set to immediate - sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL - } - - if sendBatched { - if err := AddNotificationEmailToBatch(user, post, team); err == nil { - return - } - } - - // fall back to sending a single email if we can't batch it for some reason - } - - var channelName string - var bodyText string - var subjectText string - var mailTemplate string - var mailParameters map[string]interface{} - - teamURL := c.GetSiteURL() + "/" + team.Name - tm := time.Unix(post.CreateAt/1000, 0) - - userLocale := utils.GetUserTranslations(user.Locale) - month := userLocale(tm.Month().String()) - day := fmt.Sprintf("%d", tm.Day()) - year := fmt.Sprintf("%d", tm.Year()) - zone, _ := tm.Zone() - - 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") - - senderDisplayName := senderName - - mailTemplate = "api.templates.post_subject_in_direct_message" - mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year} - } else { - bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") - subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") - channelName = channel.DisplayName - mailTemplate = "api.templates.post_subject_in_channel" - mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "ChannelName": channelName, "Month": month, "Day": day, "Year": year} - } - - subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters)) - - bodyPage := utils.NewHTMLTemplate("post_body", user.Locale) - bodyPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage.Props["PostMessage"] = getMessageForNotification(post, userLocale) - if team.Name != "select_team" { - bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id - } else { - bodyPage.Props["TeamLink"] = teamURL - } - - 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})) - - if err := utils.SendMail(user.Email, html.UnescapeString(subject), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), user.Email, err) - } - - if einterfaces.GetMetricsInterface() != nil { - einterfaces.GetMetricsInterface().IncrementPostSentEmail() - } -} - -func getMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { - if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { - return post.Message - } - - // extract the filenames from their paths and determine what type of files are attached - var infos []*model.FileInfo - if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { - l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err) - } else { - infos = result.Data.([]*model.FileInfo) - } - - filenames := make([]string, len(infos)) - onlyImages := true - for i, info := range infos { - if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { - // this should never error since filepath was escaped using url.QueryEscape - filenames[i] = escaped - } else { - filenames[i] = info.Name - } - - onlyImages = onlyImages && info.IsImage() - } - - props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} - - if onlyImages { - return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) - } else { - return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) - } -} - -func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) { - sessions := getMobileAppSessions(user.Id) - - if sessions == nil { - return - } - - var channelName string - - if channel.Type == model.CHANNEL_DIRECT { - channelName = senderName - } else { - channelName = channel.DisplayName - } - - userLocale := utils.GetUserTranslations(user.Locale) - - msg := model.PushNotification{} - if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil { - msg.Badge = 1 - l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err) - } else { - msg.Badge = int(badge.Data.(int64)) - } - msg.Type = model.PUSH_TYPE_MESSAGE - msg.TeamId = channel.TeamId - msg.ChannelId = channel.Id - msg.ChannelName = channel.Name - - if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION { - if channel.Type == model.CHANNEL_DIRECT { - msg.Category = model.CATEGORY_DM - msg.Message = "@" + senderName + ": " + model.ClearMentionTags(post.Message) - } else { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message) - } - } else { - if channel.Type == model.CHANNEL_DIRECT { - msg.Category = model.CATEGORY_DM - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") - } else if wasMentioned { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName - } else { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName - } - } - - l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) - - for _, session := range sessions { - tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) - tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) - sendToPushProxy(tmpMessage) - if einterfaces.GetMetricsInterface() != nil { - einterfaces.GetMetricsInterface().IncrementPostSentPush() - } - } -} - -func clearPushNotification(userId string, channelId string) { - sessions := getMobileAppSessions(userId) - if sessions == nil { - return - } - - msg := model.PushNotification{} - msg.Type = model.PUSH_TYPE_CLEAR - msg.ChannelId = channelId - msg.ContentAvailable = 0 - if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil { - msg.Badge = 0 - l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err) - } else { - msg.Badge = int(badge.Data.(int64)) - } - - l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) - for _, session := range sessions { - tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) - tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) - sendToPushProxy(tmpMessage) - } -} - -func sendToPushProxy(msg model.PushNotification) { - msg.ServerId = utils.CfgDiagnosticId - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - httpClient := &http.Client{Transport: tr} - request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) - - if resp, err := httpClient.Do(request); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), msg.DeviceId, err) - } else { - ioutil.ReadAll(resp.Body) - resp.Body.Close() - } -} - -func getMobileAppSessions(userId string) []*model.Session { - if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), userId, result.Err) - return nil - } else { - return result.Data.([]*model.Session) - } -} - -func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User) { - if len(profiles) == 0 { - return - } - - var usernames []string - for _, user := range profiles { - usernames = append(usernames, 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.TeamId, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: message, - CreateAt: post.CreateAt + 1, - }, - ) -} - -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{} - } - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) - message.Add("post", post.ToJson()) - - go Publish(message) -} - func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) @@ -1212,7 +98,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(post.Id) + pchan := app.Srv.Store.Post().Get(post.Id) if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) { return @@ -1258,7 +144,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { newPost.EditAt = model.GetMillis() newPost.Hashtags, _ = model.ParseHashtags(post.Message) - if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { + if result := <-app.Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { c.Err = result.Err return } else { @@ -1267,9 +153,9 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil) message.Add("post", rpost.ToJson()) - go Publish(message) + go app.Publish(message) - InvalidateCacheForChannelPosts(rpost.ChannelId) + app.InvalidateCacheForChannelPosts(rpost.ChannelId) w.Write([]byte(rpost.ToJson())) } @@ -1292,7 +178,7 @@ func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) { posts := &model.PostList{} - if result := <-Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1323,7 +209,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - etagChan := Srv.Store.Post().GetEtag(id, true) + etagChan := app.Srv.Store.Post().GetEtag(id, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_CREATE_POST) { return @@ -1335,7 +221,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().GetPosts(id, offset, limit, true) + pchan := app.Srv.Store.Post().GetPosts(id, offset, limit, true) if result := <-pchan; result.Err != nil { c.Err = result.Err @@ -1364,7 +250,7 @@ func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().GetPostsSince(id, time, true) + pchan := app.Srv.Store.Post().GetPostsSince(id, time, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) { return @@ -1396,7 +282,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) + pchan := app.Srv.Store.Post().Get(postId) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return @@ -1430,7 +316,7 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Post().Get(postId); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(postId); result.Err != nil { c.Err = result.Err return } else { @@ -1464,7 +350,7 @@ func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Post().Get(postId); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(postId); result.Err != nil { c.Err = result.Err return } else { @@ -1515,7 +401,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) + pchan := app.Srv.Store.Post().Get(postId) if result := <-pchan; result.Err != nil { c.Err = result.Err @@ -1541,7 +427,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil { + if dresult := <-app.Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil { c.Err = dresult.Err return } @@ -1549,11 +435,11 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) message.Add("post", post.ToJson()) - go Publish(message) + go app.Publish(message) go DeletePostFiles(post) go DeleteFlaggedPost(c.Session.UserId, post) - InvalidateCacheForChannelPosts(post.ChannelId) + app.InvalidateCacheForChannelPosts(post.ChannelId) result := make(map[string]string) result["id"] = postId @@ -1562,7 +448,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } func DeleteFlaggedPost(userId string, post *model.Post) { - if result := <-Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id); result.Err != nil { + if result := <-app.Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id); result.Err != nil { l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err) return } @@ -1573,7 +459,7 @@ func DeletePostFiles(post *model.Post) { return } - if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil { l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err) } } @@ -1614,7 +500,7 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b } // We can do better than this etag in this situation - etagChan := Srv.Store.Post().GetEtag(id, true) + etagChan := app.Srv.Store.Post().GetEtag(id, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) { return @@ -1627,9 +513,9 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b var pchan store.StoreChannel if before { - pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset) + pchan = app.Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset) } else { - pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset) + pchan = app.Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset) } if result := <-pchan; result.Err != nil { @@ -1664,7 +550,7 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { params.OrTerms = isOrSearch // don't allow users to search for everything if params.Terms != "*" { - channels = append(channels, Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params)) + channels = append(channels, app.Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params)) } } @@ -1698,8 +584,8 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) - fchan := Srv.Store.FileInfo().GetForPost(postId) + pchan := app.Srv.Store.Post().Get(postId) + fchan := app.Srv.Store.FileInfo().GetForPost(postId) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return |