Files
mostlymatter/app/command.go
Agniva De Sarker 021c90f29f 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 19:04:05 +05:30

765 строки
24 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"unicode"
goi18n "github.com/mattermost/go-i18n/i18n"
"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"
)
type CommandProvider interface {
GetTrigger() string
GetCommand(a *App, T goi18n.TranslateFunc) *model.Command
DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse
}
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
}
// @openTracingParams teamID, skipSlackParsing
func (a *App) CreateCommandPost(post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
if skipSlackParsing {
post.Message = response.Text
} else {
post.Message = model.ParseSlackLinksToMarkdown(response.Text)
}
post.CreateAt = model.GetMillis()
if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) {
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if response.Attachments != nil {
model.ParseSlackAttachment(post, response.Attachments)
}
if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL {
return a.CreatePostMissingChannel(post, true)
}
if (response.ResponseType == "" || response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL) && (response.Text != "" || response.Attachments != nil) {
post.ParentId = ""
a.SendEphemeralPost(post.UserId, post)
}
return post, nil
}
// @openTracingParams teamID
// previous ListCommands now ListAutocompleteCommands
func (a *App) ListAutocompleteCommands(teamID string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
for _, cmd := range a.PluginCommandsForTeam(teamID) {
if cmd.AutoComplete && !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store.Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAutocompleteCommands", "app.command.listautocompletecommands.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, cmd := range teamCmds {
if cmd.AutoComplete && !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
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)
}
}
}
return commands, nil
}
func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
teamCmds, err := a.Srv().Store.Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
return teamCmds, nil
}
func (a *App) ListAllCommands(teamID string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
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)
}
}
}
for _, cmd := range a.PluginCommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store.Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAllCommands", "app.command.listallcommands.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, cmd := range teamCmds {
if !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
return commands, nil
}
// @openTracingParams args
func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
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
}
trigger = strings.ToLower(trigger)
if !strings.HasPrefix(trigger, "/") {
return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusBadRequest)
}
trigger = strings.TrimPrefix(trigger, "/")
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
if appErr != nil {
mlog.Warn("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
}
args.TriggerId = triggerId
// Plugins can override built in and custom commands
cmd, response, appErr := a.tryExecutePluginCommand(args)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(cmd, args, response, true)
}
// Custom commands can override built ins
cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(cmd, args, response, false)
}
cmd, response = a.tryExecuteBuiltInCommand(args, trigger, message)
if cmd != nil && response != nil {
return a.HandleCommandResponse(cmd, args, response, true)
}
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound)
}
// MentionsToTeamMembers returns all the @ mentions found in message that
// belong to users in the specified team, linking them to their users
func (a *App) MentionsToTeamMembers(message, teamID string) model.UserMentionMap {
type mentionMapItem struct {
Name string
Id string
}
possibleMentions := model.PossibleAtMentions(message)
mentionChan := make(chan *mentionMapItem, len(possibleMentions))
var wg sync.WaitGroup
for _, mention := range possibleMentions {
wg.Add(1)
go func(mention string) {
defer wg.Done()
user, nErr := a.Srv().Store.User().GetByUsername(mention)
var nfErr *store.ErrNotFound
if nErr != nil && !errors.As(nErr, &nfErr) {
mlog.Warn("Failed to retrieve user @"+mention, mlog.Err(nErr))
return
}
// If it's a http.StatusNotFound error, check for usernames in substrings
// without trailing punctuation
if nErr != nil {
trimmed, ok := model.TrimUsernameSpecialChar(mention)
for ; ok; trimmed, ok = model.TrimUsernameSpecialChar(trimmed) {
userFromTrimmed, nErr := a.Srv().Store.User().GetByUsername(trimmed)
if nErr != nil && !errors.As(nErr, &nfErr) {
return
}
if nErr != nil {
continue
}
_, err := a.GetTeamMember(teamID, userFromTrimmed.Id)
if err != nil {
// The user is not in the team, so we should ignore it
return
}
mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
return
}
return
}
_, err := a.GetTeamMember(teamID, user.Id)
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 {
atMentionMap[mention.Name] = mention.Id
}
return atMentionMap
}
// MentionsToPublicChannels returns all the mentions to public channels,
// linking them to their channels
func (a *App) MentionsToPublicChannels(message, teamID string) model.ChannelMentionMap {
type mentionMapItem struct {
Name string
Id string
}
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()
channel, err := a.GetChannelByName(channelName, teamID, false)
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 {
channelMentionMap[mention.Name] = mention.Id
}
return channelMentionMap
}
// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
provider := GetCommandProvider(trigger)
if provider == nil {
return nil, nil
}
cmd := provider.GetCommand(a, args.T)
if cmd == nil {
return nil, nil
}
return cmd, provider.DoCommand(a, args, message)
}
// tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteCustomCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
// Handle custom commands
if !*a.Config().ServiceSettings.EnableCommands {
return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
chanChan := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store.Channel().Get(args.ChannelId, true)
chanChan <- store.StoreResult{Data: channel, NErr: err}
close(chanChan)
}()
teamChan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store.Team().Get(args.TeamId)
teamChan <- store.StoreResult{Data: team, NErr: err}
close(teamChan)
}()
userChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store.User().Get(context.Background(), args.UserId)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
teamCmds, err := a.Srv().Store.Command().GetByTeam(args.TeamId)
if err != nil {
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.command.tryexecutecustomcommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
tr := <-teamChan
if tr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(tr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.find.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.finding.app_error", nil, tr.NErr.Error(), http.StatusInternalServerError)
}
}
team := tr.Data.(*model.Team)
ur := <-userChan
if ur.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(ur.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.user.get.app_error", nil, ur.NErr.Error(), http.StatusInternalServerError)
}
}
user := ur.Data.(*model.User)
cr := <-chanChan
if cr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.find.app_error", nil, cr.NErr.Error(), http.StatusInternalServerError)
}
}
channel := cr.Data.(*model.Channel)
var cmd *model.Command
for _, teamCmd := range teamCmds {
if trigger == teamCmd.Trigger {
cmd = teamCmd
}
}
if cmd == nil {
return nil, nil, nil
}
mlog.Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
p := url.Values{}
p.Set("token", cmd.Token)
p.Set("team_id", cmd.TeamId)
p.Set("team_domain", team.Name)
p.Set("channel_id", args.ChannelId)
p.Set("channel_name", channel.Name)
p.Set("user_id", args.UserId)
p.Set("user_name", user.Username)
p.Set("command", "/"+trigger)
p.Set("text", message)
p.Set("trigger_id", args.TriggerId)
userMentionMap := a.MentionsToTeamMembers(message, team.Id)
for key, values := range userMentionMap.ToURLValues() {
p[key] = values
}
channelMentionMap := a.MentionsToPublicChannels(message, team.Id)
for key, values := range channelMentionMap.ToURLValues() {
p[key] = values
}
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
if appErr != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)
}
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
return a.DoCommandRequest(cmd, p)
}
func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
// Prepare the request
var req *http.Request
var err error
if cmd.Method == model.COMMAND_METHOD_GET {
req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
} else {
req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
}
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
}
if cmd.Method == model.COMMAND_METHOD_GET {
if req.URL.RawQuery != "" {
req.URL.RawQuery += "&"
}
req.URL.RawQuery += p.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Method == model.COMMAND_METHOD_POST {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
// Send the request
resp, err := a.HTTPService().MakeClient(false).Do(req)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
}
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
bodyBytes, _ := ioutil.ReadAll(body)
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError)
}
response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
} else if response == nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
}
return cmd, response, nil
}
func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
trigger := ""
if args.Command != "" {
parts := strings.Split(args.Command, " ")
trigger = parts[0][1:]
trigger = strings.ToLower(trigger)
}
var lastError *model.AppError
_, err := a.HandleCommandResponsePost(command, args, response, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
if response.ExtraResponses != nil {
for _, resp := range response.ExtraResponses {
_, err := a.HandleCommandResponsePost(command, args, resp, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
}
}
if lastError != nil {
return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
}
return response, nil
}
func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.ParentId = args.ParentId
post.UserId = args.UserId
post.Type = response.Type
post.SetProps(response.Props)
if response.ChannelId != "" {
_, err := a.GetChannelMember(response.ChannelId, args.UserId)
if err != nil {
err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, err.Error(), http.StatusForbidden)
return nil, err
}
post.ChannelId = response.ChannelId
}
isBotPost := !builtIn
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
if command.Username != "" {
post.AddProp("override_username", command.Username)
isBotPost = true
} else if response.Username != "" {
post.AddProp("override_username", response.Username)
isBotPost = true
}
}
if *a.Config().ServiceSettings.EnablePostIconOverride {
if command.IconURL != "" {
post.AddProp("override_icon_url", command.IconURL)
isBotPost = true
} else if response.IconURL != "" {
post.AddProp("override_icon_url", response.IconURL)
isBotPost = true
} else {
post.AddProp("override_icon_url", "")
}
}
if isBotPost {
post.AddProp("from_webhook", "true")
}
// Process Slack text replacements if the response does not contain "skip_slack_parsing": true.
if !response.SkipSlackParsing {
response.Text = a.ProcessSlackText(response.Text)
response.Attachments = a.ProcessSlackAttachments(response.Attachments)
}
if _, err := a.CreateCommandPost(post, args.TeamId, response, response.SkipSlackParsing); err != nil {
return post, err
}
return post, nil
}
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
return a.createCommand(cmd)
}
func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) {
cmd.Trigger = strings.ToLower(cmd.Trigger)
teamCmds, err := a.Srv().Store.Command().GetByTeam(cmd.TeamId)
if err != nil {
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, existingCommand := range teamCmds {
if cmd.Trigger == existingCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
for _, builtInProvider := range commandProviders {
builtInCommand := builtInProvider.GetCommand(a, utils.T)
if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
command, nErr := a.Srv().Store.Command().Save(cmd)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
return command, nil
}
func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
command, err := a.Srv().Store.Command().Get(commandID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Get", "store.sql_command.get.missing.app_error", map[string]interface{}{"command_id": commandID}, "", http.StatusNotFound)
default:
return nil, model.NewAppError("GetCommand", "app.command.getcommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return command, nil
}
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
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
updatedCmd.PluginId = oldCmd.PluginId
updatedCmd.TeamId = oldCmd.TeamId
command, err := a.Srv().Store.Command().Update(updatedCmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]interface{}{"command_id": updatedCmd.Id}, "", http.StatusNotFound)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateCommand", "app.command.updatecommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return command, nil
}
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
command.TeamId = team.Id
_, err := a.Srv().Store.Command().Update(command)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]interface{}{"command_id": command.Id}, "", http.StatusNotFound)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("MoveCommand", "app.command.movecommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return nil
}
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
cmd.Token = model.NewId()
command, err := a.Srv().Store.Command().Update(cmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]interface{}{"command_id": cmd.Id}, "", http.StatusNotFound)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("RegenCommandToken", "app.command.regencommandtoken.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return command, nil
}
func (a *App) DeleteCommand(commandID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableCommands {
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
err := a.Srv().Store.Command().Delete(commandID, model.GetMillis())
if err != nil {
return model.NewAppError("DeleteCommand", "app.command.deletecommand.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}