Files
goclaw/internal/channels/media_utils.go
T
Duc Nguyen 137a986d4f feat(channels): add Slack channel (#83)
* feat(channels): add Slack channel via Socket Mode (#37)

Implement Slack integration using Socket Mode (xapp-/xoxb- tokens):
- Event-driven messaging via app_mention + message events
- Policy checks: open, pairing, allowlist, disabled (DM + group)
- Thread participation with configurable TTL
- Markdown-to-mrkdwn formatting pipeline
- Streaming support (edit-in-place + native ChatStreamer)
- SSRF-protected file downloads
- Debounce, dedup, reactions, group history context
- 170 unit tests (format, helpers, stream, SSRF)

Fix BaseChannel.HandleMessage allowlist to also check chatID,
enabling group allowlist with channel IDs across all channels.

Closes #37

* feat(slack): add file/media support and edit-to-mention handling

- Wire inbound file download into handleMessage (images, audio, documents)
- Add media.go with resolveMedia, classifyMime, buildMediaTags
- Extract shared ExtractDocumentContent to channels/media_utils.go (DRY with Telegram)
- Support file_share and message_changed subtypes
- Handle edit-to-mention: respond when user edits old message to add @bot
- Add MediaMaxBytes config field (default 20MB)
- Fix debounce media accumulation (was silently dropping files)
- Add 60s HTTP client timeout on file downloads
- Refactor downloadFile signature for slack.File compatibility
2026-03-09 07:02:37 +07:00

77 lines
2.0 KiB
Go

package channels
import (
"fmt"
"html"
"os"
"path/filepath"
"strings"
)
const (
// DocMaxChars is the max characters to extract from text documents (matching TS: 200K).
DocMaxChars = 200_000
)
// TextExtensions maps file extensions to MIME types for text files we can extract.
var TextExtensions = map[string]string{
".txt": "text/plain",
".md": "text/markdown",
".csv": "text/csv",
".tsv": "text/tab-separated-values",
".json": "application/json",
".yaml": "text/yaml",
".yml": "text/yaml",
".xml": "text/xml",
".log": "text/plain",
".ini": "text/plain",
".cfg": "text/plain",
".env": "text/plain",
".sh": "text/x-shellscript",
".py": "text/x-python",
".go": "text/x-go",
".js": "text/javascript",
".ts": "text/typescript",
".html": "text/html",
".css": "text/css",
".sql": "text/x-sql",
".rs": "text/x-rust",
".java": "text/x-java",
".c": "text/x-c",
".cpp": "text/x-c++",
".h": "text/x-c",
".rb": "text/x-ruby",
".php": "text/x-php",
".toml": "text/x-toml",
}
// ExtractDocumentContent reads a document file and returns its content wrapped in XML tags.
// For text files: extracts content, truncates at DocMaxChars, wraps in <file> block.
// For binary files: returns a placeholder message.
func ExtractDocumentContent(filePath, fileName string) (string, error) {
if filePath == "" {
return fmt.Sprintf("[File: %s — download failed]", fileName), nil
}
ext := strings.ToLower(filepath.Ext(fileName))
mime, isText := TextExtensions[ext]
if !isText {
return fmt.Sprintf("[File: %s — binary format not supported, only text files can be processed]", fileName), nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("read file %s: %w", fileName, err)
}
content := string(data)
if len(content) > DocMaxChars {
content = content[:DocMaxChars] + "\n... [truncated]"
}
escaped := html.EscapeString(content)
return fmt.Sprintf("<file name=%q mime=%q>\n%s\n</file>", fileName, mime, escaped), nil
}