Files
Dmitry Verkhoturov 07c7926453 Update backend dependencies to latest
Update all Go modules in backend/ and backend/_example/memory_store/ to
their latest versions (chroma 2.27, go-redis 9.21, bbolt 1.5, slack 0.27,
golang.org/x/* and others); re-tidy and re-vendor, keep the example module
in sync.

Hold github.com/go-chi/chi/v5 at v5.2.5: v5.3.0 deprecates
middleware.RealIP (IP-spoofing advisories). Switching off RealIP changes
how the client IP is derived for rate limiting and votes, which is a
security decision better made on its own rather than inside a dependency
bump.

go test -race, go vet, golangci-lint and govulncheck all clean on both
modules.
2026-06-30 18:35:31 +01:00

214 lines
6.7 KiB
Go

package slack
import (
"encoding/json"
"fmt"
)
// TableCellType identifies the variant of a cell inside a TableBlock row.
type TableCellType string
const (
TableCellRawText TableCellType = "raw_text"
TableCellRawNumber TableCellType = "raw_number"
TableCellRichText TableCellType = "rich_text"
)
// TableCell is implemented by every cell type valid inside a TableBlock row:
// TableRichTextCell, TableRawTextCell, and TableRawNumberCell. A nil TableCell
// represents an empty cell, which Slack sends as null (common in user-pasted tables).
type TableCell interface {
TableCellType() TableCellType
}
// TableRichTextCell is a cell holding rich text formatting. Slack uses rich_text cells
// for styled content such as the bold header row of a pasted table.
type TableRichTextCell struct {
Type TableCellType `json:"type"`
Elements []RichTextElement `json:"elements"`
}
// TableCellType returns the cell variant.
func (c TableRichTextCell) TableCellType() TableCellType {
return c.Type
}
// NewTableRichTextCell returns a rich_text cell with the given rich text elements.
func NewTableRichTextCell(elements ...RichTextElement) *TableRichTextCell {
return &TableRichTextCell{Type: TableCellRichText, Elements: elements}
}
// UnmarshalJSON delegates rich text element parsing to RichTextBlock so the cell handles
// the same set of inner elements (sections, lists, quotes, preformatted, unknown).
func (c *TableRichTextCell) UnmarshalJSON(data []byte) error {
var rt RichTextBlock
if err := json.Unmarshal(data, &rt); err != nil {
return err
}
c.Type = TableCellRichText
c.Elements = rt.Elements
return nil
}
// TableRawTextCell is a plain-text cell in a TableBlock. Slack sends raw_text cells for
// non-numeric data, including text pasted from a spreadsheet.
type TableRawTextCell struct {
Type TableCellType `json:"type"`
Text string `json:"text"`
}
// TableCellType returns the cell variant.
func (c TableRawTextCell) TableCellType() TableCellType {
return c.Type
}
// NewTableRawTextCell returns a raw_text cell with the given text.
func NewTableRawTextCell(text string) *TableRawTextCell {
return &TableRawTextCell{Type: TableCellRawText, Text: text}
}
// TableRawNumberCell is a numeric cell in a TableBlock. Text, when set, overrides the
// displayed value.
//
// raw_number cells are receive-only: Slack emits them for numeric columns in user-pasted
// tables, but chat.postMessage rejects them. To post a number, use a TableRawTextCell. See
// TableBlock for the full list of postable vs receive-only cell types.
type TableRawNumberCell struct {
Type TableCellType `json:"type"`
Value float64 `json:"value"`
Text string `json:"text,omitempty"`
}
// TableCellType returns the cell variant.
func (c TableRawNumberCell) TableCellType() TableCellType {
return c.Type
}
// NewTableRawNumberCell returns a raw_number cell with the given value.
func NewTableRawNumberCell(value float64) *TableRawNumberCell {
return &TableRawNumberCell{Type: TableCellRawNumber, Value: value}
}
// WithText sets the display text shown in place of the numeric value.
func (c *TableRawNumberCell) WithText(text string) *TableRawNumberCell {
c.Text = text
return c
}
// TableBlock defines a block that lets you use a table to display your data.
//
// Rows is an array of cell arrays. Each cell is a TableRichTextCell, TableRawTextCell,
// or TableRawNumberCell; a nil cell represents an empty cell (Slack sends null).
//
// Not every cell type can be posted. chat.postMessage validates a table cell against two
// schemas only: raw_text (TableRawTextCell, requires "text") and rich_text
// (TableRichTextCell, requires "elements"). TableRawNumberCell and nil cells are produced
// by Slack on output — numeric columns and empty cells in user-pasted tables — but the API
// rejects them on input with invalid_blocks. So you may receive all cell types, but post
// only raw_text and rich_text; render a number you want to send as a raw_text cell.
//
// More Information: https://docs.slack.dev/reference/block-kit/blocks/table-block/
type TableBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
Rows [][]TableCell `json:"rows"`
ColumnSettings []ColumnSetting `json:"column_settings,omitempty"`
}
type ColumnAlignment string
const (
ColumnAlignmentLeft ColumnAlignment = "left"
ColumnAlignmentCenter ColumnAlignment = "center"
ColumnAlignmentRight ColumnAlignment = "right"
)
type ColumnSetting struct {
Align ColumnAlignment `json:"align"`
IsWrapped bool `json:"is_wrapped"`
}
// BlockType returns the type of the block
func (s TableBlock) BlockType() MessageBlockType {
return s.Type
}
// ID returns the ID of the block
func (s TableBlock) ID() string {
return s.BlockID
}
// UnmarshalJSON parses the heterogeneous cell types in each row. A null cell is decoded
// as a nil TableCell.
func (s *TableBlock) UnmarshalJSON(data []byte) error {
var raw struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id"`
ColumnSettings []ColumnSetting `json:"column_settings"`
Rows [][]json.RawMessage `json:"rows"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
rows := make([][]TableCell, 0, len(raw.Rows))
for _, rawRow := range raw.Rows {
row := make([]TableCell, 0, len(rawRow))
for _, rawCell := range rawRow {
if len(rawCell) == 0 || string(rawCell) == "null" {
row = append(row, nil)
continue
}
var probe struct {
Type TableCellType `json:"type"`
}
if err := json.Unmarshal(rawCell, &probe); err != nil {
return err
}
var cell TableCell
switch probe.Type {
case TableCellRawText:
cell = &TableRawTextCell{}
case TableCellRawNumber:
cell = &TableRawNumberCell{}
case TableCellRichText:
cell = &TableRichTextCell{}
default:
return fmt.Errorf("unsupported table cell type %q", probe.Type)
}
if err := json.Unmarshal(rawCell, cell); err != nil {
return err
}
row = append(row, cell)
}
rows = append(rows, row)
}
s.Type = raw.Type
s.BlockID = raw.BlockID
s.ColumnSettings = raw.ColumnSettings
s.Rows = rows
return nil
}
// WithColumnSettings sets the column settings for the Table Block
func (s *TableBlock) WithColumnSettings(columnSettings ...ColumnSetting) *TableBlock {
s.ColumnSettings = columnSettings
return s
}
// AddRow adds a new row of cells to the Table Block
func (s *TableBlock) AddRow(cells ...TableCell) *TableBlock {
s.Rows = append(s.Rows, append([]TableCell{}, cells...))
return s
}
// NewTableBlock returns an instance of a Table Block type
func NewTableBlock(blockID string) *TableBlock {
return &TableBlock{
Type: MBTTable,
BlockID: blockID,
Rows: make([][]TableCell, 0),
}
}