package channels import ( "clawgo/pkg/bus" "clawgo/pkg/config" "clawgo/pkg/logger" "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" "github.com/bwmarrin/discordgo" ) type DiscordChannel struct { *BaseChannel session *discordgo.Session config config.DiscordConfig } func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) { session, err := discordgo.New("Bot " + cfg.Token) if err != nil { return nil, fmt.Errorf("failed to create discord session: %w", err) } base := NewBaseChannel("discord", cfg, bus, cfg.AllowFrom) return &DiscordChannel{ BaseChannel: base, session: session, config: cfg, }, nil } func (c *DiscordChannel) Start(ctx context.Context) error { logger.InfoC("discord", logger.C0069) c.session.AddHandler(c.handleMessage) if err := c.session.Open(); err != nil { return fmt.Errorf("failed to open discord session: %w", err) } c.setRunning(true) botUser, err := c.session.User("@me") if err != nil { return fmt.Errorf("failed to get bot user: %w", err) } logger.InfoCF("discord", logger.C0070, map[string]interface{}{ "username": botUser.Username, "user_id": botUser.ID, }) return nil } func (c *DiscordChannel) Stop(ctx context.Context) error { logger.InfoC("discord", logger.C0071) c.setRunning(false) if err := c.session.Close(); err != nil { return fmt.Errorf("failed to close discord session: %w", err) } return nil } func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { if !c.IsRunning() { return fmt.Errorf("discord bot not running") } channelID := msg.ChatID if channelID == "" { return fmt.Errorf("channel ID is empty") } message := msg.Content if _, err := c.session.ChannelMessageSend(channelID, message); err != nil { return fmt.Errorf("failed to send discord message: %w", err) } return nil } func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if m == nil || m.Author == nil { return } if m.Author.ID == s.State.User.ID { return } senderID := m.Author.ID senderName := m.Author.Username if m.Author.Discriminator != "" && m.Author.Discriminator != "0" { senderName += "#" + m.Author.Discriminator } content := m.Content mediaPaths := []string{} for _, attachment := range m.Attachments { isAudio := isAudioFile(attachment.Filename, attachment.ContentType) if isAudio { localPath := c.downloadAttachment(attachment.URL, attachment.Filename) if localPath != "" { mediaPaths = append(mediaPaths, localPath) transcribedText := fmt.Sprintf("[audio: %s]", localPath) if content != "" { content += "\n" } content += transcribedText } else { mediaPaths = append(mediaPaths, attachment.URL) if content != "" { content += "\n" } content += fmt.Sprintf("[attachment: %s]", attachment.URL) } } else { mediaPaths = append(mediaPaths, attachment.URL) if content != "" { content += "\n" } content += fmt.Sprintf("[attachment: %s]", attachment.URL) } } if content == "" && len(mediaPaths) == 0 { return } if content == "" { content = "[media only]" } logger.DebugCF("discord", logger.C0072, map[string]interface{}{ "sender_name": senderName, logger.FieldSenderID: senderID, logger.FieldPreview: truncateString(content, 50), }) metadata := map[string]string{ "message_id": m.ID, "user_id": senderID, "username": m.Author.Username, "display_name": senderName, "guild_id": m.GuildID, "channel_id": m.ChannelID, "is_dm": fmt.Sprintf("%t", m.GuildID == ""), } c.HandleMessage(senderID, m.ChannelID, content, mediaPaths, metadata) } func isAudioFile(filename, contentType string) bool { audioExtensions := []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma"} audioTypes := []string{"audio/", "application/ogg", "application/x-ogg"} for _, ext := range audioExtensions { if strings.HasSuffix(strings.ToLower(filename), ext) { return true } } for _, audioType := range audioTypes { if strings.HasPrefix(strings.ToLower(contentType), audioType) { return true } } return false } func (c *DiscordChannel) downloadAttachment(url, filename string) string { mediaDir := filepath.Join(os.TempDir(), "clawgo_media") if err := os.MkdirAll(mediaDir, 0755); err != nil { logger.WarnCF("discord", logger.C0073, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" } localPath := filepath.Join(mediaDir, filename) resp, err := http.Get(url) if err != nil { logger.WarnCF("discord", logger.C0074, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { logger.WarnCF("discord", logger.C0075, map[string]interface{}{ "status_code": resp.StatusCode, }) return "" } out, err := os.Create(localPath) if err != nil { logger.WarnCF("discord", logger.C0076, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" } defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { logger.WarnCF("discord", logger.C0077, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" } logger.DebugCF("discord", logger.C0078, map[string]interface{}{ "path": localPath, }) return localPath }