mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-69010: Validate incoming webhook user membership Incoming webhook creation/update did not verify that the assigned user_id had legitimate access to the target team or channel, allowing a team admin to attribute persisted posts to an arbitrary user. Validate that the assigned user can read the target channel and does not hold privileges the requester lacks at creation, re-check channel access when a hook is moved, and require a shared team before a webhook creates a direct message via an @username payload. * MM-69010: Add regression test for owner+channel update Verify that changing both the channel and the supplied user_id in a single update still validates against the retained owner, since the owner is immutable on update. --------- Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
1030 lines
40 KiB
Go
1030 lines
40 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"maps"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
)
|
|
|
|
const (
|
|
TriggerwordsExactMatch = 0
|
|
TriggerwordsStartsWith = 1
|
|
|
|
MaxIntegrationResponseSize = 1024 * 1024 // Posts can be <100KB at most, so this is likely more than enough
|
|
MaxDialogResponseSize = 1024 * 1024 // 1MB limit for dialog responses to prevent OOM attacks
|
|
)
|
|
|
|
var linkWithTextRegex = regexp.MustCompile(`<([^\n<\|>]+)\|([^\|\n>]+)>`)
|
|
|
|
func (a *App) handleWebhookEvents(rctx request.CTX, 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.ChannelTypeOpen {
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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.TriggerWebhook(rctx, payload, hook, post, channel)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) TriggerWebhook(rctx request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
|
|
logger := rctx.Logger().With(mlog.String("outgoing_webhook_id", hook.Id), mlog.String("post_id", post.Id), mlog.String("channel_id", channel.Id), mlog.String("content_type", hook.ContentType))
|
|
|
|
var jsonBytes []byte
|
|
var err error
|
|
contentType := "application/x-www-form-urlencoded"
|
|
if hook.ContentType == "application/json" {
|
|
contentType = "application/json"
|
|
jsonBytes, err = json.Marshal(payload)
|
|
if err != nil {
|
|
logger.Warn("Failed to encode to JSON", mlog.Err(err))
|
|
return
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := range hook.CallbackURLs {
|
|
var body io.Reader
|
|
if hook.ContentType == "application/json" {
|
|
body = bytes.NewReader(jsonBytes)
|
|
} else {
|
|
body = strings.NewReader(payload.ToFormValues())
|
|
}
|
|
wg.Add(1)
|
|
|
|
// Get the callback URL by index to properly capture it for the go func
|
|
url := hook.CallbackURLs[i]
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logger.Error("Recovered from panic in outgoing webhook goroutine",
|
|
mlog.String("url", url),
|
|
mlog.Any("panic", r),
|
|
)
|
|
}
|
|
}()
|
|
|
|
var accessToken *model.OutgoingOAuthConnectionToken
|
|
|
|
// Retrieve an access token from a connection if one exists to use for the webhook request
|
|
if a.Config().ServiceSettings.EnableOutgoingOAuthConnections != nil && *a.Config().ServiceSettings.EnableOutgoingOAuthConnections && a.OutgoingOAuthConnections() != nil {
|
|
connection, err := a.OutgoingOAuthConnections().GetConnectionForAudience(rctx, url)
|
|
if err != nil {
|
|
logger.Error("Failed to find an outgoing oauth connection for the webhook", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
if connection != nil {
|
|
accessToken, err = a.OutgoingOAuthConnections().RetrieveTokenForConnection(rctx, connection)
|
|
if err != nil {
|
|
logger.Error("Failed to retrieve token for outgoing oauth connection", mlog.Err(err))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType, accessToken)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
logger.Error("Outgoing Webhook POST timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.", mlog.Err(err))
|
|
} else {
|
|
logger.Error("Outgoing Webhook POST failed", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
if webhookResp != nil && (webhookResp.Text != nil || len(webhookResp.Attachments) > 0) {
|
|
postRootId := ""
|
|
if webhookResp.ResponseType == model.OutgoingHookResponseTypeComment {
|
|
postRootId = post.Id
|
|
}
|
|
if len(webhookResp.Props) == 0 {
|
|
webhookResp.Props = make(model.StringInterface)
|
|
}
|
|
webhookResp.Props[model.PostPropsWebhookDisplayName] = hook.DisplayName
|
|
|
|
text := ""
|
|
if webhookResp.Text != nil {
|
|
text = a.ProcessSlackText(rctx, *webhookResp.Text)
|
|
}
|
|
webhookResp.Attachments = a.ProcessMessageAttachments(rctx, webhookResp.Attachments)
|
|
// attachments is in here for slack compatibility
|
|
if len(webhookResp.Attachments) > 0 {
|
|
webhookResp.Props[model.PostPropsAttachments] = 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(rctx, hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, "", webhookResp.Props, webhookResp.Type, postRootId, webhookResp.Priority); err != nil {
|
|
logger.Error("Failed to create response post.", mlog.Err(err))
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string, accessToken *model.OutgoingOAuthConnectionToken) (*model.OutgoingWebhookResponse, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
if accessToken != nil {
|
|
req.Header.Add("Authorization", accessToken.AsHeaderValue())
|
|
}
|
|
|
|
resp, err := a.Srv().outgoingWebhookClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var hookResp model.OutgoingWebhookResponse
|
|
if jsonErr := json.NewDecoder(io.LimitReader(resp.Body, MaxIntegrationResponseSize)).Decode(&hookResp); jsonErr != nil {
|
|
if jsonErr == io.EOF {
|
|
return nil, nil
|
|
}
|
|
return nil, model.NewAppError("doOutgoingWebhookRequest", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
|
|
return &hookResp, nil
|
|
}
|
|
|
|
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]any))
|
|
for k, v := range post.GetProps() {
|
|
if k != model.PostPropsAttachments {
|
|
base.AddProp(k, v)
|
|
}
|
|
}
|
|
|
|
if utf8.RuneCountInString(model.StringInterfaceToJSON(base.GetProps())) > model.PostPropsMaxUserRunes {
|
|
return nil, model.NewAppError("splitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", 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(model.PostPropsAttachments).([]*model.MessageAttachment)
|
|
for _, attachment := range attachments {
|
|
newAttachment := *attachment
|
|
for {
|
|
lastSplit := splits[len(splits)-1]
|
|
newProps := make(map[string]any)
|
|
maps.Copy(newProps, lastSplit.GetProps())
|
|
origAttachments, _ := newProps[model.PostPropsAttachments].([]*model.MessageAttachment)
|
|
newProps[model.PostPropsAttachments] = append(origAttachments, &newAttachment)
|
|
newPropsString := model.StringInterfaceToJSON(newProps)
|
|
runeCount := utf8.RuneCountInString(newPropsString)
|
|
|
|
if runeCount <= model.PostPropsMaxUserRunes {
|
|
lastSplit.SetProps(newProps)
|
|
break
|
|
}
|
|
|
|
if len(origAttachments) > 0 {
|
|
newSplit := base.Clone()
|
|
splits = append(splits, newSplit)
|
|
continue
|
|
}
|
|
|
|
truncationNeeded := runeCount - model.PostPropsMaxUserRunes
|
|
textRuneCount := utf8.RuneCountInString(attachment.Text)
|
|
if textRuneCount < truncationNeeded {
|
|
return nil, model.NewAppError("splitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", 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(rctx request.CTX, userID string, channel *model.Channel, text, overrideUsername, overrideIconURL, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string, priority *model.PostPriority) (*model.Post, *model.AppError) {
|
|
// parse links into Markdown format
|
|
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
|
|
|
post := &model.Post{UserId: userID, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId}
|
|
post.AddProp(model.PostPropsFromWebhook, "true")
|
|
|
|
if priority != nil {
|
|
if priority.Priority == nil {
|
|
err := model.NewAppError("CreateWebhookPost", "api.context.invalid_param.app_error", map[string]any{"Name": "webhook.priority.priority"}, "Setting the priority of a post is required to use priority.", http.StatusBadRequest)
|
|
return nil, err
|
|
}
|
|
post.Metadata = &model.PostMetadata{
|
|
Priority: priority,
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
|
|
err := model.NewAppError("CreateWebhookPost", "api.context.invalid_param.app_error", map[string]any{"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(model.PostPropsOverrideUsername, overrideUsername)
|
|
} else {
|
|
post.AddProp(model.PostPropsOverrideUsername, model.DefaultWebhookUsername)
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnablePostIconOverride {
|
|
if overrideIconURL != "" {
|
|
post.AddProp(model.PostPropsOverrideIconURL, overrideIconURL)
|
|
}
|
|
if overrideIconEmoji != "" {
|
|
post.AddProp(model.PostPropsOverrideIconEmoji, overrideIconEmoji)
|
|
}
|
|
}
|
|
|
|
if len(props) > 0 {
|
|
for key, val := range props {
|
|
switch key {
|
|
case model.PostPropsAttachments:
|
|
if attachments, success := val.([]*model.MessageAttachment); success {
|
|
model.ParseMessageAttachment(post, attachments)
|
|
}
|
|
case model.PostPropsOverrideIconURL,
|
|
model.PostPropsOverrideUsername,
|
|
model.PostPropsFromWebhook:
|
|
// Do nothing
|
|
case model.PostPropsMmBlocksActions:
|
|
// Webhook payloads are user-controlled even when the
|
|
// webhook is bound to a bot user, so the bot-author
|
|
// signal in CreatePost's strip rule cannot distinguish
|
|
// them. Drop here so mm_blocks_actions never reaches
|
|
// the post object.
|
|
default:
|
|
post.AddProp(key, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
splits, err := splitWebhookPost(post, a.MaxPostSize())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, split := range splits {
|
|
if _, _, err := a.CreatePost(rctx, split, channel, model.CreatePostFlags{}); err != nil {
|
|
return nil, model.NewAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return splits[0], nil
|
|
}
|
|
|
|
// ValidateIncomingWebhookUser ensures a user being assigned as an incoming webhook's owner can
|
|
// legitimately be attributed posts in the target channel: the user must have access to the
|
|
// channel and must not hold privileges the requester lacks, so a requester cannot forge posts
|
|
// as a non-member or higher-privileged user.
|
|
func (a *App) ValidateIncomingWebhookUser(rctx request.CTX, session model.Session, user *model.User, channel *model.Channel) *model.AppError {
|
|
if user.IsSystemAdmin() && !a.SessionHasPermissionTo(session, model.PermissionManageSystem) {
|
|
return model.NewAppError("ValidateIncomingWebhookUser", "api.webhook.incoming.user_role.app_error", nil, "user_id="+user.Id, http.StatusForbidden)
|
|
}
|
|
|
|
return a.ValidateIncomingWebhookUserChannelAccess(rctx, user.Id, channel)
|
|
}
|
|
|
|
// ValidateIncomingWebhookUserChannelAccess ensures the webhook owner can read the channel its
|
|
// posts are attributed to, preventing attribution to a user who is not a member of the channel
|
|
// (or its team, for open channels).
|
|
func (a *App) ValidateIncomingWebhookUserChannelAccess(rctx request.CTX, userID string, channel *model.Channel) *model.AppError {
|
|
if hasPermission, _ := a.HasPermissionToChannel(rctx, userID, channel.Id, model.PermissionReadChannelContent); !hasPermission {
|
|
return model.NewAppError("ValidateIncomingWebhookUserChannelAccess", "api.webhook.incoming.user_membership.app_error", nil, "user_id="+userID+", channel_id="+channel.Id, http.StatusForbidden)
|
|
}
|
|
|
|
return 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, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
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
|
|
updatedHook.LastUsed = oldHook.LastUsed
|
|
|
|
newWebhook, err := a.Srv().Store().Webhook().UpdateIncoming(updatedHook)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateIncomingWebhook", "app.webhooks.update_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
a.Srv().Platform().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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.Srv().Platform().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, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return webhooks, nil
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
|
return a.GetIncomingWebhooksPageByUser("", page, perPage)
|
|
}
|
|
|
|
func (a *App) GetIncomingWebhooksCount(teamID string, userID string) (int64, *model.AppError) {
|
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
return 0, model.NewAppError("GetIncomingWebhooksCount", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
totalCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID, userID)
|
|
if err != nil {
|
|
return 0, model.NewAppError("GetIncomingWebhooksCount", "app.webhooks.get_incoming_count.app_error", map[string]any{"TeamID": teamID, "UserID": userID, "Error": err.Error()}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return totalCount, nil
|
|
}
|
|
|
|
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 {
|
|
errCtx := map[string]any{"channel_id": hook.ChannelId}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(errCh, &nfErr):
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(errCh)
|
|
default:
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(errCh)
|
|
}
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeOpen || 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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
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, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) UpdateOutgoingWebhook(rctx request.CTX, 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(rctx, updatedHook.ChannelId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (a *App) HandleIncomingWebhook(rctx request.CTX, 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[*model.IncomingWebhook], 1)
|
|
go func() {
|
|
webhook, err := a.Srv().Store().Webhook().GetIncoming(hookID, true)
|
|
hchan <- store.StoreResult[*model.IncomingWebhook]{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, "", http.StatusBadRequest).Wrap(result.NErr)
|
|
}
|
|
hook = result.Data
|
|
|
|
uchan := make(chan store.StoreResult[*model.User], 1)
|
|
go func() {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), hook.UserId)
|
|
uchan <- store.StoreResult[*model.User]{Data: user, NErr: err}
|
|
close(uchan)
|
|
}()
|
|
|
|
if len(req.Props) == 0 {
|
|
req.Props = make(model.StringInterface)
|
|
}
|
|
|
|
req.Props[model.PostPropsWebhookDisplayName] = hook.DisplayName
|
|
|
|
text = a.ProcessSlackText(rctx, text)
|
|
req.Attachments = a.ProcessMessageAttachments(rctx, req.Attachments)
|
|
// attachments is in here for slack compatibility
|
|
if len(req.Attachments) > 0 {
|
|
req.Props[model.PostPropsAttachments] = req.Attachments
|
|
webhookType = model.PostTypeMessageAttachment
|
|
}
|
|
|
|
var channel *model.Channel
|
|
var cchan chan store.StoreResult[*model.Channel]
|
|
|
|
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", map[string]any{"user": channelName[1:]}, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
// Only allow a DM target the webhook owner shares a team with, so the stored
|
|
// user_id cannot be used to reach users the owner could not message directly.
|
|
if hook.UserId != result.Id {
|
|
commonTeamIDs, teamErr := a.GetCommonTeamIDsForTwoUsers(hook.UserId, result.Id)
|
|
if teamErr != nil {
|
|
return teamErr
|
|
}
|
|
if len(commonTeamIDs) == 0 {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", map[string]any{"user": hook.UserId, "channel": channelName}, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
ch, err := a.GetOrCreateDirectChannel(rctx, hook.UserId, result.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channel = ch
|
|
} else if channelName[0] == '#' {
|
|
cchan = make(chan store.StoreResult[*model.Channel], 1)
|
|
go func() {
|
|
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName[1:], true)
|
|
cchan <- store.StoreResult[*model.Channel]{Data: chnn, NErr: chnnErr}
|
|
close(cchan)
|
|
}()
|
|
} else {
|
|
cchan = make(chan store.StoreResult[*model.Channel], 1)
|
|
go func() {
|
|
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName, true)
|
|
cchan <- store.StoreResult[*model.Channel]{Data: chnn, NErr: chnnErr}
|
|
close(cchan)
|
|
}()
|
|
}
|
|
} else {
|
|
var err error
|
|
channel, err = a.Srv().Store().Channel().Get(hook.ChannelId, true)
|
|
if err != nil {
|
|
errCtx := map[string]any{"channel_id": hook.ChannelId}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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, "", http.StatusNotFound).Wrap(result2.NErr)
|
|
default:
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(result2.NErr)
|
|
}
|
|
}
|
|
channel = result2.Data
|
|
}
|
|
|
|
if hook.ChannelLocked && hook.ChannelId != channel.Id {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel_locked.app_error", map[string]any{"channel_id": channel.Id}, "", http.StatusForbidden)
|
|
}
|
|
|
|
resultU := <-uchan
|
|
if resultU.NErr != nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", map[string]any{"user": hook.UserId}, "", http.StatusForbidden).Wrap(resultU.NErr)
|
|
}
|
|
|
|
restrictedChannel := false
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
hasPermission, _ := a.HasPermissionToChannel(rctx, hook.UserId, channel.Id, model.PermissionReadChannelContent)
|
|
restrictedChannel = !hasPermission
|
|
}
|
|
if restrictedChannel {
|
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", map[string]any{"user": hook.UserId, "channel": channel.Id}, "", http.StatusForbidden)
|
|
}
|
|
|
|
threadRootID := ""
|
|
if rootId := req.RootId; rootId != "" {
|
|
if !model.IsValidId(rootId) {
|
|
return model.NewAppError("HandleIncomingWebhook", "api.context.invalid_param.app_error", map[string]any{"Name": "root_id"}, "", http.StatusBadRequest)
|
|
}
|
|
rootPost, nErr := a.Srv().Store().Post().GetSingle(rctx, rootId, false)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("HandleIncomingWebhook", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
if rootPost == nil {
|
|
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if rootPost.ChannelId != channel.Id {
|
|
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.channel_root_id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if rootPost.RootId != "" {
|
|
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
threadRootID = rootPost.Id
|
|
}
|
|
|
|
overrideUsername := hook.Username
|
|
if req.Username != "" {
|
|
overrideUsername = req.Username
|
|
}
|
|
|
|
overrideIconURL := hook.IconURL
|
|
if req.IconURL != "" {
|
|
overrideIconURL = req.IconURL
|
|
}
|
|
|
|
_, err := a.CreateWebhookPost(rctx, hook.UserId, channel, text, overrideUsername, overrideIconURL, req.IconEmoji, req.Props, webhookType, threadRootID, req.Priority)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := model.GetMillis()
|
|
if nErr := a.Srv().Store().Webhook().UpdateIncomingLastUsed(hook.Id, now); nErr != nil {
|
|
rctx.Logger().Warn("Failed to update incoming webhook LastUsed", mlog.String("hook_id", hook.Id), mlog.Err(nErr))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
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, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return savedHook, nil
|
|
}
|
|
|
|
func (a *App) HandleCommandWebhook(rctx request.CTX, 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", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
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", map[string]any{"command_id": hook.CommandId}, "", http.StatusBadRequest).Wrap(cmdErr)
|
|
}
|
|
}
|
|
|
|
args := &model.CommandArgs{
|
|
UserId: hook.UserId,
|
|
ChannelId: hook.ChannelId,
|
|
TeamId: cmd.TeamId,
|
|
RootId: hook.RootId,
|
|
}
|
|
|
|
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, "", http.StatusBadRequest).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
_, err := a.HandleCommandResponse(rctx, cmd, args, response, false)
|
|
return err
|
|
}
|