All checks were successful
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.
159 lines
4.2 KiB
Go
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
|
|
}
|