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]>
797 lines
20 KiB
Go
797 lines
20 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/markdown"
|
|
)
|
|
|
|
func appendHumanReadableInteractiveStrings(o *Post, out []string) []string {
|
|
props := o.GetProps()
|
|
if props == nil {
|
|
return out
|
|
}
|
|
if raw, ok := props[PostPropsMmBlocks]; ok {
|
|
out = appendHumanStringsFromMmBlocks(raw, out)
|
|
}
|
|
if raw, ok := props[PostPropsBlockKitBlocks]; ok {
|
|
out = appendHumanStringsFromBlockKitTree(raw, out)
|
|
}
|
|
if raw, ok := props[PostPropsAdaptiveCards]; ok {
|
|
out = appendHumanStringsFromAdaptiveCardsTree(raw, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func interactivePropJSONArray(raw any) ([]any, bool) {
|
|
switch v := raw.(type) {
|
|
case []any:
|
|
return v, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func interactivePropJSONArrayNonEmpty(raw any) bool {
|
|
arr, ok := interactivePropJSONArray(raw)
|
|
return ok && len(arr) > 0
|
|
}
|
|
|
|
func appendHumanStringsFromMmBlocks(raw any, out []string) []string {
|
|
blocks, ok := interactivePropJSONArray(raw)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, b := range blocks {
|
|
m, ok := b.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = appendHumanStringsFromMmBlockMap(m, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendHumanStringsFromMmBlockMap(m map[string]any, out []string) []string {
|
|
typ, _ := m["type"].(string)
|
|
switch typ {
|
|
case "text":
|
|
if s, ok := m["text"].(string); ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, s)
|
|
}
|
|
case "container":
|
|
out = appendHumanStringsFromMmBlocksArray(m["content"], out)
|
|
case "collapsible":
|
|
out = appendHumanStringsFromMmBlocksArray(m["header"], out)
|
|
out = appendHumanStringsFromMmBlocksArray(m["content"], out)
|
|
case "column_set":
|
|
if cols, ok := m["columns"].([]any); ok {
|
|
for _, col := range cols {
|
|
cm, ok := col.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
colItems, ok := cm["items"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = appendHumanStringsFromMmBlocksArray(colItems, out)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendHumanStringsFromMmBlocksArray(raw any, out []string) []string {
|
|
arr, ok := interactivePropJSONArray(raw)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, el := range arr {
|
|
m, ok := el.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = appendHumanStringsFromMmBlockMap(m, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendHumanStringsFromBlockKitTree(v any, out []string) []string {
|
|
blocks, ok := v.([]any)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, block := range blocks {
|
|
blockMap, ok := block.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
typ, _ := blockMap["type"].(string)
|
|
switch typ {
|
|
case "markdown":
|
|
if s, ok := blockMap["text"].(string); ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, s)
|
|
}
|
|
case "section":
|
|
if textBlock, ok := blockMap["text"].(map[string]any); ok {
|
|
if s, ok := textBlock["text"].(string); ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, s)
|
|
}
|
|
}
|
|
if fields, ok := blockMap["fields"].([]any); ok {
|
|
for _, field := range fields {
|
|
fieldMap, ok := field.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fieldText, ok := fieldMap["text"].(string)
|
|
if ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, fieldText)
|
|
}
|
|
}
|
|
}
|
|
case "header":
|
|
if textBlock, ok := blockMap["text"].(map[string]any); ok {
|
|
if s, ok := textBlock["text"].(string); ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendHumanStringsFromAdaptiveCardsTree(v any, out []string) []string {
|
|
cards, ok := v.([]any)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, card := range cards {
|
|
cardMap, ok := card.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
body, ok := cardMap["body"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range body {
|
|
out = appendHumanStringsFromAdaptiveCardsItem(item, out)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendHumanStringsFromAdaptiveCardsItem(item any, out []string) []string {
|
|
itemMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
return out
|
|
}
|
|
typ, _ := itemMap["type"].(string)
|
|
switch typ {
|
|
case "TextBlock":
|
|
if s, ok := itemMap["text"].(string); ok {
|
|
out = appendNonWhitespaceOnlyMessage(out, s)
|
|
}
|
|
case "Container":
|
|
if items, ok := itemMap["items"].([]any); ok {
|
|
for _, item := range items {
|
|
out = appendHumanStringsFromAdaptiveCardsItem(item, out)
|
|
}
|
|
}
|
|
case "ColumnSet":
|
|
if columns, ok := itemMap["columns"].([]any); ok {
|
|
for _, column := range columns {
|
|
columnMap, ok := column.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
itemMap, ok := columnMap["items"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range itemMap {
|
|
out = appendHumanStringsFromAdaptiveCardsItem(item, out)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendMmBlockImageURLs(out []string, raw any) []string {
|
|
blocks, ok := interactivePropJSONArray(raw)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, b := range blocks {
|
|
m, ok := b.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = appendMmBlockMapImageURLs(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendMmBlockMapImageURLs(out []string, m map[string]any) []string {
|
|
typ, _ := m["type"].(string)
|
|
switch typ {
|
|
case "image":
|
|
if u, ok := m["url"].(string); ok {
|
|
out = append(out, u)
|
|
}
|
|
case "container":
|
|
out = appendMmBlocksArrayImageURLs(out, m["content"])
|
|
case "collapsible":
|
|
out = appendMmBlocksArrayImageURLs(out, m["header"])
|
|
out = appendMmBlocksArrayImageURLs(out, m["content"])
|
|
case "column_set":
|
|
if cols, ok := m["columns"].([]any); ok {
|
|
for _, col := range cols {
|
|
cm, ok := col.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
colItems, ok := cm["items"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range colItems {
|
|
out = appendMmBlocksArrayImageURLs(out, item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendMmBlocksArrayImageURLs(out []string, raw any) []string {
|
|
arr, ok := interactivePropJSONArray(raw)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, el := range arr {
|
|
m, ok := el.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = appendMmBlockMapImageURLs(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendBlockKitImageURLs(out []string, v any) []string {
|
|
blocks, ok := interactivePropJSONArray(v)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, block := range blocks {
|
|
blockMap, ok := block.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
typ, _ := blockMap["type"].(string)
|
|
switch typ {
|
|
case "section":
|
|
if accessory, ok := blockMap["accessory"].(map[string]any); ok {
|
|
accessoryType, _ := accessory["type"].(string)
|
|
if accessoryType != "image" {
|
|
continue
|
|
}
|
|
|
|
if u, ok := accessory["image_url"].(string); ok {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
case "image":
|
|
if u, ok := blockMap["image_url"].(string); ok {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendAdaptiveCardImageURLs(out []string, v any) []string {
|
|
cards, ok := interactivePropJSONArray(v)
|
|
if !ok {
|
|
return out
|
|
}
|
|
for _, card := range cards {
|
|
cardMap, ok := card.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
body, ok := cardMap["body"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range body {
|
|
out = appendAdaptiveCardImageURLsFromItem(out, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendAdaptiveCardImageURLsFromItem(out []string, item any) []string {
|
|
itemMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
return out
|
|
}
|
|
typ, _ := itemMap["type"].(string)
|
|
switch typ {
|
|
case "Container":
|
|
if items, ok := itemMap["items"].([]any); ok {
|
|
for _, item := range items {
|
|
out = appendAdaptiveCardImageURLsFromItem(out, item)
|
|
}
|
|
}
|
|
case "ColumnSet":
|
|
if columns, ok := itemMap["columns"].([]any); ok {
|
|
for _, column := range columns {
|
|
columnMap, ok := column.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
items, ok := columnMap["items"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range items {
|
|
out = appendAdaptiveCardImageURLsFromItem(out, item)
|
|
}
|
|
}
|
|
}
|
|
case "Image":
|
|
if u, ok := itemMap["url"].(string); ok {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendAttachmentsImageURLs(out []string, attachments []*MessageAttachment) []string {
|
|
for _, attachment := range attachments {
|
|
if attachment == nil {
|
|
continue
|
|
}
|
|
if attachment.ImageURL != "" {
|
|
out = append(out, attachment.ImageURL)
|
|
}
|
|
if attachment.ThumbURL != "" {
|
|
out = append(out, attachment.ThumbURL)
|
|
}
|
|
if attachment.AuthorIcon != "" {
|
|
out = append(out, attachment.AuthorIcon)
|
|
}
|
|
if attachment.FooterIcon != "" {
|
|
out = append(out, attachment.FooterIcon)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
const mmactionScheme = "mmaction://"
|
|
|
|
// appendMmactionIDsFromText appends action ids from mmaction:// markdown links only.
|
|
// Inline code, fenced code blocks, and other non-link text are ignored (same approach as mentions).
|
|
func appendMmactionIDsFromText(ids map[string]struct{}, text string) map[string]struct{} {
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
markdown.Inspect(text, func(blockOrInline any) bool {
|
|
switch v := blockOrInline.(type) {
|
|
case *markdown.InlineLink:
|
|
ids = appendMmactionIDFromURL(ids, v.Destination())
|
|
case *markdown.ReferenceLink:
|
|
if v.ReferenceDefinition != nil {
|
|
ids = appendMmactionIDFromURL(ids, v.ReferenceDefinition.Destination())
|
|
}
|
|
case *markdown.Autolink:
|
|
ids = appendMmactionIDFromURL(ids, v.Destination())
|
|
}
|
|
return true
|
|
})
|
|
return ids
|
|
}
|
|
|
|
func appendMmactionIDFromURL(ids map[string]struct{}, url string) map[string]struct{} {
|
|
if !strings.HasPrefix(url, mmactionScheme) {
|
|
return ids
|
|
}
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
withoutScheme := url[len(mmactionScheme):]
|
|
actionID := withoutScheme
|
|
if i := strings.IndexAny(withoutScheme, "/?#"); i >= 0 {
|
|
actionID = withoutScheme[:i]
|
|
}
|
|
if actionID != "" && mmBlocksActionIDRegex.MatchString(actionID) {
|
|
ids[actionID] = struct{}{}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func mergeActionIDs(into, from map[string]struct{}) {
|
|
maps.Copy(into, from)
|
|
}
|
|
|
|
func interactiveControlDisabled(m map[string]any) bool {
|
|
disabled, ok := m["disabled"].(bool)
|
|
return ok && disabled
|
|
}
|
|
|
|
func appendMmBlockActionIDsFromMap(ids map[string]struct{}, m map[string]any) map[string]struct{} {
|
|
typ, _ := m["type"].(string)
|
|
switch typ {
|
|
case "text":
|
|
if s, ok := m["text"].(string); ok {
|
|
ids = appendMmactionIDsFromText(ids, s)
|
|
}
|
|
case "button", "static_select":
|
|
if interactiveControlDisabled(m) {
|
|
break
|
|
}
|
|
if id, ok := m["action_id"].(string); ok && id != "" {
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
ids[id] = struct{}{}
|
|
}
|
|
case "container":
|
|
ids = appendMmBlockActionIDsFromArray(ids, m["content"])
|
|
case "collapsible":
|
|
ids = appendMmBlockActionIDsFromArray(ids, m["header"])
|
|
ids = appendMmBlockActionIDsFromArray(ids, m["content"])
|
|
case "column_set":
|
|
if cols, ok := m["columns"].([]any); ok {
|
|
for _, col := range cols {
|
|
cm, ok := col.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
colTyp, _ := cm["type"].(string)
|
|
if colTyp != "column" {
|
|
continue
|
|
}
|
|
ids = appendMmBlockActionIDsFromArray(ids, cm["items"])
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendMmBlockActionIDsFromArray(ids map[string]struct{}, raw any) map[string]struct{} {
|
|
arr, ok := interactivePropJSONArray(raw)
|
|
if !ok {
|
|
return ids
|
|
}
|
|
for _, el := range arr {
|
|
m, ok := el.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ids = appendMmBlockActionIDsFromMap(ids, m)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectMmBlockActionIDs returns action_id values referenced by interactive mm_blocks controls.
|
|
func CollectMmBlockActionIDs(blocks []any) map[string]struct{} {
|
|
var ids map[string]struct{}
|
|
for _, b := range blocks {
|
|
m, ok := b.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ids = appendMmBlockActionIDsFromMap(ids, m)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendBlockKitTextMmaction(ids map[string]struct{}, raw any) map[string]struct{} {
|
|
if raw == nil {
|
|
return ids
|
|
}
|
|
switch v := raw.(type) {
|
|
case string:
|
|
return appendMmactionIDsFromText(ids, v)
|
|
case map[string]any:
|
|
if s, ok := v["text"].(string); ok {
|
|
return appendMmactionIDsFromText(ids, s)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendBlockKitAccessory(ids map[string]struct{}, accessory map[string]any) map[string]struct{} {
|
|
typ, _ := accessory["type"].(string)
|
|
switch typ {
|
|
case "button", "static_select":
|
|
if interactiveControlDisabled(accessory) {
|
|
return ids
|
|
}
|
|
if id, ok := accessory["action_id"].(string); ok && id != "" {
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
ids[id] = struct{}{}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendBlockKitActionElement(ids map[string]struct{}, el any) map[string]struct{} {
|
|
e, ok := el.(map[string]any)
|
|
if !ok {
|
|
return ids
|
|
}
|
|
typ, _ := e["type"].(string)
|
|
switch typ {
|
|
case "button", "static_select":
|
|
if interactiveControlDisabled(e) {
|
|
return ids
|
|
}
|
|
if id, ok := e["action_id"].(string); ok && id != "" {
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
ids[id] = struct{}{}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendBlockKitActionIDsFromBlock(ids map[string]struct{}, m map[string]any) map[string]struct{} {
|
|
typ, _ := m["type"].(string)
|
|
switch typ {
|
|
case "actions":
|
|
if elements, ok := m["elements"].([]any); ok {
|
|
for _, el := range elements {
|
|
ids = appendBlockKitActionElement(ids, el)
|
|
}
|
|
}
|
|
case "section":
|
|
ids = appendBlockKitTextMmaction(ids, m["text"])
|
|
if accessory, ok := m["accessory"].(map[string]any); ok {
|
|
ids = appendBlockKitAccessory(ids, accessory)
|
|
}
|
|
if fields, ok := m["fields"].([]any); ok {
|
|
for _, field := range fields {
|
|
ids = appendBlockKitTextMmaction(ids, field)
|
|
}
|
|
}
|
|
case "markdown":
|
|
ids = appendBlockKitTextMmaction(ids, m["text"])
|
|
case "header":
|
|
ids = appendBlockKitTextMmaction(ids, m["text"])
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectBlockKitActionIDs returns action_id values from Block Kit blocks (props.blocks).
|
|
func CollectBlockKitActionIDs(blocks []any) map[string]struct{} {
|
|
var ids map[string]struct{}
|
|
for _, b := range blocks {
|
|
m, ok := b.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ids = appendBlockKitActionIDsFromBlock(ids, m)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendAdaptiveCardActionElement(ids map[string]struct{}, action any) map[string]struct{} {
|
|
ac, ok := action.(map[string]any)
|
|
if !ok {
|
|
return ids
|
|
}
|
|
typ, _ := ac["type"].(string)
|
|
if typ == "Action.Submit" {
|
|
if id, ok := ac["id"].(string); ok && id != "" {
|
|
if ids == nil {
|
|
ids = make(map[string]struct{})
|
|
}
|
|
ids[id] = struct{}{}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func appendAdaptiveCardActionIDsFromItem(ids map[string]struct{}, item any) map[string]struct{} {
|
|
itemMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
return ids
|
|
}
|
|
typ, _ := itemMap["type"].(string)
|
|
switch typ {
|
|
case "TextBlock":
|
|
if s, ok := itemMap["text"].(string); ok {
|
|
ids = appendMmactionIDsFromText(ids, s)
|
|
}
|
|
case "Container":
|
|
if items, ok := itemMap["items"].([]any); ok {
|
|
for _, nested := range items {
|
|
ids = appendAdaptiveCardActionIDsFromItem(ids, nested)
|
|
}
|
|
}
|
|
case "ColumnSet":
|
|
if columns, ok := itemMap["columns"].([]any); ok {
|
|
for _, column := range columns {
|
|
columnMap, ok := column.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if items, ok := columnMap["items"].([]any); ok {
|
|
for _, nested := range items {
|
|
ids = appendAdaptiveCardActionIDsFromItem(ids, nested)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "ActionSet":
|
|
if actions, ok := itemMap["actions"].([]any); ok {
|
|
for _, action := range actions {
|
|
ids = appendAdaptiveCardActionElement(ids, action)
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectAdaptiveCardActionIDs returns action ids from Adaptive Cards (props.cards).
|
|
func CollectAdaptiveCardActionIDs(cards []any) map[string]struct{} {
|
|
var ids map[string]struct{}
|
|
for _, card := range cards {
|
|
cardMap, ok := card.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if body, ok := cardMap["body"].([]any); ok {
|
|
for _, item := range body {
|
|
ids = appendAdaptiveCardActionIDsFromItem(ids, item)
|
|
}
|
|
}
|
|
if actions, ok := cardMap["actions"].([]any); ok {
|
|
for _, action := range actions {
|
|
ids = appendAdaptiveCardActionElement(ids, action)
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectInteractiveActionIDs returns action ids referenced by interactive post props.
|
|
func CollectInteractiveActionIDs(props map[string]any) map[string]struct{} {
|
|
ids := make(map[string]struct{})
|
|
if props == nil {
|
|
return ids
|
|
}
|
|
if raw, ok := props[PostPropsMmBlocks]; ok {
|
|
if blocks, ok := interactivePropJSONArray(raw); ok {
|
|
mergeActionIDs(ids, CollectMmBlockActionIDs(blocks))
|
|
}
|
|
}
|
|
if raw, ok := props[PostPropsBlockKitBlocks]; ok {
|
|
if blocks, ok := interactivePropJSONArray(raw); ok {
|
|
mergeActionIDs(ids, CollectBlockKitActionIDs(blocks))
|
|
}
|
|
}
|
|
if raw, ok := props[PostPropsAdaptiveCards]; ok {
|
|
if cards, ok := interactivePropJSONArray(raw); ok {
|
|
mergeActionIDs(ids, CollectAdaptiveCardActionIDs(cards))
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectInteractiveActionIDsFromPost includes mmaction:// links in the post message.
|
|
func CollectInteractiveActionIDsFromPost(o *Post) map[string]struct{} {
|
|
ids := CollectInteractiveActionIDs(o.GetProps())
|
|
if o.Message != "" {
|
|
ids = appendMmactionIDsFromText(ids, o.Message)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CollectMmactionIDsFromText returns action ids from mmaction:// links in a string.
|
|
func CollectMmactionIDsFromText(text string) map[string]struct{} {
|
|
return appendMmactionIDsFromText(nil, text)
|
|
}
|
|
|
|
// SubsetMmBlocksActions returns registry entries referenced by actionIDs.
|
|
func SubsetMmBlocksActions(allActions any, actionIDs map[string]struct{}) map[string]any {
|
|
if allActions == nil || len(actionIDs) == 0 {
|
|
return nil
|
|
}
|
|
top, ok := allActions.(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make(map[string]any, len(actionIDs))
|
|
for id := range actionIDs {
|
|
if entry, ok := top[id]; ok {
|
|
out[id] = entry
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// RefreshInteractiveActionsOnPost sets mm_blocks_actions to the subset needed by this post's interactive content.
|
|
func RefreshInteractiveActionsOnPost(o *Post, allActions any) {
|
|
ids := CollectInteractiveActionIDsFromPost(o)
|
|
props := o.GetProps()
|
|
if props == nil {
|
|
props = make(map[string]any)
|
|
}
|
|
if len(ids) == 0 {
|
|
delete(props, PostPropsMmBlocksActions)
|
|
} else if subset := SubsetMmBlocksActions(allActions, ids); len(subset) > 0 {
|
|
props[PostPropsMmBlocksActions] = subset
|
|
} else {
|
|
delete(props, PostPropsMmBlocksActions)
|
|
}
|
|
o.SetProps(props)
|
|
}
|
|
|
|
// ApplyMmBlocksWithActionsToProps sets mm_blocks and refreshes mm_blocks_actions for the props payload.
|
|
// When props is nil, a new map is allocated and returned.
|
|
func ApplyMmBlocksWithActionsToProps(props map[string]any, blocks []any, allActions any) StringInterface {
|
|
if props == nil {
|
|
props = make(map[string]any)
|
|
}
|
|
props[PostPropsMmBlocks] = blocks
|
|
RefreshInteractiveActionsOnPost(&Post{Props: props}, allActions)
|
|
return props
|
|
}
|
|
|
|
// validateMmBlocksActionsPairing requires mm_blocks_actions to define exactly the actions
|
|
// referenced by mm_blocks, blocks, cards, and mmaction:// links in the post message.
|
|
func validateMmBlocksActionsPairing(o *Post, actions map[string]any) error {
|
|
referenced := CollectInteractiveActionIDsFromPost(o)
|
|
if len(referenced) == 0 {
|
|
if len(actions) > 0 {
|
|
return fmt.Errorf("mm_blocks_actions must only define actions referenced by interactive content")
|
|
}
|
|
return nil
|
|
}
|
|
for id := range referenced {
|
|
if _, ok := actions[id]; !ok {
|
|
return fmt.Errorf("mm_blocks_actions missing entry for action_id %q", id)
|
|
}
|
|
}
|
|
for key := range actions {
|
|
if _, ok := referenced[key]; !ok {
|
|
return fmt.Errorf("mm_blocks_actions entry %q is not referenced by interactive content", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateInteractiveActionsForWebhook checks interactive payloads and mm_blocks_actions are paired.
|
|
func ValidateInteractiveActionsForWebhook(o *Post) error {
|
|
return ValidateMmBlocksActions(o)
|
|
}
|
|
|
|
// ValidateMmBlocksActionsForWebhook validates mm_blocks-only webhook payloads (legacy helper).
|
|
func ValidateMmBlocksActionsForWebhook(blocks []any, actions any) error {
|
|
return ValidateInteractiveActionsForWebhook(&Post{
|
|
Props: map[string]any{
|
|
PostPropsMmBlocks: blocks,
|
|
PostPropsMmBlocksActions: actions,
|
|
},
|
|
})
|
|
}
|