220 lines
4.9 KiB
Go
220 lines
4.9 KiB
Go
package slack
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"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 := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := r.Body.Close(); err != nil {
|
|
fmt.Printf("Error closing request body: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// 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 func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
fmt.Printf("Error closing response body: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// 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
|
|
}
|