diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 7f6abc4cb..e1af01b52 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -48,7 +48,7 @@ func (m *Manager) initChannels() error { if m.config.Channels.Telegram.Enabled && m.config.Channels.Telegram.Token != "" { logger.DebugC("channels", "Attempting to initialize Telegram channel") - telegram, err := NewTelegramChannel(m.config, m.bus) + telegram, err := NewTelegramChannel(m.config.Channels.Telegram, m.bus) if err != nil { logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]interface{}{ "error": err.Error(), @@ -176,6 +176,19 @@ func (m *Manager) initChannels() error { } } + if m.config.Channels.Pushover.Enabled && m.config.Channels.Pushover.AppToken != "" && m.config.Channels.Pushover.UserKey != "" { + logger.DebugC("channels", "Attempting to initialize Pushover channel") + pushover, err := NewPushoverChannel(m.config.Channels.Pushover, m.bus) + if err != nil { + logger.ErrorCF("channels", "Failed to initialize Pushover channel", map[string]interface{}{ + "error": err.Error(), + }) + } else { + m.channels["pushover"] = pushover + logger.InfoC("channels", "Pushover channel enabled successfully") + } + } + logger.InfoCF("channels", "Channel initialization completed", map[string]interface{}{ "enabled_channels": len(m.channels), }) diff --git a/pkg/channels/pushover.go b/pkg/channels/pushover.go new file mode 100644 index 000000000..839daf2df --- /dev/null +++ b/pkg/channels/pushover.go @@ -0,0 +1,88 @@ +package channels + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/logger" +) + +type PushoverChannel struct { + *BaseChannel + config config.PushoverConfig + client *http.Client +} + +func NewPushoverChannel(cfg config.PushoverConfig, bus *bus.MessageBus) (*PushoverChannel, error) { + base := NewBaseChannel("pushover", cfg, bus, nil) + + return &PushoverChannel{ + BaseChannel: base, + config: cfg, + client: &http.Client{}, + }, nil +} + +func (c *PushoverChannel) Name() string { + return "pushover" +} + +func (c *PushoverChannel) Start(ctx context.Context) error { + logger.InfoC("pushover", "Starting Pushover channel") + c.setRunning(true) + return nil +} + +func (c *PushoverChannel) Stop(ctx context.Context) error { + logger.InfoC("pushover", "Stopping Pushover channel") + c.setRunning(false) + return nil +} + +func (c *PushoverChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { + if !c.IsRunning() { + return fmt.Errorf("pushover channel not running") + } + + if c.config.AppToken == "" || c.config.UserKey == "" { + return fmt.Errorf("pushover app_token and user_key are required") + } + + data := url.Values{} + data.Set("token", c.config.AppToken) + data.Set("user", c.config.UserKey) + data.Set("message", msg.Content) + + // Truncate message if too long (Pushover limit is 1024 chars) + if len(msg.Content) > 1024 { + data.Set("message", msg.Content[:1021]+"...") + } + + req, err := http.NewRequestWithContext(ctx, "POST", "https://api.pushover.net/1/messages.json", strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send notification: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("pushover API returned status %d", resp.StatusCode) + } + + logger.DebugCF("pushover", "Notification sent", map[string]any{ + "content_length": len(msg.Content), + }) + + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index fbc7e77be..11abda621 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -79,6 +79,7 @@ type ChannelsConfig struct { Slack SlackConfig `json:"slack"` LINE LINEConfig `json:"line"` OneBot OneBotConfig `json:"onebot"` + Pushover PushoverConfig `json:"pushover"` } type WhatsAppConfig struct { @@ -104,10 +105,9 @@ type FeishuConfig struct { } type DiscordConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` - MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` } type MaixCamConfig struct { @@ -157,6 +157,12 @@ type OneBotConfig struct { AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` } +type PushoverConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PUSHOVER_ENABLED"` + AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_PUSHOVER_APP_TOKEN"` + UserKey string `json:"user_key" env:"PICOCLAW_CHANNELS_PUSHOVER_USER_KEY"` +} + type HeartbeatConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5