* MM-30882: Fix read-after-write issue for demoting user In (*App).DemoteUserToGuest, we would demote a user, and then immediately read it back to do future operations from the user. This reading back of the user had the effect of sticking the old value into the cache after which it would never be updated. There was another issue along with this, which was when the invalidation message would broadcast across the cluster, it would hit the cache invalidation problem where an unrelated store call would miss the cache because it was invalidated, and then again read from replica and stick the old value. To fix all these, we return the new value directly from the store method to avoid having the app to read it again. And we add a map in the localcache layer which tracks invalidations made, and then switch to use master if it's true. The core change is fairly limited, but due to changing the store method signatures, a lot of code needed to be updated to pass "context.Background". Therefore the PR just "appears" to be big, but the main changes are limited to app/user.go, sqlstore/user_store.go and user_layer.go https://mattermost.atlassian.net/browse/MM-30882 ```release-note Fix an issue where demoting a user to guest would not take effect in an environment with read replicas. ``` * Fix concurrent map access * Fixing mistakes * fix tests
862 строки
32 KiB
Go
862 строки
32 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
"github.com/mattermost/mattermost-server/v5/store"
|
|
"github.com/mattermost/mattermost-server/v5/utils"
|
|
)
|
|
|
|
const (
|
|
TriggerwordsExactMatch = 0
|
|
TriggerwordsStartsWith = 1
|
|
|
|
MaxIntegrationResponseSize = 1024 * 1024 // Posts can be <100KB at most, so this is likely more than enough
|
|
)
|
|
|
|
func (a *App) handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil
|
|
}
|
|
|
|
if channel.Type != model.CHANNEL_OPEN {
|
|
return nil
|
|
}
|
|
|
|
hooks, err := a.Srv().Store.Webhook().GetOutgoingByTeam(team.Id, -1, -1)
|
|
if err != nil {
|
|
return model.NewAppError("handleWebhookEvents", "app.webhooks.get_outgoing_by_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if len(hooks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var firstWord, triggerWord string
|
|
|
|
splitWords := strings.Fields(post.Message)
|
|
if len(splitWords) > 0 {
|
|
firstWord = splitWords[0]
|
|
}
|
|
|
|
relevantHooks := []*model.OutgoingWebhook{}
|
|
for _, hook := range hooks {
|
|
if hook.ChannelId == post.ChannelId || hook.ChannelId == "" {
|
|
if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
|
|
relevantHooks = append(relevantHooks, hook)
|
|
triggerWord = ""
|
|
} else if hook.TriggerWhen == TriggerwordsExactMatch && hook.TriggerWordExactMatch(firstWord) {
|
|
relevantHooks = append(relevantHooks, hook)
|
|
triggerWord = hook.GetTriggerWord(firstWord, true)
|
|
} else if hook.TriggerWhen == TriggerwordsStartsWith && hook.TriggerWordStartsWith(firstWord) {
|
|
relevantHooks = append(relevantHooks, hook)
|
|
triggerWord = hook.GetTriggerWord(firstWord, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, hook := range relevantHooks {
|
|
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: triggerWord,
|
|
FileIds: strings.Join(post.FileIds, ","),
|
|
}
|
|
a.Srv().Go(func(hook *model.OutgoingWebhook) func() {
|
|
return func() {
|
|
a.TriggerWebhook(payload, hook, post, channel)
|
|
}
|
|
}(hook))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
|
|
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"
|
|
}
|
|
|
|
for i := range hook.CallbackURLs {
|
|
// Get the callback URL by index to properly capture it for the go func
|
|
url := hook.CallbackURLs[i]
|
|
|
|
a.Srv().Go(func() {
|
|
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType)
|
|
if err != nil {
|
|
mlog.Error("Event POST failed.", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
if webhookResp != nil && (webhookResp.Text != nil || len(webhookResp.Attachments) > 0) {
|
|
postRootId := ""
|
|
if webhookResp.ResponseType == model.OUTGOING_HOOK_RESPONSE_TYPE_COMMENT {
|
|
postRootId = post.Id
|
|
}
|
|
if len(webhookResp.Props) == 0 {
|
|
webhookResp.Props = make(model.StringInterface)
|
|
}
|
|
webhookResp.Props["webhook_display_name"] = hook.DisplayName
|
|
|
|
text := ""
|
|
if webhookResp.Text != nil {
|
|
text = a.ProcessSlackText(*webhookResp.Text)
|
|
}
|
|
webhookResp.Attachments = a.ProcessSlackAttachments(webhookResp.Attachments)
|
|
// attachments is in here for slack compatibility
|
|
if len(webhookResp.Attachments) > 0 {
|
|
webhookResp.Props["attachments"] = webhookResp.Attachments
|
|
}
|
|
if *a.Config().ServiceSettings.EnablePostUsernameOverride && hook.Username != "" && webhookResp.Username == "" {
|
|
webhookResp.Username = hook.Username
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnablePostIconOverride && hook.IconURL != "" && webhookResp.IconURL == "" {
|
|
webhookResp.IconURL = hook.IconURL
|
|
}
|
|
if _, err := a.CreateWebhookPost(hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, "", webhookResp.Props, webhookResp.Type, postRootId); err != nil {
|
|
mlog.Error("Failed to create response post.", mlog.Err(err))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string) (*model.OutgoingWebhookResponse, error) {
|
|
req, err := http.NewRequest("POST", url, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := a.HTTPService().MakeClient(false).Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return model.OutgoingWebhookResponseFromJson(io.LimitReader(resp.Body, MaxIntegrationResponseSize))
|
|
}
|
|
|
|
func SplitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model.AppError) {
|
|
splits := make([]*model.Post, 0)
|
|
remainingText := post.Message
|
|
|
|
base := post.Clone()
|
|
base.Message = ""
|
|
base.SetProps(make(map[string]interface{}))
|
|
for k, v := range post.GetProps() {
|
|
if k != "attachments" {
|
|
base.AddProp(k, v)
|
|
}
|
|
}
|
|
|
|
if utf8.RuneCountInString(model.StringInterfaceToJson(base.GetProps())) > model.POST_PROPS_MAX_USER_RUNES {
|
|
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]interface{}{"Max": model.POST_PROPS_MAX_USER_RUNES}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
for utf8.RuneCountInString(remainingText) > maxPostSize {
|
|
split := base.Clone()
|
|
x := 0
|
|
for index := range remainingText {
|
|
x++
|
|
if x > maxPostSize {
|
|
split.Message = remainingText[:index]
|
|
remainingText = remainingText[index:]
|
|
break
|
|
}
|
|
}
|
|
splits = append(splits, split)
|
|
}
|
|
|
|
split := base.Clone()
|
|
split.Message = remainingText
|
|
splits = append(splits, split)
|
|
|
|
attachments, _ := post.GetProp("attachments").([]*model.SlackAttachment)
|
|
for _, attachment := range attachments {
|
|
newAttachment := *attachment
|
|
for {
|
|
lastSplit := splits[len(splits)-1]
|
|
newProps := make(map[string]interface{})
|
|
for k, v := range lastSplit.GetProps() {
|
|
newProps[k] = v
|
|
}
|
|
origAttachments, _ := newProps["attachments"].([]*model.SlackAttachment)
|
|
newProps["attachments"] = append(origAttachments, &newAttachment)
|
|
newPropsString := model.StringInterfaceToJson(newProps)
|
|
runeCount := utf8.RuneCountInString(newPropsString)
|
|
|
|
if runeCount <= model.POST_PROPS_MAX_USER_RUNES {
|
|
lastSplit.SetProps(newProps)
|
|
break
|
|
}
|
|
|
|
if len(origAttachments) > 0 {
|
|
newSplit := base.Clone()
|
|
splits = append(splits, newSplit)
|
|
continue
|
|
}
|
|
|
|
truncationNeeded := runeCount - model.POST_PROPS_MAX_USER_RUNES
|
|
textRuneCount := utf8.RuneCountInString(attachment.Text)
|
|
if textRuneCount < truncationNeeded {
|
|
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]interface{}{"Max": model.POST_PROPS_MAX_USER_RUNES}, "", http.StatusBadRequest)
|
|
}
|
|
x := 0
|
|
for index := range attachment.Text {
|
|
x++
|
|
if x > textRuneCount-truncationNeeded {
|
|
newAttachment.Text = newAttachment.Text[:index]
|
|
break
|
|
}
|
|
}
|
|
lastSplit.SetProps(newProps)
|
|
break
|
|
}
|
|
}
|
|
|
|
return splits, nil
|
|
}
|
|
|
|
func (a *App) CreateWebhookPost(userID string, channel *model.Channel, text, overrideUsername, overrideIconURL, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
|
|
// parse links into Markdown format
|
|
linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`)
|
|
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
|
|
|
post := &model.Post{UserId: userID, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId}
|
|
post.AddProp("from_webhook", "true")
|
|
|
|
if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) {
|
|
err := model.NewAppError("CreateWebhookPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest)
|
|
return nil, err
|
|
}
|
|
|
|
if metrics := a.Metrics(); metrics != nil {
|
|
metrics.IncrementWebhookPost()
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
|
|
if overrideUsername != "" {
|
|
post.AddProp("override_username", overrideUsername)
|
|
} else {
|
|
post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnablePostIconOverride {
|
|
if overrideIconURL != "" {
|
|
post.AddProp("override_icon_url", overrideIconURL)
|
|
}
|
|
if overrideIconEmoji != "" {
|
|
post.AddProp("override_icon_emoji", overrideIconEmoji)
|
|
}
|
|
}
|
|
|
|
if len(props) > 0 {
|
|
for key, val := range props {
|
|
if key == "attachments" {
|
|
if attachments, success := val.([]*model.SlackAttachment); success {
|
|
model.ParseSlackAttachment(post, attachments)
|
|
}
|
|
} else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
|
|
post.AddProp(key, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
splits, err := SplitWebhookPost(post, a.MaxPostSize())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, split := range splits {
|
|
if _, err := a.CreatePostMissingChannel(split, false); err != nil {
|
|
return nil, model.NewAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return splits[0], nil
|
|
}
|
|
|
|
func (a *App) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
hook.UserId = creatorId
|
|
hook.TeamId = channel.TeamId
|
|
|
|
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
|
|
hook.Username = ""
|
|
}
|
|
if !*a.Config().ServiceSettings.EnablePostIconOverride {
|
|
hook.IconURL = ""
|
|
}
|
|
|
|
if hook.Username != "" && !model.IsValidUsername(hook.Username) {
|
|
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
webhook, err := a.Srv().Store.Webhook().SaveIncoming(hook)
|
|
if err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
|
|
default:
|
|
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) UpdateIncomingWebhook(oldHook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
|
|
updatedHook.Username = oldHook.Username
|
|
}
|
|
if !*a.Config().ServiceSettings.EnablePostIconOverride {
|
|
updatedHook.IconURL = oldHook.IconURL
|
|
}
|
|
|
|
if updatedHook.Username != "" && !model.IsValidUsername(updatedHook.Username) {
|
|
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
updatedHook.Id = oldHook.Id
|
|
updatedHook.UserId = oldHook.UserId
|
|
updatedHook.CreateAt = oldHook.CreateAt
|
|
updatedHook.UpdateAt = model.GetMillis()
|
|
updatedHook.TeamId = oldHook.TeamId
|
|
updatedHook.DeleteAt = oldHook.DeleteAt
|
|
|
|
newWebhook, err := a.Srv().Store.Webhook().UpdateIncoming(updatedHook)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateIncomingWebhook", "app.webhooks.update_incoming.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
a.invalidateCacheForWebhook(oldHook.Id)
|
|
return newWebhook, nil
|
|
}
|
|
|
|
func (a *App) DeleteIncomingWebhook(hookID string) *model.AppError {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return model.NewAppError("DeleteIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if err := a.Srv().Store.Webhook().DeleteIncoming(hookID, model.GetMillis()); err != nil {
|
|
return model.NewAppError("DeleteIncomingWebhook", "app.webhooks.delete_incoming.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.invalidateCacheForWebhook(hookID)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return nil, model.NewAppError("GetIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhook, err := a.Srv().Store.Webhook().GetIncoming(hookID, true)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
|
return a.GetIncomingWebhooksForTeamPageByUser(teamID, "", page, perPage)
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhooks, err := a.Srv().Store.Webhook().GetIncomingByTeamByUser(teamID, userID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "app.webhooks.get_incoming_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksPageByUser(userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhooks, err := a.Srv().Store.Webhook().GetIncomingListByUser(userID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "app.webhooks.get_incoming_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
|
return a.GetIncomingWebhooksPageByUser("", page, perPage)
|
|
}
|
|
|
|
func (a *App) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if hook.ChannelId != "" {
|
|
channel, errCh := a.Srv().Store.Channel().Get(hook.ChannelId, true)
|
|
if errCh != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(errCh, &nfErr):
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.find.app_error", nil, errCh.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if channel.Type != model.CHANNEL_OPEN {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if channel.Type != model.CHANNEL_OPEN || channel.TeamId != hook.TeamId {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
} else if len(hook.TriggerWords) == 0 {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
allHooks, err := a.Srv().Store.Webhook().GetOutgoingByTeam(hook.TeamId, -1, -1)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
for _, existingOutHook := range allHooks {
|
|
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, hook.CallbackURLs)
|
|
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, hook.TriggerWords)
|
|
|
|
if existingOutHook.ChannelId == hook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.intersect.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
webhook, err := a.Srv().Store.Webhook().SaveOutgoing(hook)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.override.app_error", nil, invErr.Error(), http.StatusBadRequest)
|
|
default:
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) UpdateOutgoingWebhook(oldHook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if updatedHook.ChannelId != "" {
|
|
channel, err := a.GetChannel(updatedHook.ChannelId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if channel.Type != model.CHANNEL_OPEN {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.not_open.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if channel.TeamId != oldHook.TeamId {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
} else if len(updatedHook.TriggerWords) == 0 {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
allHooks, err := a.Srv().Store.Webhook().GetOutgoingByTeam(oldHook.TeamId, -1, -1)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
for _, existingOutHook := range allHooks {
|
|
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, updatedHook.CallbackURLs)
|
|
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, updatedHook.TriggerWords)
|
|
|
|
if existingOutHook.ChannelId == updatedHook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 && existingOutHook.Id != updatedHook.Id {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.update_outgoing.intersect.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
updatedHook.CreatorId = oldHook.CreatorId
|
|
updatedHook.CreateAt = oldHook.CreateAt
|
|
updatedHook.DeleteAt = oldHook.DeleteAt
|
|
updatedHook.TeamId = oldHook.TeamId
|
|
updatedHook.UpdateAt = model.GetMillis()
|
|
|
|
webhook, err := a.Srv().Store.Webhook().UpdateOutgoing(updatedHook)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.update_outgoing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("GetOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhook, err := a.Srv().Store.Webhook().GetOutgoing(hookID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhooksPage(page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
|
|
return a.GetOutgoingWebhooksPageByUser("", page, perPage)
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhooksPageByUser(userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhooks, err := a.Srv().Store.Webhook().GetOutgoingListByUser(userID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "app.webhooks.get_outgoing_by_channel.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhooksForChannelPageByUser(channelId string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhooks, err := a.Srv().Store.Webhook().GetOutgoingByChannelByUser(channelId, userID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "app.webhooks.get_outgoing_by_channel.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
|
|
return a.GetOutgoingWebhooksForTeamPageByUser(teamID, "", page, perPage)
|
|
}
|
|
|
|
func (a *App) GetOutgoingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
webhooks, err := a.Srv().Store.Webhook().GetOutgoingByTeamByUser(teamID, userID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "app.webhooks.get_outgoing_by_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) DeleteOutgoingWebhook(hookID string) *model.AppError {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return model.NewAppError("DeleteOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if err := a.Srv().Store.Webhook().DeleteOutgoing(hookID, model.GetMillis()); err != nil {
|
|
return model.NewAppError("DeleteOutgoingWebhook", "app.webhooks.delete_outgoing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
|
return nil, model.NewAppError("RegenOutgoingWebhookToken", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
hook.Token = model.NewId()
|
|
|
|
webhook, err := a.Srv().Store.Webhook().UpdateOutgoing(hook)
|
|
if err != nil {
|
|
return nil, model.NewAppError("RegenOutgoingWebhookToken", "app.webhooks.update_outgoing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) HandleIncomingWebhook(hookID string, req *model.IncomingWebhookRequest) *model.AppError {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
hchan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
webhook, err := a.Srv().Store.Webhook().GetIncoming(hookID, true)
|
|
hchan <- store.StoreResult{Data: webhook, NErr: err}
|
|
close(hchan)
|
|
}()
|
|
|
|
if req == nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.parse.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
text := req.Text
|
|
if text == "" && req.Attachments == nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.text.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channelName := req.ChannelName
|
|
webhookType := req.Type
|
|
|
|
var hook *model.IncomingWebhook
|
|
result := <-hchan
|
|
if result.NErr != nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.invalid.app_error", nil, result.NErr.Error(), http.StatusBadRequest)
|
|
}
|
|
hook = result.Data.(*model.IncomingWebhook)
|
|
|
|
uchan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
user, err := a.Srv().Store.User().Get(context.Background(), hook.UserId)
|
|
uchan <- store.StoreResult{Data: user, NErr: err}
|
|
close(uchan)
|
|
}()
|
|
|
|
if len(req.Props) == 0 {
|
|
req.Props = make(model.StringInterface)
|
|
}
|
|
|
|
req.Props["webhook_display_name"] = hook.DisplayName
|
|
|
|
text = a.ProcessSlackText(text)
|
|
req.Attachments = a.ProcessSlackAttachments(req.Attachments)
|
|
// attachments is in here for slack compatibility
|
|
if len(req.Attachments) > 0 {
|
|
req.Props["attachments"] = req.Attachments
|
|
webhookType = model.POST_SLACK_ATTACHMENT
|
|
}
|
|
|
|
var channel *model.Channel
|
|
var cchan chan store.StoreResult
|
|
|
|
if channelName != "" {
|
|
if channelName[0] == '@' {
|
|
result, nErr := a.Srv().Store.User().GetByUsername(channelName[1:])
|
|
if nErr != nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, nErr.Error(), http.StatusBadRequest)
|
|
}
|
|
ch, err := a.GetOrCreateDirectChannel(hook.UserId, result.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channel = ch
|
|
} else if channelName[0] == '#' {
|
|
cchan = make(chan store.StoreResult, 1)
|
|
go func() {
|
|
chnn, chnnErr := a.Srv().Store.Channel().GetByName(hook.TeamId, channelName[1:], true)
|
|
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
|
|
close(cchan)
|
|
}()
|
|
} else {
|
|
cchan = make(chan store.StoreResult, 1)
|
|
go func() {
|
|
chnn, chnnErr := a.Srv().Store.Channel().GetByName(hook.TeamId, channelName, true)
|
|
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
|
|
close(cchan)
|
|
}()
|
|
}
|
|
} else {
|
|
var err error
|
|
channel, err = a.Srv().Store.Channel().Get(hook.ChannelId, true)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.find.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
if channel == nil {
|
|
result2 := <-cchan
|
|
if result2.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(result2.NErr, &nfErr):
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, result2.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
channel = result2.Data.(*model.Channel)
|
|
}
|
|
|
|
if hook.ChannelLocked && hook.ChannelId != channel.Id {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel_locked.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
var user *model.User
|
|
result = <-uchan
|
|
if result.NErr != nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, result.NErr.Error(), http.StatusForbidden)
|
|
}
|
|
user = result.Data.(*model.User)
|
|
|
|
if a.Srv().License() != nil && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
|
|
channel.Name == model.DEFAULT_CHANNEL && !a.RolesGrantPermission(user.GetRoles(), model.PERMISSION_MANAGE_SYSTEM.Id) {
|
|
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.town_square_read_only", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if channel.Type != model.CHANNEL_OPEN && !a.HasPermissionToChannel(hook.UserId, channel.Id, model.PERMISSION_READ_CHANNEL) {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
overrideUsername := hook.Username
|
|
if req.Username != "" {
|
|
overrideUsername = req.Username
|
|
}
|
|
|
|
overrideIconURL := hook.IconURL
|
|
if req.IconURL != "" {
|
|
overrideIconURL = req.IconURL
|
|
}
|
|
|
|
_, err := a.CreateWebhookPost(hook.UserId, channel, text, overrideUsername, overrideIconURL, req.IconEmoji, req.Props, webhookType, "")
|
|
return err
|
|
}
|
|
|
|
func (a *App) CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) {
|
|
hook := &model.CommandWebhook{
|
|
CommandId: commandID,
|
|
UserId: args.UserId,
|
|
ChannelId: args.ChannelId,
|
|
RootId: args.RootId,
|
|
ParentId: args.ParentId,
|
|
}
|
|
|
|
savedHook, err := a.Srv().Store.CommandWebhook().Save(hook)
|
|
if err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.existing", nil, invErr.Error(), http.StatusBadRequest)
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.internal_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
}
|
|
return savedHook, nil
|
|
}
|
|
|
|
func (a *App) HandleCommandWebhook(hookID string, response *model.CommandResponse) *model.AppError {
|
|
if response == nil {
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.handle_command_webhook.parse", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
hook, nErr := a.Srv().Store.CommandWebhook().Get(hookID)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.missing", map[string]interface{}{"hook_id": hookID}, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
cmd, cmdErr := a.Srv().Store.Command().Get(hook.CommandId)
|
|
if cmdErr != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(cmdErr, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("HandleCommandWebhook", "web.command_webhook.command.app_error", nil, "err="+cmdErr.Error(), http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
args := &model.CommandArgs{
|
|
UserId: hook.UserId,
|
|
ChannelId: hook.ChannelId,
|
|
TeamId: cmd.TeamId,
|
|
RootId: hook.RootId,
|
|
ParentId: hook.ParentId,
|
|
}
|
|
|
|
if nErr := a.Srv().Store.CommandWebhook().TryUse(hook.Id, 5); nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.invalid", nil, invErr.Error(), http.StatusBadRequest)
|
|
default:
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
_, err := a.HandleCommandResponse(cmd, args, response, false)
|
|
return err
|
|
}
|