Files
goclaw/internal/channels/slack/send.go
T
viettranx e85624ce96 refactor: split large Go files into smaller modules for maintainability
Split 11 Go files exceeding 500 lines into smaller, focused files:
- channels/manager.go → manager.go + dispatch.go + runs.go + events.go
- channels/slack/channel.go → channel.go + send.go + utils.go
- channels/slack/handlers.go → handlers.go + handlers_mention.go + handlers_files.go
- channels/telegram/handlers.go → handlers.go + handlers_utils.go
- channels/zalo/personal/channel.go → channel.go + send.go + listen.go + handlers.go
- gateway/methods/agents.go → agents.go + agents_create.go + agents_update.go + agents_delete.go
- http/summoner.go → summoner.go + summoner_regenerate.go + summoner_prompts.go + summoner_utils.go
- http/skills.go → skills.go + skills_upload.go + skills_versions.go + skills_grants.go
- http/mcp.go → mcp.go + mcp_tools.go + mcp_grants.go + mcp_requests.go
- store/pg/cron.go → cron.go + cron_crud.go + cron_update.go + cron_exec.go

No logic changes — pure file reorganization to keep files under 200 lines.
2026-03-09 10:41:54 +07:00

121 lines
3.2 KiB
Go

package slack
import (
"context"
"fmt"
"log/slog"
"strings"
slackapi "github.com/slack-go/slack"
"github.com/nextlevelbuilder/goclaw/internal/bus"
)
// Send delivers an outbound message to Slack.
func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
if !c.IsRunning() {
return fmt.Errorf("slack bot not running")
}
channelID := msg.ChatID
if channelID == "" {
return fmt.Errorf("empty chat ID for slack send")
}
placeholderKey := channelID
if pk := msg.Metadata["placeholder_key"]; pk != "" {
placeholderKey = pk
}
threadTS := msg.Metadata["message_thread_id"]
// Placeholder update (LLM retry notification)
if msg.Metadata["placeholder_update"] == "true" {
if pTS, ok := c.placeholders.Load(placeholderKey); ok {
ts := pTS.(string)
_, _, _, _ = c.api.UpdateMessage(channelID, ts,
slackapi.MsgOptionText(msg.Content, false))
}
return nil
}
content := msg.Content
// NO_REPLY: delete placeholder, return
if content == "" {
if pTS, ok := c.placeholders.Load(placeholderKey); ok {
c.placeholders.Delete(placeholderKey)
ts := pTS.(string)
_, _, _ = c.api.DeleteMessage(channelID, ts)
}
return nil
}
content = markdownToSlackMrkdwn(content)
// Edit placeholder with first chunk, send rest as follow-ups
if pTS, ok := c.placeholders.Load(placeholderKey); ok {
c.placeholders.Delete(placeholderKey)
ts := pTS.(string)
editContent, remaining := splitAtLimit(content, maxMessageLen)
opts := []slackapi.MsgOption{slackapi.MsgOptionText(editContent, false)}
if threadTS != "" {
opts = append(opts, slackapi.MsgOptionTS(threadTS))
}
if _, _, _, editErr := c.api.UpdateMessage(channelID, ts, opts...); editErr == nil {
if remaining != "" {
return c.sendChunked(channelID, remaining, threadTS)
}
return nil
} else {
slog.Warn("slack placeholder edit failed, sending new message",
"channel_id", channelID, "error", editErr)
}
}
// Handle media attachments
for _, media := range msg.Media {
if err := c.uploadFile(channelID, threadTS, media); err != nil {
slog.Warn("slack: file upload failed",
"file", media.URL, "error", err)
c.sendChunked(channelID, fmt.Sprintf("[File upload failed: %s]", media.URL), threadTS)
}
}
return c.sendChunked(channelID, content, threadTS)
}
func (c *Channel) sendChunked(channelID, content, threadTS string) error {
for len(content) > 0 {
chunk, rest := splitAtLimit(content, maxMessageLen)
content = rest
opts := []slackapi.MsgOption{slackapi.MsgOptionText(chunk, false)}
if threadTS != "" {
opts = append(opts, slackapi.MsgOptionTS(threadTS))
}
if _, _, err := c.api.PostMessage(channelID, opts...); err != nil {
return fmt.Errorf("send slack message: %w", err)
}
}
return nil
}
// splitAtLimit splits content at maxLen runes, preferring newline boundaries.
func splitAtLimit(content string, maxLen int) (chunk, remaining string) {
runes := []rune(content)
if len(runes) <= maxLen {
return content, ""
}
cutAt := maxLen
// Try to break at a newline in the second half
candidate := string(runes[:maxLen])
if idx := strings.LastIndex(candidate, "\n"); idx > len(candidate)/2 {
return content[:idx+1], content[idx+1:]
}
return string(runes[:cutAt]), string(runes[cutAt:])
}