* Slash commands accept Slack format Until this commit the slash commands only accepted 'text' properties. For better styling however, Slack formatting support was added. However, ephemeral messages are not supported, and only text will be displayed. * Allow emphemeral Slack messages
1739 строки
52 KiB
Go
1739 строки
52 KiB
Go
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
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/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() {
|
|
l4g.Debug(utils.T("api.post.init.debug"))
|
|
|
|
BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequiredActivity(searchPosts, true)).Methods("POST")
|
|
BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getFlaggedPosts)).Methods("GET")
|
|
BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
|
|
BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).Methods("GET")
|
|
|
|
BaseRoutes.Posts.Handle("/create", ApiUserRequiredActivity(createPost, true)).Methods("POST")
|
|
BaseRoutes.Posts.Handle("/update", ApiUserRequiredActivity(updatePost, true)).Methods("POST")
|
|
BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getPosts)).Methods("GET")
|
|
BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequired(getPostsSince)).Methods("GET")
|
|
|
|
BaseRoutes.NeedPost.Handle("/get", ApiUserRequired(getPost)).Methods("GET")
|
|
BaseRoutes.NeedPost.Handle("/delete", ApiUserRequiredActivity(deletePost, true)).Methods("POST")
|
|
BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
|
|
BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
|
|
BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET")
|
|
}
|
|
|
|
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
post := model.PostFromJson(r.Body)
|
|
if post == nil {
|
|
c.SetInvalidParam("createPost", "post")
|
|
return
|
|
}
|
|
post.UserId = c.Session.UserId
|
|
|
|
cchan := Srv.Store.Channel().Get(post.ChannelId, true)
|
|
|
|
if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_CREATE_POST) {
|
|
return
|
|
}
|
|
|
|
// Check that channel has not been deleted
|
|
var channel *model.Channel
|
|
if result := <-cchan; result.Err != nil {
|
|
c.SetInvalidParam("createPost", "post.channelId")
|
|
return
|
|
} else {
|
|
channel = result.Data.(*model.Channel)
|
|
}
|
|
|
|
if channel.DeleteAt != 0 {
|
|
c.Err = model.NewLocAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "")
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if rp, err := CreatePost(c, post, true); err != nil {
|
|
c.Err = err
|
|
|
|
if c.Err.Id == "api.post.create_post.root_id.app_error" ||
|
|
c.Err.Id == "api.post.create_post.channel_root_id.app_error" ||
|
|
c.Err.Id == "api.post.create_post.parent_id.app_error" {
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
}
|
|
|
|
return
|
|
} 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 {
|
|
l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, c.Session.UserId, result.Err)
|
|
}
|
|
}
|
|
|
|
w.Write([]byte(rp.ToJson()))
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
if len(props) > 0 {
|
|
for key, val := range props {
|
|
if key == "attachments" {
|
|
createSlackPost(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 = response.Text
|
|
post.UserId = c.Session.UserId
|
|
post.CreateAt = model.GetMillis()
|
|
|
|
if response.Attachments != nil {
|
|
createSlackPost(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 createSlackPost(post *model.Post, attachments interface{}) {
|
|
post.Type = model.POST_SLACK_ATTACHMENT
|
|
|
|
// parse links into Markdown format
|
|
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
|
post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})")
|
|
|
|
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 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 {
|
|
if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
|
|
l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err)
|
|
return nil
|
|
} else {
|
|
statuses := result.Data.([]*model.Status)
|
|
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
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found && 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)
|
|
|
|
if post == nil {
|
|
c.SetInvalidParam("updatePost", "post")
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().Get(post.Id)
|
|
|
|
if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) {
|
|
return
|
|
}
|
|
|
|
var oldPost *model.Post
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
oldPost = result.Data.(*model.PostList).Posts[post.Id]
|
|
|
|
if oldPost == nil {
|
|
c.Err = model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if oldPost.UserId != c.Session.UserId {
|
|
c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
|
|
if oldPost.DeleteAt != 0 {
|
|
c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil,
|
|
c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}))
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
|
|
if oldPost.IsSystemMessage() {
|
|
c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
}
|
|
|
|
newPost := &model.Post{}
|
|
*newPost = *oldPost
|
|
|
|
newPost.Message = post.Message
|
|
newPost.Hashtags, _ = model.ParseHashtags(post.Message)
|
|
|
|
if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
rpost := result.Data.(*model.Post)
|
|
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
|
|
message.Add("post", rpost.ToJson())
|
|
|
|
go Publish(message)
|
|
|
|
InvalidateCacheForChannelPosts(rpost.ChannelId)
|
|
|
|
w.Write([]byte(rpost.ToJson()))
|
|
}
|
|
}
|
|
|
|
func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
offset, err := strconv.Atoi(params["offset"])
|
|
if err != nil {
|
|
c.SetInvalidParam("getFlaggedPosts", "offset")
|
|
return
|
|
}
|
|
|
|
limit, err := strconv.Atoi(params["limit"])
|
|
if err != nil {
|
|
c.SetInvalidParam("getFlaggedPosts", "limit")
|
|
return
|
|
}
|
|
|
|
posts := &model.PostList{}
|
|
|
|
if result := <-Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
posts = result.Data.(*model.PostList)
|
|
}
|
|
|
|
w.Write([]byte(posts.ToJson()))
|
|
}
|
|
|
|
func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
id := params["channel_id"]
|
|
if len(id) != 26 {
|
|
c.SetInvalidParam("getPosts", "channelId")
|
|
return
|
|
}
|
|
|
|
offset, err := strconv.Atoi(params["offset"])
|
|
if err != nil {
|
|
c.SetInvalidParam("getPosts", "offset")
|
|
return
|
|
}
|
|
|
|
limit, err := strconv.Atoi(params["limit"])
|
|
if err != nil {
|
|
c.SetInvalidParam("getPosts", "limit")
|
|
return
|
|
}
|
|
|
|
etagChan := Srv.Store.Post().GetEtag(id, true)
|
|
|
|
if !HasPermissionToChannelContext(c, id, model.PERMISSION_CREATE_POST) {
|
|
return
|
|
}
|
|
|
|
etag := (<-etagChan).Data.(string)
|
|
|
|
if HandleEtag(etag, "Get Posts", w, r) {
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().GetPosts(id, offset, limit, true)
|
|
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
|
|
}
|
|
|
|
func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
id := params["channel_id"]
|
|
if len(id) != 26 {
|
|
c.SetInvalidParam("getPostsSince", "channelId")
|
|
return
|
|
}
|
|
|
|
time, err := strconv.ParseInt(params["time"], 10, 64)
|
|
if err != nil {
|
|
c.SetInvalidParam("getPostsSince", "time")
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().GetPostsSince(id, time, true)
|
|
|
|
if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) {
|
|
return
|
|
}
|
|
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
|
|
}
|
|
|
|
func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
channelId := params["channel_id"]
|
|
if len(channelId) != 26 {
|
|
c.SetInvalidParam("getPost", "channelId")
|
|
return
|
|
}
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("getPost", "postId")
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().Get(postId)
|
|
|
|
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
|
|
return
|
|
}
|
|
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else if HandleEtag(result.Data.(*model.PostList).Etag(), "Get Post", w, r) {
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
if !list.IsChannelId(channelId) {
|
|
c.Err = model.NewLocAppError("getPost", "api.post.get_post.permissions.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
}
|
|
|
|
func getPostById(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("getPostById", "postId")
|
|
return
|
|
}
|
|
|
|
if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
if len(list.Order) != 1 {
|
|
c.Err = model.NewLocAppError("getPostById", "api.post_get_post_by_id.get.app_error", nil, "")
|
|
return
|
|
}
|
|
post := list.Posts[list.Order[0]]
|
|
|
|
if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_READ_CHANNEL) {
|
|
return
|
|
}
|
|
|
|
if HandleEtag(list.Etag(), "Get Post By Id", w, r) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
}
|
|
|
|
func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("getPermalinkTmp", "postId")
|
|
return
|
|
}
|
|
|
|
if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
if len(list.Order) != 1 {
|
|
c.Err = model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "")
|
|
return
|
|
}
|
|
post := list.Posts[list.Order[0]]
|
|
|
|
// Because we confuse permissions and membership in Mattermost's model, we have to just
|
|
// try to join the channel without checking if we already have permission to it. This is
|
|
// because system admins have permissions to every channel but are not nessisary a member
|
|
// of every channel. If we checked here then system admins would skip joining the channel and
|
|
// error when they tried to view it.
|
|
if err, _ := JoinChannelById(c, c.Session.UserId, post.ChannelId); err != nil {
|
|
// On error just return with permissions error
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if HandleEtag(list.Etag(), "Get Permalink TMP", w, r) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
}
|
|
|
|
func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
channelId := params["channel_id"]
|
|
if len(channelId) != 26 {
|
|
c.SetInvalidParam("deletePost", "channelId")
|
|
return
|
|
}
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("deletePost", "postId")
|
|
return
|
|
}
|
|
|
|
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_EDIT_POST) {
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().Get(postId)
|
|
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
|
|
post := result.Data.(*model.PostList).Posts[postId]
|
|
|
|
if post == nil {
|
|
c.SetInvalidParam("deletePost", "postId")
|
|
return
|
|
}
|
|
|
|
if post.ChannelId != channelId {
|
|
c.Err = model.NewLocAppError("deletePost", "api.post.delete_post.permissions.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
|
|
if post.UserId != c.Session.UserId && !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_OTHERS_POSTS) {
|
|
c.Err = model.NewLocAppError("deletePost", "api.post.delete_post.permissions.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusForbidden
|
|
return
|
|
}
|
|
|
|
if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil {
|
|
c.Err = dresult.Err
|
|
return
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
|
|
message.Add("post", post.ToJson())
|
|
|
|
go Publish(message)
|
|
go DeletePostFiles(post)
|
|
go DeleteFlaggedPost(c.Session.UserId, post)
|
|
|
|
InvalidateCacheForChannelPosts(post.ChannelId)
|
|
|
|
result := make(map[string]string)
|
|
result["id"] = postId
|
|
w.Write([]byte(model.MapToJson(result)))
|
|
}
|
|
}
|
|
|
|
func DeleteFlaggedPost(userId string, post *model.Post) {
|
|
if result := <-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
|
|
}
|
|
}
|
|
|
|
func DeletePostFiles(post *model.Post) {
|
|
if len(post.FileIds) != 0 {
|
|
return
|
|
}
|
|
|
|
if result := <-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)
|
|
}
|
|
}
|
|
|
|
func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
getPostsBeforeOrAfter(c, w, r, true)
|
|
}
|
|
|
|
func getPostsAfter(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
getPostsBeforeOrAfter(c, w, r, false)
|
|
}
|
|
|
|
func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, before bool) {
|
|
params := mux.Vars(r)
|
|
|
|
id := params["channel_id"]
|
|
if len(id) != 26 {
|
|
c.SetInvalidParam("getPostsBeforeOrAfter", "channelId")
|
|
return
|
|
}
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("getPostsBeforeOrAfter", "postId")
|
|
return
|
|
}
|
|
|
|
numPosts, err := strconv.Atoi(params["num_posts"])
|
|
if err != nil || numPosts <= 0 {
|
|
c.SetInvalidParam("getPostsBeforeOrAfter", "numPosts")
|
|
return
|
|
}
|
|
|
|
offset, err := strconv.Atoi(params["offset"])
|
|
if err != nil || offset < 0 {
|
|
c.SetInvalidParam("getPostsBeforeOrAfter", "offset")
|
|
return
|
|
}
|
|
|
|
// We can do better than this etag in this situation
|
|
etagChan := Srv.Store.Post().GetEtag(id, true)
|
|
|
|
if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) {
|
|
return
|
|
}
|
|
|
|
etag := (<-etagChan).Data.(string)
|
|
if HandleEtag(etag, "Get Posts Before or After", w, r) {
|
|
return
|
|
}
|
|
|
|
var pchan store.StoreChannel
|
|
if before {
|
|
pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset)
|
|
} else {
|
|
pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset)
|
|
}
|
|
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
list := result.Data.(*model.PostList)
|
|
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
|
w.Write([]byte(list.ToJson()))
|
|
}
|
|
}
|
|
|
|
func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
props := model.StringInterfaceFromJson(r.Body)
|
|
|
|
terms := props["terms"].(string)
|
|
if len(terms) == 0 {
|
|
c.SetInvalidParam("search", "terms")
|
|
return
|
|
}
|
|
|
|
isOrSearch := false
|
|
if val, ok := props["is_or_search"]; ok && val != nil {
|
|
isOrSearch = val.(bool)
|
|
}
|
|
|
|
paramsList := model.ParseSearchParams(terms)
|
|
channels := []store.StoreChannel{}
|
|
|
|
for _, params := range paramsList {
|
|
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))
|
|
}
|
|
}
|
|
|
|
posts := &model.PostList{}
|
|
for _, channel := range channels {
|
|
if result := <-channel; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
data := result.Data.(*model.PostList)
|
|
posts.Extend(data)
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Write([]byte(posts.ToJson()))
|
|
}
|
|
|
|
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
params := mux.Vars(r)
|
|
|
|
channelId := params["channel_id"]
|
|
if len(channelId) != 26 {
|
|
c.SetInvalidParam("getFileInfosForPost", "channelId")
|
|
return
|
|
}
|
|
|
|
postId := params["post_id"]
|
|
if len(postId) != 26 {
|
|
c.SetInvalidParam("getFileInfosForPost", "postId")
|
|
return
|
|
}
|
|
|
|
pchan := Srv.Store.Post().Get(postId)
|
|
fchan := Srv.Store.FileInfo().GetForPost(postId)
|
|
|
|
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
|
|
return
|
|
}
|
|
|
|
var infos []*model.FileInfo
|
|
if result := <-fchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
infos = result.Data.([]*model.FileInfo)
|
|
}
|
|
|
|
if len(infos) == 0 {
|
|
// No FileInfos were returned so check if they need to be created for this post
|
|
var post *model.Post
|
|
if result := <-pchan; result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
post = result.Data.(*model.PostList).Posts[postId]
|
|
}
|
|
|
|
if len(post.Filenames) > 0 {
|
|
// The post has Filenames that need to be replaced with FileInfos
|
|
infos = migrateFilenamesToFileInfos(post)
|
|
}
|
|
}
|
|
|
|
etag := model.GetEtagForFileInfos(infos)
|
|
|
|
if HandleEtag(etag, "Get File Infos For Post", w, r) {
|
|
return
|
|
} else {
|
|
w.Header().Set("Cache-Control", "max-age=2592000, public")
|
|
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
|
w.Write([]byte(model.FileInfosToJson(infos)))
|
|
}
|
|
}
|