butterrobot/internal/model/message.go
Felipe M. 8801c43f92
All checks were successful
CI / goreleaser-lint (push) Successful in 10s
CI / format (push) Successful in 1m52s
Release / release (push) Successful in 5m40s
CI / lint (push) Successful in 2m31s
CI / test (push) Successful in 1m0s
CI / build (push) Successful in 4m6s
feat: add !gallerydl command to download media from links
Reply to a message containing a link with !gallerydl (or send it with a
link inline) to download the media via the gallery-dl CLI and have the
files posted back as a reply. Restricted to an allowed_users allowlist
(comma-separated, per-channel/global config); no one is allowed by
default.

- Add media support to the platform layer: model.Message.Files,
  ActionSendMedia, and Platform.SendMedia. Telegram uploads via
  multipart (sendPhoto/sendVideo/sendDocument, 50MB cap); Slack returns
  unsupported for now.
- Add internal/shell (os/exec wrapper) and internal/urlutil (URL
  extraction) helpers with tests.
- Dispatch ActionSendMedia in app and clean up the temp download dir.
- Admin config UI for allowed_users / gallery_dl_path; docs entry.
- Containerfile: base on alpine and install gallery-dl + ffmpeg (was
  scratch); release workflow sets up QEMU + Buildx for cross-arch image
  builds.
2026-07-05 20:05:57 +02:00

159 lines
4.2 KiB
Go

package model
import (
"time"
)
// ActionType defines the type of action to perform
type ActionType string
const (
// ActionSendMessage is for sending a message to the chat
ActionSendMessage ActionType = "send_message"
// ActionDeleteMessage is for deleting a message from the chat
ActionDeleteMessage ActionType = "delete_message"
// ActionSendMedia is for uploading local media files to the chat
ActionSendMedia ActionType = "send_media"
)
// MessageAction represents an action to be performed on the platform
type MessageAction struct {
Type ActionType
Message *Message // For send_message
MessageID string // For delete_message
Chat string // Chat where the action happens
Channel *Channel // Channel reference
Raw map[string]interface{} // Additional data for the action
}
// Message represents a chat message
type Message struct {
Text string
Chat string
Channel *Channel
Author string
FromBot bool
Date time.Time
ID string
ReplyTo string
// Files holds absolute local filesystem paths to upload alongside the
// message. Used by ActionSendMedia; empty for plain text messages.
Files []string
Raw map[string]interface{}
}
// Channel represents a chat channel
type Channel struct {
ID int64
Platform string
PlatformChannelID string
ChannelRaw map[string]interface{}
Enabled bool
EnableAllPlugins bool
Plugins map[string]*ChannelPlugin
}
// HasEnabledPlugin checks if a plugin is enabled for this channel
func (c *Channel) HasEnabledPlugin(pluginID string) bool {
// If EnableAllPlugins is true, all plugins are considered enabled
if c.EnableAllPlugins {
return true
}
plugin, exists := c.Plugins[pluginID]
if !exists {
return false
}
return plugin.Enabled
}
// ChannelName returns the channel name
func (c *Channel) ChannelName() string {
// In a real implementation, this would use the platform-specific
// ParseChannelNameFromRaw function
// For simplicity, we'll just use the PlatformChannelID if we can't extract a name
// Check if ChannelRaw has a name field
if c.ChannelRaw == nil {
return c.PlatformChannelID
}
// Check common name fields in ChannelRaw
if name, ok := c.ChannelRaw["name"].(string); ok && name != "" {
return name
}
// Check for nested objects like "chat" (used by Telegram)
if chat, ok := c.ChannelRaw["chat"].(map[string]interface{}); ok {
// Try different fields in order of preference
if title, ok := chat["title"].(string); ok && title != "" {
return title
}
if username, ok := chat["username"].(string); ok && username != "" {
return username
}
if firstName, ok := chat["first_name"].(string); ok && firstName != "" {
return firstName
}
}
return c.PlatformChannelID
}
// ChannelPlugin represents a plugin enabled for a channel
type ChannelPlugin struct {
ID int64
ChannelID int64
PluginID string
Enabled bool
Config map[string]any
}
// GlobalPluginConfig represents an app-wide default configuration for a plugin.
// Channels can override individual values; see MergeConfig.
type GlobalPluginConfig struct {
PluginID string
Config map[string]any
}
// MergeConfig returns the effective plugin configuration by layering channel
// overrides on top of the global configuration. A channel value overrides the
// global value only when it is non-empty; a blank (empty string) or unset
// channel value inherits the global value. This lets a channel form leave a
// field blank to follow the global default.
func MergeConfig(global, override map[string]any) map[string]any {
merged := make(map[string]any, len(global)+len(override))
for k, v := range global {
merged[k] = v
}
for k, v := range override {
if s, ok := v.(string); ok && s == "" {
// Blank channel value inherits the global value.
continue
}
merged[k] = v
}
return merged
}
// User represents an admin user
type User struct {
ID int64
Username string
Password string
}
// Reminder represents a scheduled reminder
type Reminder struct {
ID int64
Platform string
ChannelID string
MessageID string
ReplyToID string
UserID string
Username string
CreatedAt time.Time
TriggerAt time.Time
Content string
Processed bool
}