package slack import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "strings" "time" "git.nakama.town/fmartingr/butterrobot/internal/config" "git.nakama.town/fmartingr/butterrobot/internal/model" ) // SlackPlatform implements the Platform interface for Slack type SlackPlatform struct { config *config.SlackConfig } // New creates a new SlackPlatform instance func New(cfg *config.SlackConfig) *SlackPlatform { return &SlackPlatform{ config: cfg, } } // Init initializes the Slack platform func (s *SlackPlatform) Init(_ *config.Config) error { // Validate config if s.config.Token == "" || s.config.BotOAuthAccessToken == "" { return model.ErrPlatformInit } return nil } // ParseIncomingMessage parses an incoming Slack message func (s *SlackPlatform) ParseIncomingMessage(r *http.Request) (*model.Message, error) { // Read request body body, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } defer r.Body.Close() // Parse JSON var requestData map[string]interface{} if err := json.Unmarshal(body, &requestData); err != nil { return nil, err } // Verify Slack request // This is a simplified version, production should include signature verification urlVerify, ok := requestData["type"] if ok && urlVerify == "url_verification" { return nil, errors.New("url verification") // Handle separately } // Process event event, ok := requestData["event"].(map[string]interface{}) if !ok { return nil, errors.New("invalid event") } // Create message msg := &model.Message{ Raw: requestData, } // Get text if text, ok := event["text"].(string); ok { msg.Text = text } // Get channel if channel, ok := event["channel"].(string); ok { msg.Chat = channel // Create Channel object channelRaw, err := s.ParseChannelFromMessage(body) if err != nil { return nil, err } msg.Channel = &model.Channel{ Platform: "slack", PlatformChannelID: channel, ChannelRaw: channelRaw, } } // Check if from bot if botID, ok := event["bot_id"].(string); ok && botID != "" { msg.FromBot = true } // Get user if user, ok := event["user"].(string); ok { msg.Author = user } // Get timestamp if ts, ok := event["ts"].(string); ok { // Convert Unix timestamp parts := strings.Split(ts, ".") if len(parts) > 0 { if sec, err := parseInt64(parts[0]); err == nil { msg.Date = time.Unix(sec, 0) msg.ID = ts } } } return msg, nil } // ParseChannelNameFromRaw extracts a human-readable channel name from raw data func (s *SlackPlatform) ParseChannelNameFromRaw(channelRaw map[string]interface{}) string { // Extract name from channel raw data if name, ok := channelRaw["name"].(string); ok { return name } // Fallback to ID if available if id, ok := channelRaw["id"].(string); ok { return id } return "unknown" } // ParseChannelFromMessage extracts channel data from a message func (s *SlackPlatform) ParseChannelFromMessage(body []byte) (map[string]any, error) { // Parse JSON var requestData map[string]interface{} if err := json.Unmarshal(body, &requestData); err != nil { return nil, err } // Extract channel info from event event, ok := requestData["event"].(map[string]interface{}) if !ok { return nil, errors.New("invalid event data") } channelID, ok := event["channel"].(string) if !ok { return nil, errors.New("channel ID not found") } // In a real implementation, you might want to fetch more details about the channel // using the Slack API, but for simplicity we'll just return the ID channelRaw := map[string]interface{}{ "id": channelID, } return channelRaw, nil } // SendMessage sends a message to Slack func (s *SlackPlatform) SendMessage(msg *model.Message) error { if s.config.BotOAuthAccessToken == "" { return errors.New("bot token not configured") } // Prepare payload payload := map[string]interface{}{ "channel": msg.Chat, "text": msg.Text, } // Add thread_ts if it's a reply if msg.ReplyTo != "" { payload["thread_ts"] = msg.ReplyTo } // Convert payload to JSON data, err := json.Marshal(payload) if err != nil { return err } // Send HTTP request req, err := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", strings.NewReader(string(data))) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.config.BotOAuthAccessToken)) client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() // Check response if resp.StatusCode != http.StatusOK { return fmt.Errorf("slack API error: %d", resp.StatusCode) } return nil } // Helper function to parse int64 func parseInt64(s string) (int64, error) { var n int64 _, err := fmt.Sscanf(s, "%d", &n) return n, err }