mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Mattermost Blocks * Rethink some block props and styles * Remove obsolete comment * Style fixes and add examples * Add mention, search, and image metadata support * Several UI improvements * Validate blocks by translating them * Add initial e2e tests * Fix lint * Fix tests and lint * Fix lint * Improve e2e and simplify doPostAction * server side cleanup * Fix typo * Fix style * Fix some tests * Fix more tests * Fix cypress tests * Fix playwhright tests * Fix flaky system console test * Frontend cleanup * Add blocks editor * Fix CI * Limit all blocks width * Fix test * Enable webhook service on playwhright CI * fix flaky test * Fix style * Cleanups * Address coderabbit comments * Address coderabbit comments * Address CI errors * Fix tests * Address changes in valid actions * UX fixes * Fix test * Fix danger buttons and add allow emojis in buttons * style tweaks use semantic color tokens for buttons and accents, update max-heights for scollables, adjustments to containers, removed btn-sm (using default button size for mm blocks with height and padding adjustments for use within blocks. removed hover selector from utils to prevent hover state issue with primary button, refined semantic-color-warning. * Address feedback * Add interactive content to post previews * Add collapse transitions * Address autocomplete position * Handle columns within column sets * Fix css lint * Fix tests * Use feature flag * Address feedback * Fix lint and add missing tests * Fix e2e tests * Fix e2e test * Allow interactive posts without a message * Fix lint * Address feedback * Fix ci * Fix lint * Add limits to block translations * Fix lint * Address feedback * Address feedback * Update snapshot * Fix collapsible not defaulting to the right value * Add total text limit * Sync translations with mobile * Fix snapshot --------- Co-authored-by: Matthew Birtch <[email protected]> Co-authored-by: Mattermost Build <[email protected]>
504 lines
18 KiB
Go
504 lines
18 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
|
|
"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"
|
|
)
|
|
|
|
// maxMmBlocksActionsCloneDepth caps recursion in cloneMmBlocksActionsProp.
|
|
// ValidateMmBlocksActions bounds top-level entry count and key length but
|
|
// does not bound nesting depth inside spec.Context — a bot/plugin could
|
|
// otherwise stash a pathologically nested object that drives stack
|
|
// exhaustion on the restore path. 64 is well past any plausible legitimate
|
|
// nesting; deeper input is treated as malicious and truncated.
|
|
const maxMmBlocksActionsCloneDepth = 64
|
|
|
|
// cloneMmBlocksActionsProp deep-clones the post.props.mm_blocks_actions value.
|
|
// Each per-action entry can carry nested context / query maps (and arrays
|
|
// inside those), so the clone walks the structure recursively — a shallow
|
|
// clone at any level would leave nested objects aliased back to the live
|
|
// post's props, defeating the restore-after-invalid-response guarantee.
|
|
func cloneMmBlocksActionsProp(v any) any {
|
|
return cloneMmBlocksActionsPropAt(v, 0)
|
|
}
|
|
|
|
func cloneMmBlocksActionsPropAt(v any, depth int) any {
|
|
if depth > maxMmBlocksActionsCloneDepth {
|
|
// Defense-in-depth: drop the subtree rather than risk stack
|
|
// exhaustion. The restore path that calls this helper is on a
|
|
// rare branch (plugin response is invalid), and pathological
|
|
// nesting at this depth is not a legitimate use case.
|
|
return nil
|
|
}
|
|
switch typed := v.(type) {
|
|
case map[string]any:
|
|
out := make(map[string]any, len(typed))
|
|
for k, child := range typed {
|
|
out[k] = cloneMmBlocksActionsPropAt(child, depth+1)
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make([]any, len(typed))
|
|
for i, child := range typed {
|
|
out[i] = cloneMmBlocksActionsPropAt(child, depth+1)
|
|
}
|
|
return out
|
|
default:
|
|
// Scalars (string/number/bool/nil) are immutable — safe to share.
|
|
return v
|
|
}
|
|
}
|
|
|
|
// postActionSetup holds resolved state for one interactive post action before the upstream HTTP call.
|
|
type postActionSetup struct {
|
|
upstreamURL string
|
|
upstreamRequest *model.PostActionIntegrationRequest
|
|
datasource string
|
|
retain map[string]any
|
|
remove []string
|
|
originalProps map[string]any
|
|
originalIsPinned bool
|
|
originalHasReactions bool
|
|
rootPostId string
|
|
ephemeralInteractive bool
|
|
ephemeralChannelID string
|
|
ephemeralRootPostID string
|
|
ephemeralSetUserID bool
|
|
}
|
|
|
|
func (a *App) resolvePostActionSetup(
|
|
rctx request.CTX,
|
|
postID, actionID, userID string,
|
|
legacyCookie *model.PostActionCookie,
|
|
mmBlocksCookie *model.MmBlocksActionCookie,
|
|
clientQuery map[string]string,
|
|
integrationFormat string,
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
upstreamRequest := &model.PostActionIntegrationRequest{
|
|
UserId: userID,
|
|
PostId: postID,
|
|
}
|
|
|
|
userChan := a.startUpstreamUserFetch(userID)
|
|
postChan := a.startUpstreamPostFetch(rctx, postID)
|
|
channelChan := a.startUpstreamChannelFetch(postID)
|
|
|
|
postResult := <-postChan
|
|
if postResult.NErr != nil {
|
|
setup, gotoURL, appErr := a.resolvePostActionSetupFromCookies(postID, actionID, legacyCookie, mmBlocksCookie, clientQuery, upstreamRequest, postResult)
|
|
return a.finishPostActionSetup(setup, gotoURL, appErr, userChan)
|
|
}
|
|
|
|
post := postResult.Data
|
|
chResult := <-channelChan
|
|
if chResult.NErr != nil {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "app.channel.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(chResult.NErr)
|
|
}
|
|
|
|
setup, gotoURL, appErr := a.resolvePostActionSetupFromPost(post, chResult.Data, actionID, clientQuery, integrationFormat, upstreamRequest)
|
|
return a.finishPostActionSetup(setup, gotoURL, appErr, userChan)
|
|
}
|
|
|
|
func (a *App) startUpstreamPostFetch(rctx request.CTX, postID string) <-chan store.StoreResult[*model.Post] {
|
|
postChan := make(chan store.StoreResult[*model.Post], 1)
|
|
go func() {
|
|
post, err := a.Srv().Store().Post().GetSingle(rctx, postID, false)
|
|
postChan <- store.StoreResult[*model.Post]{Data: post, NErr: err}
|
|
close(postChan)
|
|
}()
|
|
return postChan
|
|
}
|
|
|
|
func (a *App) startUpstreamChannelFetch(postID string) <-chan store.StoreResult[*model.Channel] {
|
|
channelChan := make(chan store.StoreResult[*model.Channel], 1)
|
|
go func() {
|
|
channel, err := a.Srv().Store().Channel().GetForPost(postID)
|
|
channelChan <- store.StoreResult[*model.Channel]{Data: channel, NErr: err}
|
|
close(channelChan)
|
|
}()
|
|
return channelChan
|
|
}
|
|
|
|
func (a *App) startUpstreamUserFetch(userID string) <-chan store.StoreResult[*model.User] {
|
|
userChan := make(chan store.StoreResult[*model.User], 1)
|
|
go func() {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
userChan <- store.StoreResult[*model.User]{Data: user, NErr: err}
|
|
close(userChan)
|
|
}()
|
|
return userChan
|
|
}
|
|
|
|
func (a *App) startUpstreamTeamFetch(teamID string) <-chan store.StoreResult[*model.Team] {
|
|
if teamID == "" {
|
|
return nil
|
|
}
|
|
|
|
teamChan := make(chan store.StoreResult[*model.Team], 1)
|
|
go func() {
|
|
defer close(teamChan)
|
|
|
|
team, err := a.Srv().Store().Team().Get(teamID)
|
|
teamChan <- store.StoreResult[*model.Team]{Data: team, NErr: err}
|
|
}()
|
|
return teamChan
|
|
}
|
|
|
|
func (a *App) populateUpstreamRequestIdentity(
|
|
upstreamRequest *model.PostActionIntegrationRequest,
|
|
userChan <-chan store.StoreResult[*model.User],
|
|
) *model.AppError {
|
|
teamChan := a.startUpstreamTeamFetch(upstreamRequest.TeamId)
|
|
|
|
ur := <-userChan
|
|
if ur.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(ur.NErr, &nfErr):
|
|
return model.NewAppError("DoPostActionWithCookie", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
|
|
default:
|
|
return model.NewAppError("DoPostActionWithCookie", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
|
|
}
|
|
}
|
|
upstreamRequest.UserName = ur.Data.Username
|
|
|
|
if teamChan != nil {
|
|
if tr, ok := <-teamChan; ok {
|
|
if tr.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(tr.NErr, &nfErr):
|
|
return model.NewAppError("DoPostActionWithCookie", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
|
|
default:
|
|
return model.NewAppError("DoPostActionWithCookie", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
|
|
}
|
|
}
|
|
upstreamRequest.TeamName = tr.Data.Name
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) finishPostActionSetup(
|
|
setup *postActionSetup,
|
|
gotoURL string,
|
|
appErr *model.AppError,
|
|
userChan <-chan store.StoreResult[*model.User],
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
if appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
if gotoURL != "" {
|
|
return nil, gotoURL, nil
|
|
}
|
|
if appErr := a.populateUpstreamRequestIdentity(setup.upstreamRequest, userChan); appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
return setup, "", nil
|
|
}
|
|
|
|
func (a *App) resolvePostActionSetupFromCookies(
|
|
postID, actionID string,
|
|
legacyCookie *model.PostActionCookie,
|
|
mmBlocksCookie *model.MmBlocksActionCookie,
|
|
clientQuery map[string]string,
|
|
upstreamRequest *model.PostActionIntegrationRequest,
|
|
postResult store.StoreResult[*model.Post],
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
if legacyCookie == nil && mmBlocksCookie == nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(postResult.NErr, &nfErr):
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(postResult.NErr)
|
|
default:
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(postResult.NErr)
|
|
}
|
|
}
|
|
if legacyCookie != nil && mmBlocksCookie != nil {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "multiple action cookies set", http.StatusBadRequest)
|
|
}
|
|
|
|
var nfPost *store.ErrNotFound
|
|
ephemeral := errors.As(postResult.NErr, &nfPost)
|
|
|
|
if legacyCookie != nil {
|
|
return a.setupFromLegacyCookie(postID, legacyCookie, upstreamRequest, ephemeral)
|
|
}
|
|
return a.setupFromMmBlocksCookie(postID, actionID, mmBlocksCookie, clientQuery, upstreamRequest, ephemeral)
|
|
}
|
|
|
|
func (a *App) setupFromLegacyCookie(
|
|
postID string,
|
|
cookie *model.PostActionCookie,
|
|
upstreamRequest *model.PostActionIntegrationRequest,
|
|
ephemeral bool,
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
if cookie.Integration == nil {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "no Integration in action cookie", http.StatusBadRequest)
|
|
}
|
|
if postID != cookie.PostId {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
|
|
}
|
|
|
|
channel, appErr := a.channelForPostAction(cookie.ChannelId)
|
|
if appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
|
|
fillUpstreamChannel(upstreamRequest, channel, cookie.ChannelId)
|
|
upstreamRequest.Type = cookie.Type
|
|
// Clone the Context map — later code may add selected_option to it, and
|
|
// we must not mutate the shared source.
|
|
upstreamRequest.Context = maps.Clone(cookie.Integration.Context)
|
|
|
|
setup := &postActionSetup{
|
|
upstreamURL: cookie.Integration.URL,
|
|
upstreamRequest: upstreamRequest,
|
|
datasource: cookie.DataSource,
|
|
retain: cookie.RetainProps,
|
|
remove: cookie.RemoveProps,
|
|
rootPostId: cookie.RootPostId,
|
|
ephemeralInteractive: ephemeral,
|
|
ephemeralChannelID: cookie.ChannelId,
|
|
ephemeralRootPostID: cookie.RootPostId,
|
|
ephemeralSetUserID: true,
|
|
}
|
|
return setup, "", nil
|
|
}
|
|
|
|
func (a *App) setupFromMmBlocksCookie(
|
|
postID, actionID string,
|
|
cookie *model.MmBlocksActionCookie,
|
|
clientQuery map[string]string,
|
|
upstreamRequest *model.PostActionIntegrationRequest,
|
|
ephemeral bool,
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
if cookie.Kind != model.MmBlocksActionCookieKind {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "invalid mm_blocks action cookie", http.StatusBadRequest)
|
|
}
|
|
if postID != cookie.PostId {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
|
|
}
|
|
|
|
channel, appErr := a.channelForPostAction(cookie.ChannelId)
|
|
if appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
|
|
mmSpec := cookie.ActionSpec(actionID)
|
|
resolved, err := model.ResolveMmBlocksAction(mmSpec, actionID, clientQuery)
|
|
if appErr := mmBlocksResolveAppError(actionID, err); appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
if resolved.OpenURLGoto != "" {
|
|
return nil, resolved.OpenURLGoto, nil
|
|
}
|
|
|
|
fillUpstreamChannel(upstreamRequest, channel, cookie.ChannelId)
|
|
upstreamRequest.Type = model.PostActionTypeButton
|
|
upstreamRequest.Context = resolved.Context
|
|
|
|
setup := &postActionSetup{
|
|
upstreamURL: resolved.ExternalURL,
|
|
upstreamRequest: upstreamRequest,
|
|
retain: cookie.RetainProps,
|
|
remove: cookie.RemoveProps,
|
|
rootPostId: cookie.RootPostId,
|
|
ephemeralInteractive: ephemeral,
|
|
ephemeralChannelID: cookie.ChannelId,
|
|
ephemeralRootPostID: cookie.RootPostId,
|
|
}
|
|
return setup, "", nil
|
|
}
|
|
|
|
func (a *App) resolvePostActionSetupFromPost(
|
|
post *model.Post,
|
|
channel *model.Channel,
|
|
actionID string,
|
|
clientQuery map[string]string,
|
|
integrationFormat string,
|
|
upstreamRequest *model.PostActionIntegrationRequest,
|
|
) (*postActionSetup, string, *model.AppError) {
|
|
fillUpstreamChannel(upstreamRequest, channel, post.ChannelId)
|
|
|
|
switch model.NormalizePostActionIntegrationFormat(integrationFormat) {
|
|
case model.PostActionIntegrationFormatMmBlock,
|
|
model.PostActionIntegrationFormatBlock,
|
|
model.PostActionIntegrationFormatCard:
|
|
if !a.Config().FeatureFlags.MmBlocksEnabled {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "mm_blocks are not enabled", http.StatusBadRequest)
|
|
}
|
|
mmSpec := post.GetMmBlocksActionSpec(actionID)
|
|
resolved, err := model.ResolveMmBlocksAction(mmSpec, actionID, clientQuery)
|
|
if appErr := mmBlocksResolveAppError(actionID, err); appErr != nil {
|
|
return nil, "", appErr
|
|
}
|
|
if resolved.OpenURLGoto != "" {
|
|
return nil, resolved.OpenURLGoto, nil
|
|
}
|
|
|
|
preserve := post.PostActionPreserveState()
|
|
upstreamRequest.Type = model.PostActionTypeButton
|
|
upstreamRequest.Context = resolved.Context
|
|
|
|
return &postActionSetup{
|
|
upstreamURL: resolved.ExternalURL,
|
|
upstreamRequest: upstreamRequest,
|
|
retain: preserve.Retain,
|
|
remove: preserve.Remove,
|
|
originalProps: preserve.OriginalProps,
|
|
originalIsPinned: preserve.OriginalIsPinned,
|
|
originalHasReactions: preserve.OriginalHasReactions,
|
|
rootPostId: preserve.RootPostId,
|
|
}, "", nil
|
|
|
|
case model.PostActionIntegrationFormatAttachment:
|
|
action := post.GetAction(actionID)
|
|
if action == nil || action.Integration == nil {
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
|
|
}
|
|
|
|
preserve := post.PostActionPreserveState()
|
|
upstreamRequest.Type = action.Type
|
|
// Clone the Context map — the action pointer returned from
|
|
// post.GetAction may alias post.props state. Mutating it directly
|
|
// would leak per-click values (selected_option) into the post's
|
|
// cached integration for subsequent clickers.
|
|
upstreamRequest.Context = maps.Clone(action.Integration.Context)
|
|
|
|
return &postActionSetup{
|
|
upstreamURL: action.Integration.URL,
|
|
upstreamRequest: upstreamRequest,
|
|
datasource: action.DataSource,
|
|
retain: preserve.Retain,
|
|
remove: preserve.Remove,
|
|
originalProps: preserve.OriginalProps,
|
|
originalIsPinned: preserve.OriginalIsPinned,
|
|
originalHasReactions: preserve.OriginalHasReactions,
|
|
rootPostId: preserve.RootPostId,
|
|
}, "", nil
|
|
}
|
|
|
|
return nil, "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("integration_context=%s", integrationFormat), http.StatusBadRequest)
|
|
}
|
|
|
|
func fillUpstreamChannel(req *model.PostActionIntegrationRequest, channel *model.Channel, channelID string) {
|
|
req.ChannelId = channelID
|
|
req.ChannelName = channel.Name
|
|
req.TeamId = channel.TeamId
|
|
}
|
|
|
|
func (a *App) channelForPostAction(channelID string) (*model.Channel, *model.AppError) {
|
|
channel, err := a.Srv().Store().Channel().Get(channelID, true)
|
|
if err != nil {
|
|
errCtx := map[string]any{"channel_id": channelID}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("DoPostActionWithCookie", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("DoPostActionWithCookie", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
func mmBlocksResolveAppError(actionID string, err error) *model.AppError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case errors.Is(err, model.ErrMmBlocksActionNotFound):
|
|
return model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("mm_blocks action_id=%s", actionID), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
|
|
func (a *App) applyPostActionUpdate(
|
|
rctx request.CTX,
|
|
setup *postActionSetup,
|
|
postID, userID string,
|
|
update *model.Post,
|
|
) *model.AppError {
|
|
update.Id = postID
|
|
|
|
if update.GetProps() == nil {
|
|
switch {
|
|
case setup.originalProps != nil:
|
|
update.SetProps(setup.originalProps)
|
|
case setup.ephemeralInteractive && len(setup.retain) > 0:
|
|
props := make(model.StringInterface)
|
|
maps.Copy(props, setup.retain)
|
|
update.SetProps(props)
|
|
default:
|
|
update.SetProps(setup.originalProps)
|
|
}
|
|
} else {
|
|
for key, value := range setup.retain {
|
|
update.AddProp(key, value)
|
|
}
|
|
for _, key := range setup.remove {
|
|
update.DelProp(key)
|
|
}
|
|
}
|
|
update.IsPinned = setup.originalIsPinned
|
|
update.HasReactions = setup.originalHasReactions
|
|
|
|
// Validate mm_blocks_actions on update responses. Since
|
|
// AllowMmBlocksActionsUpdate bypasses the non-integration guard in
|
|
// UpdatePost, and mm_blocks_actions are not in PostActionRetainPropKeys,
|
|
// a bad response would otherwise permanently replace the post's valid
|
|
// mm_blocks_actions. Keep the original value (if any) and log a warning
|
|
// so integration authors can diagnose.
|
|
if update.GetProp(model.PostPropsMmBlocksActions) != nil {
|
|
originalMmBlocks := any(nil)
|
|
if setup.originalProps != nil {
|
|
originalMmBlocks = setup.originalProps[model.PostPropsMmBlocksActions]
|
|
}
|
|
if originalMmBlocks == nil {
|
|
rctx.Logger().Info("Dropping mm_blocks_actions from plugin update response: original post had none",
|
|
mlog.String("post_id", postID),
|
|
mlog.String("url", setup.upstreamURL),
|
|
)
|
|
update.DelProp(model.PostPropsMmBlocksActions)
|
|
} else if err := model.ValidateMmBlocksActions(update); err != nil {
|
|
rctx.Logger().Info("Restoring original mm_blocks_actions: plugin update response was invalid",
|
|
mlog.String("post_id", postID),
|
|
mlog.String("url", setup.upstreamURL),
|
|
mlog.Err(err),
|
|
)
|
|
update.AddProp(model.PostPropsMmBlocksActions, cloneMmBlocksActionsProp(originalMmBlocks))
|
|
}
|
|
}
|
|
|
|
if setup.ephemeralInteractive {
|
|
update.ChannelId = setup.ephemeralChannelID
|
|
if update.RootId == "" && setup.ephemeralRootPostID != "" && setup.ephemeralRootPostID != postID {
|
|
update.RootId = setup.ephemeralRootPostID
|
|
}
|
|
if setup.ephemeralSetUserID && update.UserId == "" {
|
|
update.UserId = userID
|
|
}
|
|
a.UpdateEphemeralPost(rctx, userID, update)
|
|
return nil
|
|
}
|
|
|
|
if _, _, appErr := a.UpdatePost(rctx, update, &model.UpdatePostOptions{SafeUpdate: false, AllowMmBlocksActionsUpdate: true}); appErr != nil {
|
|
return appErr
|
|
}
|
|
return nil
|
|
}
|