2019-11-29 06:59:40 -05:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
|
|
|
// See LICENSE.txt for license information.
|
2017-01-13 13:53:37 -05:00
|
|
|
|
|
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
MM-30882: Fix read-after-write issue for demoting user (#16911)
* 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
2021-02-12 08:34:05 -05:00
|
|
|
"context"
|
2020-06-02 12:28:29 -04:00
|
|
|
"errors"
|
2019-01-09 17:07:08 -05:00
|
|
|
"io"
|
2025-07-18 06:54:51 -04:00
|
|
|
"maps"
|
2017-03-13 09:23:16 -04:00
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
2021-07-13 09:56:20 -04:00
|
|
|
"regexp"
|
2017-03-13 09:23:16 -04:00
|
|
|
"strings"
|
2020-03-11 06:50:12 -04:00
|
|
|
"sync"
|
2023-12-18 10:07:00 -05:00
|
|
|
"time"
|
2020-03-23 13:38:21 -04:00
|
|
|
"unicode"
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2023-06-11 01:24:35 -04:00
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
2023-09-05 03:47:30 -04:00
|
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
2023-06-11 01:24:35 -04:00
|
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
2017-01-13 13:53:37 -05:00
|
|
|
)
|
|
|
|
|
|
2021-02-18 06:08:01 -05:00
|
|
|
const (
|
|
|
|
|
CmdCustomStatusTrigger = "status"
|
2021-07-13 09:56:20 -04:00
|
|
|
usernameSpecialChars = ".-_"
|
2022-02-25 02:34:18 -05:00
|
|
|
maxTriggerLen = 512
|
2021-02-18 06:08:01 -05:00
|
|
|
)
|
|
|
|
|
|
2021-07-13 09:56:20 -04:00
|
|
|
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
|
|
|
|
|
|
2017-03-13 09:23:16 -04:00
|
|
|
type CommandProvider interface {
|
|
|
|
|
GetTrigger() string
|
2021-02-26 02:12:49 -05:00
|
|
|
GetCommand(a *App, T i18n.TranslateFunc) *model.Command
|
2025-09-10 09:11:32 -04:00
|
|
|
DoCommand(a *App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commandProviders = make(map[string]CommandProvider)
|
|
|
|
|
|
|
|
|
|
func RegisterCommandProvider(newProvider CommandProvider) {
|
|
|
|
|
commandProviders[newProvider.GetTrigger()] = newProvider
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetCommandProvider(name string) CommandProvider {
|
|
|
|
|
provider, ok := commandProviders[name]
|
|
|
|
|
if ok {
|
|
|
|
|
return provider
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) CreateCommandPost(rctx request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
|
2019-10-22 08:39:49 -04:00
|
|
|
if skipSlackParsing {
|
|
|
|
|
post.Message = response.Text
|
|
|
|
|
} else {
|
|
|
|
|
post.Message = model.ParseSlackLinksToMarkdown(response.Text)
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-13 13:53:37 -05:00
|
|
|
post.CreateAt = model.GetMillis()
|
|
|
|
|
|
2021-07-12 14:05:36 -04:00
|
|
|
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
|
2022-07-05 02:46:50 -04:00
|
|
|
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
|
2017-10-09 13:30:48 -04:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-13 13:53:37 -05:00
|
|
|
if response.Attachments != nil {
|
2018-09-17 10:15:28 -04:00
|
|
|
model.ParseSlackAttachment(post, response.Attachments)
|
2017-01-13 13:53:37 -05:00
|
|
|
}
|
|
|
|
|
|
2021-07-12 14:05:36 -04:00
|
|
|
if response.ResponseType == model.CommandResponseTypeInChannel {
|
2025-09-10 09:11:32 -04:00
|
|
|
return a.CreatePostMissingChannel(rctx, post, true, true)
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
|
|
|
|
|
2021-07-12 14:05:36 -04:00
|
|
|
if (response.ResponseType == "" || response.ResponseType == model.CommandResponseTypeEphemeral) && (response.Text != "" || response.Attachments != nil) {
|
2025-09-10 09:11:32 -04:00
|
|
|
a.SendEphemeralPost(rctx, post.UserId, post)
|
2017-01-13 13:53:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return post, nil
|
|
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2017-04-03 08:12:50 -04:00
|
|
|
// previous ListCommands now ListAutocompleteCommands
|
2021-02-26 02:12:49 -05:00
|
|
|
func (a *App) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
|
2017-03-13 09:23:16 -04:00
|
|
|
commands := make([]*model.Command, 0, 32)
|
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
|
|
2021-02-18 06:08:01 -05:00
|
|
|
// Disable custom status slash command if the feature or the setting is off
|
2021-03-05 03:21:58 -05:00
|
|
|
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
|
2021-02-18 06:08:01 -05:00
|
|
|
seen[CmdCustomStatusTrigger] = true
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-22 17:22:27 -04:00
|
|
|
for _, cmd := range a.CommandsForTeam(teamID) {
|
2017-12-08 14:55:41 -05:00
|
|
|
if cmd.AutoComplete && !seen[cmd.Trigger] {
|
|
|
|
|
seen[cmd.Trigger] = true
|
|
|
|
|
commands = append(commands, cmd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-18 18:36:43 -04:00
|
|
|
if *a.Config().ServiceSettings.EnableCommands {
|
2022-10-06 04:04:21 -04:00
|
|
|
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
2019-04-29 04:20:52 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("ListAutocompleteCommands", "app.command.listautocompletecommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, cmd := range teamCmds {
|
2020-07-02 16:28:21 -04:00
|
|
|
if cmd.AutoComplete && !seen[cmd.Trigger] {
|
2018-10-17 08:24:31 -04:00
|
|
|
cmd.Sanitize()
|
|
|
|
|
seen[cmd.Trigger] = true
|
|
|
|
|
commands = append(commands, cmd)
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-02 16:28:21 -04:00
|
|
|
for _, value := range commandProviders {
|
|
|
|
|
if cmd := value.GetCommand(a, T); cmd != nil {
|
|
|
|
|
cpy := *cmd
|
|
|
|
|
if cpy.AutoComplete && !seen[cpy.Trigger] {
|
|
|
|
|
cpy.Sanitize()
|
|
|
|
|
seen[cpy.Trigger] = true
|
|
|
|
|
commands = append(commands, &cpy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-13 09:23:16 -04:00
|
|
|
return commands, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 05:22:27 -05:00
|
|
|
func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
|
2025-11-13 06:12:30 -05:00
|
|
|
return a.ListTeamCommandsByUser(teamID, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) ListTeamCommandsByUser(teamID string, userID string) ([]*model.Command, *model.AppError) {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
2020-07-16 09:26:07 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-13 06:12:30 -05:00
|
|
|
// Filter by user if userID is specified
|
|
|
|
|
if userID != "" {
|
|
|
|
|
filteredCmds := make([]*model.Command, 0)
|
|
|
|
|
for _, cmd := range teamCmds {
|
|
|
|
|
if cmd.CreatorId == userID {
|
|
|
|
|
filteredCmds = append(filteredCmds, cmd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return filteredCmds, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 09:26:07 -04:00
|
|
|
return teamCmds, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-26 02:12:49 -05:00
|
|
|
func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
|
2025-11-13 06:12:30 -05:00
|
|
|
return a.ListAllCommandsByUser(teamID, "", T)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) ListAllCommandsByUser(teamID string, userID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
|
2017-04-03 08:12:50 -04:00
|
|
|
commands := make([]*model.Command, 0, 32)
|
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
|
for _, value := range commandProviders {
|
2017-11-09 15:46:20 -05:00
|
|
|
if cmd := value.GetCommand(a, T); cmd != nil {
|
|
|
|
|
cpy := *cmd
|
2017-12-08 14:55:41 -05:00
|
|
|
if cpy.AutoComplete && !seen[cpy.Trigger] {
|
2017-11-09 15:46:20 -05:00
|
|
|
cpy.Sanitize()
|
|
|
|
|
seen[cpy.Trigger] = true
|
|
|
|
|
commands = append(commands, &cpy)
|
|
|
|
|
}
|
2017-04-03 08:12:50 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-22 17:22:27 -04:00
|
|
|
for _, cmd := range a.CommandsForTeam(teamID) {
|
2017-12-08 14:55:41 -05:00
|
|
|
if !seen[cmd.Trigger] {
|
|
|
|
|
seen[cmd.Trigger] = true
|
|
|
|
|
commands = append(commands, cmd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-18 18:36:43 -04:00
|
|
|
if *a.Config().ServiceSettings.EnableCommands {
|
2022-10-06 04:04:21 -04:00
|
|
|
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
2019-04-29 04:20:52 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("ListAllCommands", "app.command.listallcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
|
|
|
|
for _, cmd := range teamCmds {
|
|
|
|
|
if !seen[cmd.Trigger] {
|
2025-11-13 06:12:30 -05:00
|
|
|
// Filter by user if userID is specified (before sanitizing)
|
|
|
|
|
if userID != "" && cmd.CreatorId != userID {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2018-10-17 08:24:31 -04:00
|
|
|
cmd.Sanitize()
|
|
|
|
|
seen[cmd.Trigger] = true
|
|
|
|
|
commands = append(commands, cmd)
|
2017-04-03 08:12:50 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return commands, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) ExecuteCommand(rctx request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
2020-03-23 13:38:21 -04:00
|
|
|
trigger := ""
|
|
|
|
|
message := ""
|
|
|
|
|
index := strings.IndexFunc(args.Command, unicode.IsSpace)
|
|
|
|
|
if index != -1 {
|
|
|
|
|
trigger = args.Command[:index]
|
|
|
|
|
message = args.Command[index+1:]
|
|
|
|
|
} else {
|
|
|
|
|
trigger = args.Command
|
|
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
trigger = strings.ToLower(trigger)
|
2020-03-23 13:38:21 -04:00
|
|
|
if !strings.HasPrefix(trigger, "/") {
|
2022-07-05 02:46:50 -04:00
|
|
|
return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]any{"Trigger": trigger}, "", http.StatusBadRequest)
|
2020-03-23 13:38:21 -04:00
|
|
|
}
|
|
|
|
|
trigger = strings.TrimPrefix(trigger, "/")
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2021-03-23 05:32:54 -04:00
|
|
|
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
|
2018-11-19 15:27:17 -05:00
|
|
|
if appErr != nil {
|
2025-09-10 09:11:32 -04:00
|
|
|
rctx.Logger().Warn("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
|
2018-11-19 15:27:17 -05:00
|
|
|
}
|
|
|
|
|
|
2021-03-23 05:32:54 -04:00
|
|
|
args.TriggerId = triggerId
|
2018-11-19 15:27:17 -05:00
|
|
|
|
2023-10-17 10:55:41 -04:00
|
|
|
// Plugins can override built in and custom commands
|
2025-09-10 09:11:32 -04:00
|
|
|
cmd, response, appErr := a.tryExecutePluginCommand(rctx, args)
|
2018-10-17 08:24:31 -04:00
|
|
|
if appErr != nil {
|
|
|
|
|
return nil, appErr
|
2019-01-09 17:07:08 -05:00
|
|
|
} else if cmd != nil && response != nil {
|
2021-03-23 05:32:54 -04:00
|
|
|
response.TriggerId = clientTriggerId
|
2025-09-10 09:11:32 -04:00
|
|
|
return a.HandleCommandResponse(rctx, cmd, args, response, true)
|
2017-12-08 14:55:41 -05:00
|
|
|
}
|
|
|
|
|
|
2020-07-02 16:28:21 -04:00
|
|
|
// Custom commands can override built ins
|
2025-09-10 09:11:32 -04:00
|
|
|
cmd, response, appErr = a.tryExecuteCustomCommand(rctx, args, trigger, message)
|
2019-01-09 17:07:08 -05:00
|
|
|
if appErr != nil {
|
|
|
|
|
return nil, appErr
|
|
|
|
|
} else if cmd != nil && response != nil {
|
2021-03-23 05:32:54 -04:00
|
|
|
response.TriggerId = clientTriggerId
|
2025-09-10 09:11:32 -04:00
|
|
|
return a.HandleCommandResponse(rctx, cmd, args, response, false)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
cmd, response = a.tryExecuteBuiltInCommand(rctx, args, trigger, message)
|
2020-07-02 16:28:21 -04:00
|
|
|
if cmd != nil && response != nil {
|
2025-09-10 09:11:32 -04:00
|
|
|
return a.HandleCommandResponse(rctx, cmd, args, response, true)
|
2020-07-02 16:28:21 -04:00
|
|
|
}
|
|
|
|
|
|
2022-02-25 02:34:18 -05:00
|
|
|
if len(trigger) > maxTriggerLen {
|
|
|
|
|
trigger = trigger[:maxTriggerLen]
|
|
|
|
|
trigger += "..."
|
|
|
|
|
}
|
2022-07-05 02:46:50 -04:00
|
|
|
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]any{"Trigger": trigger}, "", http.StatusNotFound)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
2020-08-12 12:29:58 -04:00
|
|
|
// MentionsToTeamMembers returns all the @ mentions found in message that
|
2020-03-11 06:50:12 -04:00
|
|
|
// belong to users in the specified team, linking them to their users
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) MentionsToTeamMembers(rctx request.CTX, message, teamID string) model.UserMentionMap {
|
2020-03-11 06:50:12 -04:00
|
|
|
type mentionMapItem struct {
|
|
|
|
|
Name string
|
2021-03-23 05:32:54 -04:00
|
|
|
Id string
|
2020-03-11 06:50:12 -04:00
|
|
|
}
|
|
|
|
|
|
2021-07-13 09:56:20 -04:00
|
|
|
possibleMentions := possibleAtMentions(message)
|
2020-03-11 06:50:12 -04:00
|
|
|
mentionChan := make(chan *mentionMapItem, len(possibleMentions))
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
for _, mention := range possibleMentions {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func(mention string) {
|
|
|
|
|
defer wg.Done()
|
2022-10-06 04:04:21 -04:00
|
|
|
user, nErr := a.Srv().Store().User().GetByUsername(mention)
|
2020-03-11 06:50:12 -04:00
|
|
|
|
2020-10-26 05:41:27 -04:00
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
if nErr != nil && !errors.As(nErr, &nfErr) {
|
2025-09-10 09:11:32 -04:00
|
|
|
rctx.Logger().Warn("Failed to retrieve user @"+mention, mlog.Err(nErr))
|
2020-03-11 06:50:12 -04:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If it's a http.StatusNotFound error, check for usernames in substrings
|
|
|
|
|
// without trailing punctuation
|
2020-10-26 05:41:27 -04:00
|
|
|
if nErr != nil {
|
2021-07-13 09:56:20 -04:00
|
|
|
trimmed, ok := trimUsernameSpecialChar(mention)
|
|
|
|
|
for ; ok; trimmed, ok = trimUsernameSpecialChar(trimmed) {
|
2022-10-06 04:04:21 -04:00
|
|
|
userFromTrimmed, nErr := a.Srv().Store().User().GetByUsername(trimmed)
|
2020-10-26 05:41:27 -04:00
|
|
|
if nErr != nil && !errors.As(nErr, &nfErr) {
|
2020-03-11 06:50:12 -04:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-26 05:41:27 -04:00
|
|
|
if nErr != nil {
|
2020-03-11 06:50:12 -04:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
_, err := a.GetTeamMember(rctx, teamID, userFromTrimmed.Id)
|
2020-03-11 06:50:12 -04:00
|
|
|
if err != nil {
|
|
|
|
|
// The user is not in the team, so we should ignore it
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
_, err := a.GetTeamMember(rctx, teamID, user.Id)
|
2020-03-11 06:50:12 -04:00
|
|
|
if err != nil {
|
|
|
|
|
// The user is not in the team, so we should ignore it
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mentionChan <- &mentionMapItem{mention, user.Id}
|
|
|
|
|
}(mention)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
close(mentionChan)
|
|
|
|
|
|
|
|
|
|
atMentionMap := make(model.UserMentionMap)
|
|
|
|
|
for mention := range mentionChan {
|
2021-03-23 05:32:54 -04:00
|
|
|
atMentionMap[mention.Name] = mention.Id
|
2020-03-11 06:50:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return atMentionMap
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-12 12:29:58 -04:00
|
|
|
// MentionsToPublicChannels returns all the mentions to public channels,
|
2020-03-11 06:50:12 -04:00
|
|
|
// linking them to their channels
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) MentionsToPublicChannels(rctx request.CTX, message, teamID string) model.ChannelMentionMap {
|
2020-03-11 06:50:12 -04:00
|
|
|
type mentionMapItem struct {
|
|
|
|
|
Name string
|
2021-03-23 05:32:54 -04:00
|
|
|
Id string
|
2020-03-11 06:50:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
channelMentions := model.ChannelMentions(message)
|
|
|
|
|
mentionChan := make(chan *mentionMapItem, len(channelMentions))
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
for _, channelName := range channelMentions {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func(channelName string) {
|
|
|
|
|
defer wg.Done()
|
2025-09-10 09:11:32 -04:00
|
|
|
channel, err := a.GetChannelByName(rctx, channelName, teamID, false)
|
2020-03-11 06:50:12 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !channel.IsOpen() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mentionChan <- &mentionMapItem{channelName, channel.Id}
|
|
|
|
|
}(channelName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
close(mentionChan)
|
|
|
|
|
|
|
|
|
|
channelMentionMap := make(model.ChannelMentionMap)
|
|
|
|
|
for mention := range mentionChan {
|
2021-03-23 05:32:54 -04:00
|
|
|
channelMentionMap[mention.Name] = mention.Id
|
2020-03-11 06:50:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return channelMentionMap
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-10 12:28:12 -04:00
|
|
|
// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
|
2019-01-09 17:07:08 -05:00
|
|
|
// found, returns nil for all arguments.
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) tryExecuteBuiltInCommand(rctx request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
|
2019-01-09 17:07:08 -05:00
|
|
|
provider := GetCommandProvider(trigger)
|
|
|
|
|
if provider == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := provider.GetCommand(a, args.T)
|
|
|
|
|
if cmd == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
return cmd, provider.DoCommand(a, rctx, args, message)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
|
|
|
|
|
// found, returns nil for all arguments.
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) tryExecuteCustomCommand(rctx request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
|
2019-01-09 17:07:08 -05:00
|
|
|
// Handle custom commands
|
2017-11-09 15:46:20 -05:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2019-01-09 17:07:08 -05:00
|
|
|
return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
2017-11-09 15:46:20 -05:00
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2024-01-04 06:30:21 -05:00
|
|
|
chanChan := make(chan store.StoreResult[*model.Channel], 1)
|
2019-04-24 15:28:06 -04:00
|
|
|
go func() {
|
2022-10-06 04:04:21 -04:00
|
|
|
channel, err := a.Srv().Store().Channel().Get(args.ChannelId, true)
|
2024-01-04 06:30:21 -05:00
|
|
|
chanChan <- store.StoreResult[*model.Channel]{Data: channel, NErr: err}
|
2019-04-24 15:28:06 -04:00
|
|
|
close(chanChan)
|
|
|
|
|
}()
|
2019-04-26 12:18:07 -04:00
|
|
|
|
2024-01-04 06:30:21 -05:00
|
|
|
teamChan := make(chan store.StoreResult[*model.Team], 1)
|
2019-04-26 12:18:07 -04:00
|
|
|
go func() {
|
2022-10-06 04:04:21 -04:00
|
|
|
team, err := a.Srv().Store().Team().Get(args.TeamId)
|
2024-01-04 06:30:21 -05:00
|
|
|
teamChan <- store.StoreResult[*model.Team]{Data: team, NErr: err}
|
2019-04-26 12:18:07 -04:00
|
|
|
close(teamChan)
|
|
|
|
|
}()
|
|
|
|
|
|
2024-01-04 06:30:21 -05:00
|
|
|
userChan := make(chan store.StoreResult[*model.User], 1)
|
2019-04-15 16:53:52 -04:00
|
|
|
go func() {
|
2022-10-06 04:04:21 -04:00
|
|
|
user, err := a.Srv().Store().User().Get(context.Background(), args.UserId)
|
2024-01-04 06:30:21 -05:00
|
|
|
userChan <- store.StoreResult[*model.User]{Data: user, NErr: err}
|
2019-04-15 16:53:52 -04:00
|
|
|
close(userChan)
|
|
|
|
|
}()
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
teamCmds, err := a.Srv().Store().Command().GetByTeam(args.TeamId)
|
2019-04-29 04:20:52 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.command.tryexecutecustomcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2018-10-17 08:24:31 -04:00
|
|
|
tr := <-teamChan
|
2020-09-03 00:29:57 -04:00
|
|
|
if tr.NErr != nil {
|
|
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(tr.NErr, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
|
2020-09-03 00:29:57 -04:00
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
|
2020-09-03 00:29:57 -04:00
|
|
|
}
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
2023-11-06 07:52:12 -05:00
|
|
|
team := tr.Data
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2018-10-17 08:24:31 -04:00
|
|
|
ur := <-userChan
|
2020-10-26 05:41:27 -04:00
|
|
|
if ur.NErr != nil {
|
|
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(ur.NErr, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
|
2020-10-26 05:41:27 -04:00
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
|
2020-10-26 05:41:27 -04:00
|
|
|
}
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
2023-11-06 07:52:12 -05:00
|
|
|
user := ur.Data
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2018-10-17 08:24:31 -04:00
|
|
|
cr := <-chanChan
|
2020-06-02 12:28:29 -04:00
|
|
|
if cr.NErr != nil {
|
2024-11-21 03:23:10 -05:00
|
|
|
errCtx := map[string]any{"channel_id": args.ChannelId}
|
2020-06-02 12:28:29 -04:00
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(cr.NErr, &nfErr):
|
2024-11-21 03:23:10 -05:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(cr.NErr)
|
2020-06-02 12:28:29 -04:00
|
|
|
default:
|
2024-11-21 03:23:10 -05:00
|
|
|
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(cr.NErr)
|
2020-06-02 12:28:29 -04:00
|
|
|
}
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
2023-11-06 07:52:12 -05:00
|
|
|
channel := cr.Data
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
var cmd *model.Command
|
|
|
|
|
|
|
|
|
|
for _, teamCmd := range teamCmds {
|
|
|
|
|
if trigger == teamCmd.Trigger {
|
|
|
|
|
cmd = teamCmd
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
if cmd == nil {
|
|
|
|
|
return nil, nil, nil
|
|
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
rctx.Logger().Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p := url.Values{}
|
|
|
|
|
p.Set("token", cmd.Token)
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p.Set("team_id", cmd.TeamId)
|
|
|
|
|
p.Set("team_domain", team.Name)
|
2017-08-16 08:17:57 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p.Set("channel_id", args.ChannelId)
|
|
|
|
|
p.Set("channel_name", channel.Name)
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p.Set("user_id", args.UserId)
|
|
|
|
|
p.Set("user_name", user.Username)
|
2018-11-19 15:27:17 -05:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p.Set("command", "/"+trigger)
|
|
|
|
|
p.Set("text", message)
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
p.Set("trigger_id", args.TriggerId)
|
2025-08-26 14:57:19 -04:00
|
|
|
p.Set("root_id", args.RootId)
|
2017-11-09 15:46:20 -05:00
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
userMentionMap := a.MentionsToTeamMembers(rctx, message, team.Id)
|
2025-07-18 06:54:51 -04:00
|
|
|
maps.Copy(p, userMentionMap.ToURLValues())
|
2020-03-11 06:50:12 -04:00
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
channelMentionMap := a.MentionsToPublicChannels(rctx, message, team.Id)
|
2025-07-18 06:54:51 -04:00
|
|
|
maps.Copy(p, channelMentionMap.ToURLValues())
|
2020-03-11 06:50:12 -04:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
|
|
|
|
|
if appErr != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError).Wrap(appErr)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
|
2018-10-17 08:24:31 -04:00
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
return a.DoCommandRequest(rctx, cmd, p)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
2018-11-19 15:27:17 -05:00
|
|
|
|
2023-12-18 10:07:00 -05:00
|
|
|
func (a *App) DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2024-02-09 14:49:49 -05:00
|
|
|
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, cmd.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.Log().Error("Failed to find an outgoing oauth connection for the webhook", mlog.Err(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if connection != nil {
|
|
|
|
|
accessToken, err = a.OutgoingOAuthConnections().RetrieveTokenForConnection(rctx, connection)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.Log().Error("Failed to retrieve token for outgoing oauth connection", mlog.Err(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
// Prepare the request
|
|
|
|
|
var req *http.Request
|
|
|
|
|
var err error
|
2021-07-12 14:05:36 -04:00
|
|
|
if cmd.Method == model.CommandMethodGet {
|
2023-12-18 10:07:00 -05:00
|
|
|
req, err = http.NewRequestWithContext(ctx, http.MethodGet, cmd.URL, nil)
|
2019-01-09 17:07:08 -05:00
|
|
|
} else {
|
2023-12-18 10:07:00 -05:00
|
|
|
req, err = http.NewRequestWithContext(ctx, http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
2018-11-19 15:27:17 -05:00
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
2021-07-12 14:05:36 -04:00
|
|
|
if cmd.Method == model.CommandMethodGet {
|
2019-01-09 17:07:08 -05:00
|
|
|
if req.URL.RawQuery != "" {
|
|
|
|
|
req.URL.RawQuery += "&"
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
2019-01-09 17:07:08 -05:00
|
|
|
req.URL.RawQuery += p.Encode()
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2019-01-09 17:07:08 -05:00
|
|
|
req.Header.Set("Accept", "application/json")
|
2024-02-09 14:49:49 -05:00
|
|
|
if cmd.Token != "" {
|
|
|
|
|
req.Header.Set("Authorization", "Token "+cmd.Token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if accessToken != nil {
|
|
|
|
|
req.Header.Set("Authorization", accessToken.AsHeaderValue())
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-12 14:05:36 -04:00
|
|
|
if cmd.Method == model.CommandMethodPost {
|
2019-01-09 17:07:08 -05:00
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 23:09:22 -05:00
|
|
|
resp, err := a.Srv().outgoingWebhookClient.Do(req)
|
2019-01-09 17:07:08 -05:00
|
|
|
if err != nil {
|
2023-12-18 10:07:00 -05:00
|
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
|
|
|
rctx.Logger().Info("Outgoing Command request timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.")
|
|
|
|
|
}
|
2022-08-18 05:01:37 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
// Handle the response
|
|
|
|
|
body := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
|
2022-08-09 07:25:46 -04:00
|
|
|
bodyBytes, _ := io.ReadAll(body)
|
2019-01-09 17:07:08 -05:00
|
|
|
|
2022-07-05 02:46:50 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]any{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
|
|
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
2019-01-09 17:07:08 -05:00
|
|
|
} else if response == nil {
|
2022-07-05 02:46:50 -04:00
|
|
|
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
|
2019-01-09 17:07:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, response, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) HandleCommandResponse(rctx request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
|
2019-01-09 15:41:53 -05:00
|
|
|
trigger := ""
|
2020-12-22 08:50:59 -05:00
|
|
|
if args.Command != "" {
|
2019-01-09 15:41:53 -05:00
|
|
|
parts := strings.Split(args.Command, " ")
|
|
|
|
|
trigger = parts[0][1:]
|
|
|
|
|
trigger = strings.ToLower(trigger)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var lastError *model.AppError
|
2025-09-10 09:11:32 -04:00
|
|
|
_, err := a.HandleCommandResponsePost(rctx, command, args, response, builtIn)
|
2019-01-09 15:41:53 -05:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-09-10 09:11:32 -04:00
|
|
|
rctx.Logger().Debug("Error occurred in handling command response post", mlog.Err(err))
|
2019-01-09 15:41:53 -05:00
|
|
|
lastError = err
|
|
|
|
|
}
|
2018-11-19 11:00:50 -05:00
|
|
|
|
|
|
|
|
if response.ExtraResponses != nil {
|
|
|
|
|
for _, resp := range response.ExtraResponses {
|
2025-09-10 09:11:32 -04:00
|
|
|
_, err := a.HandleCommandResponsePost(rctx, command, args, resp, builtIn)
|
2019-01-09 15:41:53 -05:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-09-10 09:11:32 -04:00
|
|
|
rctx.Logger().Debug("Error occurred in handling command response post", mlog.Err(err))
|
2019-01-09 15:41:53 -05:00
|
|
|
lastError = err
|
|
|
|
|
}
|
2018-11-19 11:00:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-09 15:41:53 -05:00
|
|
|
if lastError != nil {
|
2022-07-05 02:46:50 -04:00
|
|
|
return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError)
|
2019-01-09 15:41:53 -05:00
|
|
|
}
|
|
|
|
|
|
2018-11-19 11:00:50 -05:00
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
func (a *App) HandleCommandResponsePost(rctx request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
|
2017-03-13 09:23:16 -04:00
|
|
|
post := &model.Post{}
|
|
|
|
|
post.ChannelId = args.ChannelId
|
|
|
|
|
post.RootId = args.RootId
|
|
|
|
|
post.UserId = args.UserId
|
2017-09-28 12:08:16 -04:00
|
|
|
post.Type = response.Type
|
2020-03-13 16:12:20 -04:00
|
|
|
post.SetProps(response.Props)
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2020-12-22 08:50:59 -05:00
|
|
|
if response.ChannelId != "" {
|
2025-09-10 09:11:32 -04:00
|
|
|
_, err := a.GetChannelMember(rctx, response.ChannelId, args.UserId)
|
2019-01-09 15:41:53 -05:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, "", http.StatusForbidden).Wrap(err)
|
2019-01-09 15:41:53 -05:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2018-12-12 01:43:31 -05:00
|
|
|
post.ChannelId = response.ChannelId
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-23 14:26:31 -05:00
|
|
|
isBotPost := !builtIn
|
2017-03-13 09:23:16 -04:00
|
|
|
|
2019-01-31 08:12:01 -05:00
|
|
|
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
|
2020-12-22 08:50:59 -05:00
|
|
|
if command.Username != "" {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsOverrideUsername, command.Username)
|
2018-01-23 14:26:31 -05:00
|
|
|
isBotPost = true
|
2020-12-22 08:50:59 -05:00
|
|
|
} else if response.Username != "" {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsOverrideUsername, response.Username)
|
2018-01-23 14:26:31 -05:00
|
|
|
isBotPost = true
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-31 08:12:01 -05:00
|
|
|
if *a.Config().ServiceSettings.EnablePostIconOverride {
|
2020-12-22 08:50:59 -05:00
|
|
|
if command.IconURL != "" {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsOverrideIconURL, command.IconURL)
|
2018-01-23 14:26:31 -05:00
|
|
|
isBotPost = true
|
2020-12-22 08:50:59 -05:00
|
|
|
} else if response.IconURL != "" {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsOverrideIconURL, response.IconURL)
|
2018-01-23 14:26:31 -05:00
|
|
|
isBotPost = true
|
2017-03-13 09:23:16 -04:00
|
|
|
} else {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsOverrideIconURL, "")
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-23 14:26:31 -05:00
|
|
|
if isBotPost {
|
2025-03-20 07:53:50 -04:00
|
|
|
post.AddProp(model.PostPropsFromWebhook, "true")
|
2018-01-23 14:26:31 -05:00
|
|
|
}
|
|
|
|
|
|
2020-01-17 02:34:11 -05:00
|
|
|
// Process Slack text replacements if the response does not contain "skip_slack_parsing": true.
|
|
|
|
|
if !response.SkipSlackParsing {
|
2025-09-18 10:14:24 -04:00
|
|
|
response.Text = a.ProcessSlackText(rctx, response.Text)
|
|
|
|
|
response.Attachments = a.ProcessSlackAttachments(rctx, response.Attachments)
|
2019-10-22 08:39:49 -04:00
|
|
|
}
|
2017-11-17 11:17:59 -05:00
|
|
|
|
2025-09-10 09:11:32 -04:00
|
|
|
if _, err := a.CreateCommandPost(rctx, post, args.TeamId, response, response.SkipSlackParsing); err != nil {
|
2019-01-09 15:41:53 -05:00
|
|
|
return post, err
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2018-12-12 01:43:31 -05:00
|
|
|
return post, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2017-09-06 18:12:54 -04:00
|
|
|
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-31 11:40:15 -04:00
|
|
|
return a.createCommand(cmd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) {
|
2017-03-13 09:23:16 -04:00
|
|
|
cmd.Trigger = strings.ToLower(cmd.Trigger)
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
teamCmds, err := a.Srv().Store().Command().GetByTeam(cmd.TeamId)
|
2019-04-29 04:20:52 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, existingCommand := range teamCmds {
|
|
|
|
|
if cmd.Trigger == existingCommand.Trigger {
|
|
|
|
|
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
2018-10-17 08:24:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, builtInProvider := range commandProviders {
|
2021-02-26 02:12:49 -05:00
|
|
|
builtInCommand := builtInProvider.GetCommand(a, i18n.T)
|
2018-10-17 08:24:31 -04:00
|
|
|
if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
|
|
|
|
|
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
command, nErr := a.Srv().Store().Command().Save(cmd)
|
2020-07-16 09:26:07 -04:00
|
|
|
if nErr != nil {
|
|
|
|
|
var appErr *model.AppError
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(nErr, &appErr):
|
|
|
|
|
return nil, appErr
|
|
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return command, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-05 05:22:27 -05:00
|
|
|
func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
command, err := a.Srv().Store().Command().Get(commandID)
|
2019-05-06 12:12:41 -04:00
|
|
|
if err != nil {
|
2020-07-16 09:26:07 -04:00
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(err, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("SqlCommandStore.Get", "store.sql_command.get.missing.app_error", map[string]any{"command_id": commandID}, "", http.StatusNotFound).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("GetCommand", "app.command.getcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
2020-07-16 09:26:07 -04:00
|
|
|
return command, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2017-09-06 18:12:54 -04:00
|
|
|
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
|
|
|
|
|
updatedCmd.Id = oldCmd.Id
|
|
|
|
|
updatedCmd.Token = oldCmd.Token
|
|
|
|
|
updatedCmd.CreateAt = oldCmd.CreateAt
|
|
|
|
|
updatedCmd.UpdateAt = model.GetMillis()
|
|
|
|
|
updatedCmd.DeleteAt = oldCmd.DeleteAt
|
|
|
|
|
updatedCmd.CreatorId = oldCmd.CreatorId
|
2020-07-31 11:40:15 -04:00
|
|
|
updatedCmd.PluginId = oldCmd.PluginId
|
2017-03-13 09:23:16 -04:00
|
|
|
updatedCmd.TeamId = oldCmd.TeamId
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
command, err := a.Srv().Store().Command().Update(updatedCmd)
|
2020-07-16 09:26:07 -04:00
|
|
|
if err != nil {
|
|
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
var appErr *model.AppError
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(err, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": updatedCmd.Id}, "", http.StatusNotFound).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
case errors.As(err, &appErr):
|
|
|
|
|
return nil, appErr
|
|
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("UpdateCommand", "app.command.updatecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return command, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2017-10-04 14:08:59 -04:00
|
|
|
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
|
|
|
|
|
command.TeamId = team.Id
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
_, err := a.Srv().Store().Command().Update(command)
|
2019-04-29 10:22:44 -04:00
|
|
|
if err != nil {
|
2020-07-16 09:26:07 -04:00
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
var appErr *model.AppError
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(err, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": command.Id}, "", http.StatusNotFound).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
case errors.As(err, &appErr):
|
|
|
|
|
return appErr
|
|
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return model.NewAppError("MoveCommand", "app.command.movecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
2017-10-04 14:08:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-06 18:12:54 -04:00
|
|
|
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Token = model.NewId()
|
|
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
command, err := a.Srv().Store().Command().Update(cmd)
|
2020-07-16 09:26:07 -04:00
|
|
|
if err != nil {
|
|
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
var appErr *model.AppError
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(err, &nfErr):
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": cmd.Id}, "", http.StatusNotFound).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
case errors.As(err, &appErr):
|
|
|
|
|
return nil, appErr
|
|
|
|
|
default:
|
2022-08-18 05:01:37 -04:00
|
|
|
return nil, model.NewAppError("RegenCommandToken", "app.command.regencommandtoken.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return command, nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-05 05:22:27 -05:00
|
|
|
func (a *App) DeleteCommand(commandID string) *model.AppError {
|
2017-10-18 18:36:43 -04:00
|
|
|
if !*a.Config().ServiceSettings.EnableCommands {
|
2017-03-13 09:23:16 -04:00
|
|
|
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
|
|
|
}
|
2019-05-01 03:10:27 -04:00
|
|
|
|
2022-10-06 04:04:21 -04:00
|
|
|
err := a.Srv().Store().Command().Delete(commandID, model.GetMillis())
|
2020-07-16 09:26:07 -04:00
|
|
|
if err != nil {
|
2022-08-18 05:01:37 -04:00
|
|
|
return model.NewAppError("DeleteCommand", "app.command.deletecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
2020-07-16 09:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2017-03-13 09:23:16 -04:00
|
|
|
}
|
2021-07-13 09:56:20 -04:00
|
|
|
|
|
|
|
|
// possibleAtMentions returns all substrings in message that look like valid @
|
|
|
|
|
// mentions.
|
|
|
|
|
func possibleAtMentions(message string) []string {
|
|
|
|
|
var names []string
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(message, "@") {
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alreadyMentioned := make(map[string]bool)
|
|
|
|
|
for _, match := range atMentionRegexp.FindAllString(message, -1) {
|
|
|
|
|
name := model.NormalizeUsername(match[1:])
|
|
|
|
|
if !alreadyMentioned[name] && model.IsValidUsernameAllowRemote(name) {
|
|
|
|
|
names = append(names, name)
|
|
|
|
|
alreadyMentioned[name] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// trimUsernameSpecialChar tries to remove the last character from word if it
|
|
|
|
|
// is a special character for usernames (dot, dash or underscore). If not, it
|
|
|
|
|
// returns the same string.
|
|
|
|
|
func trimUsernameSpecialChar(word string) (string, bool) {
|
2023-06-13 04:38:36 -04:00
|
|
|
l := len(word)
|
2021-07-13 09:56:20 -04:00
|
|
|
|
2023-06-13 04:38:36 -04:00
|
|
|
if l > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (l-1) {
|
|
|
|
|
return word[:l-1], true
|
2021-07-13 09:56:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return word, false
|
|
|
|
|
}
|