diff --git a/.goreleaser.yml b/.goreleaser.yml index a3836e9..c89e189 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -93,7 +93,7 @@ docker_manifests: nfpms: - maintainer: Felipe Martin - description: A chatbot server with customizable commands and triggers + description: SMTP server to forward messages to shoutrrr endpoints homepage: https://git.nakama.town/fmartingr/butterrobot license: AGPL-3.0 formats: diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml index 5b32d48..4353088 100644 --- a/.woodpecker/ci.yml +++ b/.woodpecker/ci.yml @@ -3,7 +3,7 @@ when: - push - pull_request branch: - - master + - main steps: format: diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index 39dbf65..3630566 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -1,6 +1,6 @@ when: - event: tag - branch: master + branch: main steps: - name: Release diff --git a/README.md b/README.md index 920d087..214afa6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Butter Robot -![Status badge](https://woodpecker.local.fmartingr.dev/api/badges/5/status.svg) +| Stable | Master | +| --- | --- | +| ![Build stable tag docker image](https://git.nakama.town/fmartingr/butterrobot/workflows/Build%20stable%20tag%20docker%20image/badge.svg?branch=stable) | ![Build latest tag docker image](https://git.nakama.town/fmartingr/butterrobot/workflows/Build%20latest%20tag%20docker%20image/badge.svg?branch=master) | +| ![Test](https://git.nakama.town/fmartingr/butterrobot/workflows/Test/badge.svg?branch=stable) | ![Test](https://git.nakama.town/fmartingr/butterrobot/workflows/Test/badge.svg?branch=master) | Go framework to create bots for several platforms. @@ -10,7 +13,7 @@ Go framework to create bots for several platforms. ## Features -- Support for multiple chat platforms (Slack (untested!), Telegram) +- Support for multiple chat platforms (Slack, Telegram) - Plugin system for easy extension - Admin interface for managing channels and plugins - Message queue for asynchronous processing diff --git a/cmd/butterrobot/main.go b/cmd/butterrobot/main.go index 3bc56cb..2982a96 100644 --- a/cmd/butterrobot/main.go +++ b/cmd/butterrobot/main.go @@ -8,8 +8,6 @@ import ( "git.nakama.town/fmartingr/butterrobot/internal/app" "git.nakama.town/fmartingr/butterrobot/internal/config" - - _ "golang.org/x/crypto/x509roots/fallback" ) func main() { diff --git a/docs/creating-a-plugin.md b/docs/creating-a-plugin.md index 469491a..945d03c 100644 --- a/docs/creating-a-plugin.md +++ b/docs/creating-a-plugin.md @@ -1,18 +1,6 @@ # Creating a Plugin -## Plugin Categories - -ButterRobot organizes plugins into different categories: - -- **Development**: Utility plugins like `ping` -- **Fun**: Entertainment plugins like dice rolling, coin flipping -- **Social**: Social media related plugins like URL transformers/expanders - -When creating a new plugin, consider which category it fits into and place it in the appropriate directory. - -## Plugin Examples - -### Basic Example: Marco Polo +## Example This simple "Marco Polo" plugin will answer _Polo_ to the user that says _Marco_: @@ -59,92 +47,6 @@ func (p *MarcoPlugin) OnMessage(msg *model.Message, config map[string]interface{ } ``` -### Advanced Example: URL Transformer - -This more complex plugin transforms URLs, useful for improving media embedding in chat platforms: - -```go -package social - -import ( - "net/url" - "regexp" - "strings" - - "git.nakama.town/fmartingr/butterrobot/internal/model" - "git.nakama.town/fmartingr/butterrobot/internal/plugin" -) - -// TwitterExpander transforms twitter.com links to fxtwitter.com links -type TwitterExpander struct { - plugin.BasePlugin -} - -// New creates a new TwitterExpander instance -func NewTwitter() *TwitterExpander { - return &TwitterExpander{ - BasePlugin: plugin.BasePlugin{ - ID: "social.twitter", - Name: "Twitter Link Expander", - Help: "Automatically converts twitter.com links to fxtwitter.com links and removes tracking parameters", - }, - } -} - -// OnMessage handles incoming messages -func (p *TwitterExpander) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message { - // Skip empty messages - if strings.TrimSpace(msg.Text) == "" { - return nil - } - - // Regex to match twitter.com links - twitterRegex := regexp.MustCompile(`https?://(www\.)?(twitter\.com|x\.com)/[^\s]+`) - - // Check if the message contains a Twitter link - if !twitterRegex.MatchString(msg.Text) { - return nil - } - - // Transform the URL - transformed := twitterRegex.ReplaceAllStringFunc(msg.Text, func(link string) string { - // Parse the URL - parsedURL, err := url.Parse(link) - if err != nil { - // If parsing fails, just do the simple replacement - link = strings.Replace(link, "twitter.com", "fxtwitter.com", 1) - link = strings.Replace(link, "x.com", "fxtwitter.com", 1) - return link - } - - // Change the host - if strings.Contains(parsedURL.Host, "twitter.com") { - parsedURL.Host = strings.Replace(parsedURL.Host, "twitter.com", "fxtwitter.com", 1) - } else if strings.Contains(parsedURL.Host, "x.com") { - parsedURL.Host = strings.Replace(parsedURL.Host, "x.com", "fxtwitter.com", 1) - } - - // Remove query parameters - parsedURL.RawQuery = "" - - // Return the cleaned URL - return parsedURL.String() - }) - - // Create response message - response := &model.Message{ - Text: transformed, - Chat: msg.Chat, - ReplyTo: msg.ID, - Channel: msg.Channel, - } - - return []*model.Message{response} -} -``` - -## Registering Plugins - To use the plugin, register it in your application: ```go @@ -153,10 +55,7 @@ func (a *App) Run() error { // ... // Register plugins - plugin.Register(ping.New()) // Development plugin - plugin.Register(fun.NewCoin()) // Fun plugin - plugin.Register(social.NewTwitter()) // Social media plugin - plugin.Register(myplugin.New()) // Your custom plugin + plugin.Register(myplugin.New()) // ... } diff --git a/docs/plugins.md b/docs/plugins.md index 84578e5..11e3d16 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -9,12 +9,3 @@ - Lo quito: What happens when you say _"lo quito"_...? (Spanish pun) - Dice: Put `!dice` and wathever roll you want to perform. - Coin: Flip a coin and get heads or tails. - -### Utility - -- Remind Me: Reply to a message with `!remindme ` to set a reminder. Supported duration units: y (years), mo (months), d (days), h (hours), m (minutes), s (seconds). Examples: `!remindme 1y` for 1 year, `!remindme 3mo` for 3 months, `!remindme 2d` for 2 days, `!remindme 3h` for 3 hours. The bot will mention you with a reminder after the specified time. - -### Social Media - -- Twitter Link Expander: Automatically converts twitter.com and x.com links to fxtwitter.com links and removes tracking parameters. This allows for better media embedding in chat platforms. -- Instagram Link Expander: Automatically converts instagram.com links to ddinstagram.com links and removes tracking parameters. This allows for better media embedding in chat platforms. diff --git a/go.mod b/go.mod index cd1bee5..3f17f0d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24 require ( github.com/gorilla/sessions v1.4.0 golang.org/x/crypto v0.37.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20250418111936-9c1aa6af88df modernc.org/sqlite v1.37.0 ) diff --git a/go.sum b/go.sum index 00c4a3c..f331cb5 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250418111936-9c1aa6af88df h1:SwgTucX8ajPE0La2ELpYOIs8jVMoCMpAvYB6mDqP9vk= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250418111936-9c1aa6af88df/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= diff --git a/internal/admin/admin.go b/internal/admin/admin.go index 69c769b..045d980 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -46,7 +46,6 @@ type TemplateData struct { Channels []*model.Channel Channel *model.Channel ChannelPlugin *model.ChannelPlugin - Version string } // Admin represents the admin interface @@ -56,11 +55,10 @@ type Admin struct { store *sessions.CookieStore templates map[string]*template.Template baseTemplate *template.Template - version string } // New creates a new Admin instance -func New(cfg *config.Config, database *db.Database, version string) *Admin { +func New(cfg *config.Config, database *db.Database) *Admin { // Create session store with appropriate options store := sessions.NewCookieStore([]byte(cfg.SecretKey)) store.Options = &sessions.Options{ @@ -106,19 +104,19 @@ func New(cfg *config.Config, database *db.Database, version string) *Admin { if err != nil { panic(err) } - + // Create a clone of the base template t, err := baseTemplate.Clone() if err != nil { panic(err) } - + // Parse the template content t, err = t.Parse(string(content)) if err != nil { panic(err) } - + templates[tf] = t } @@ -128,7 +126,6 @@ func New(cfg *config.Config, database *db.Database, version string) *Admin { store: store, templates: templates, baseTemplate: baseTemplate, - version: version, } } @@ -194,7 +191,7 @@ func (a *Admin) addFlash(w http.ResponseWriter, r *http.Request, message string, } // Map internal categories to Bootstrap alert classes - var alertClass string + alertClass := category switch category { case "success": alertClass = "success" @@ -249,6 +246,17 @@ func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessag return messages } +// requireLogin middleware checks if the user is logged in +func (a *Admin) requireLogin(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !a.isLoggedIn(r) { + http.Redirect(w, r, "/admin/login", http.StatusSeeOther) + return + } + next(w, r) + } +} + // render renders a template with the given data func (a *Admin) render(w http.ResponseWriter, r *http.Request, templateName string, data TemplateData) { // Add current user data @@ -256,7 +264,6 @@ func (a *Admin) render(w http.ResponseWriter, r *http.Request, templateName stri data.LoggedIn = a.isLoggedIn(r) data.Path = r.URL.Path data.Flash = a.getFlashes(w, r) - data.Version = a.version // Get template tmpl, ok := a.templates[templateName] @@ -323,10 +330,7 @@ func (a *Admin) handleLogin(w http.ResponseWriter, r *http.Request) { // Set session expiration session.Options.MaxAge = 3600 * 24 * 7 // 1 week - err = session.Save(r, w) - if err != nil { - fmt.Printf("Error saving session: %v\n", err) - } + session.Save(r, w) a.addFlash(w, r, "You were logged in", "success") @@ -354,7 +358,7 @@ func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/admin/login", http.StatusSeeOther) return } - + session.Values = make(map[interface{}]interface{}) session.Options.MaxAge = -1 // Delete session err = session.Save(r, w) diff --git a/internal/admin/templates/_base.html b/internal/admin/templates/_base.html index 3ebdf85..4a414e3 100644 --- a/internal/admin/templates/_base.html +++ b/internal/admin/templates/_base.html @@ -117,19 +117,6 @@ -
-
-
-
-
    -
  • - ButterRobot {{if .Version}}v{{.Version}}{{else}}(development){{end}} -
  • -
-
-
-
-
diff --git a/internal/app/app.go b/internal/app/app.go index 7403396..8d4ffcd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "os/signal" - "runtime/debug" "strings" "syscall" "time" @@ -17,25 +16,21 @@ import ( "git.nakama.town/fmartingr/butterrobot/internal/admin" "git.nakama.town/fmartingr/butterrobot/internal/config" "git.nakama.town/fmartingr/butterrobot/internal/db" - "git.nakama.town/fmartingr/butterrobot/internal/model" "git.nakama.town/fmartingr/butterrobot/internal/platform" "git.nakama.town/fmartingr/butterrobot/internal/plugin" "git.nakama.town/fmartingr/butterrobot/internal/plugin/fun" "git.nakama.town/fmartingr/butterrobot/internal/plugin/ping" - "git.nakama.town/fmartingr/butterrobot/internal/plugin/reminder" - "git.nakama.town/fmartingr/butterrobot/internal/plugin/social" "git.nakama.town/fmartingr/butterrobot/internal/queue" ) // App represents the application type App struct { - config *config.Config - logger *slog.Logger - db *db.Database - router *http.ServeMux - queue *queue.Queue - admin *admin.Admin - version string + config *config.Config + logger *slog.Logger + db *db.Database + router *http.ServeMux + queue *queue.Queue + admin *admin.Admin } // New creates a new App instance @@ -52,24 +47,16 @@ func New(cfg *config.Config, logger *slog.Logger) (*App, error) { // Initialize message queue messageQueue := queue.New(logger) - // Get version information - version := "" - info, ok := debug.ReadBuildInfo() - if ok { - version = info.Main.Version - } - // Initialize admin interface - adminInterface := admin.New(cfg, database, version) + adminInterface := admin.New(cfg, database) return &App{ - config: cfg, - logger: logger, - db: database, - router: router, - queue: messageQueue, - admin: adminInterface, - version: version, + config: cfg, + logger: logger, + db: database, + router: router, + queue: messageQueue, + admin: adminInterface, }, nil } @@ -85,12 +72,6 @@ func (a *App) Run() error { plugin.Register(fun.NewCoin()) plugin.Register(fun.NewDice()) plugin.Register(fun.NewLoquito()) - plugin.Register(social.NewTwitterExpander()) - plugin.Register(social.NewInstagramExpander()) - - // Register reminder plugin - reminderPlugin := reminder.New(a.db) - plugin.Register(reminderPlugin) // Initialize routes a.initializeRoutes() @@ -98,9 +79,6 @@ func (a *App) Run() error { // Start message queue worker a.queue.Start(a.handleMessage) - // Start reminder scheduler - a.queue.StartReminderScheduler(a.handleReminder) - // Create server addr := fmt.Sprintf(":%s", a.config.Port) srv := &http.Server{ @@ -152,9 +130,7 @@ func (a *App) initializeRoutes() { a.router.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]interface{}{}); err != nil { - a.logger.Error("Error encoding response", "error", err) - } + json.NewEncoder(w).Encode(map[string]interface{}{}) }) // Platform webhook endpoints @@ -177,9 +153,7 @@ func (a *App) handleIncomingWebhook(w http.ResponseWriter, r *http.Request) { if _, err := platform.Get(platformName); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - if err := json.NewEncoder(w).Encode(map[string]string{"error": "Unknown platform"}); err != nil { - a.logger.Error("Error encoding response", "error", err) - } + json.NewEncoder(w).Encode(map[string]string{"error": "Unknown platform"}) return } @@ -188,9 +162,7 @@ func (a *App) handleIncomingWebhook(w http.ResponseWriter, r *http.Request) { if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - if err := json.NewEncoder(w).Encode(map[string]string{"error": "Failed to read request body"}); err != nil { - a.logger.Error("Error encoding response", "error", err) - } + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to read request body"}) return } @@ -206,9 +178,7 @@ func (a *App) handleIncomingWebhook(w http.ResponseWriter, r *http.Request) { // Respond with success w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{}); err != nil { - a.logger.Error("Error encoding response", "error", err) - } + json.NewEncoder(w).Encode(map[string]any{}) } // extractPlatformName extracts the platform name from the URL path @@ -321,73 +291,3 @@ func (a *App) handleMessage(item queue.Item) { } } } - -// handleReminder handles reminder processing -func (a *App) handleReminder(reminder *model.Reminder) { - // When called with nil, it means we should check for pending reminders - if reminder == nil { - // Get pending reminders - reminders, err := a.db.GetPendingReminders() - if err != nil { - a.logger.Error("Error getting pending reminders", "error", err) - return - } - - // Process each reminder - for _, r := range reminders { - a.processReminder(r) - } - return - } - - // Otherwise, process the specific reminder - a.processReminder(reminder) -} - -// processReminder processes an individual reminder -func (a *App) processReminder(reminder *model.Reminder) { - a.logger.Info("Processing reminder", - "id", reminder.ID, - "platform", reminder.Platform, - "channel", reminder.ChannelID, - "trigger_at", reminder.TriggerAt, - ) - - // Get the platform handler - p, err := platform.Get(reminder.Platform) - if err != nil { - a.logger.Error("Error getting platform for reminder", "error", err, "platform", reminder.Platform) - return - } - - // Get the channel - channel, err := a.db.GetChannelByPlatform(reminder.Platform, reminder.ChannelID) - if err != nil { - a.logger.Error("Error getting channel for reminder", "error", err) - return - } - - // Create the reminder message - reminderText := fmt.Sprintf("@%s reminding you of this", reminder.Username) - - message := &model.Message{ - Text: reminderText, - Chat: reminder.ChannelID, - Channel: channel, - Author: "bot", - FromBot: true, - Date: time.Now(), - ReplyTo: reminder.ReplyToID, // Reply to the original message - } - - // Send the reminder message - if err := p.SendMessage(message); err != nil { - a.logger.Error("Error sending reminder", "error", err) - return - } - - // Mark the reminder as processed - if err := a.db.MarkReminderAsProcessed(reminder.ID); err != nil { - a.logger.Error("Error marking reminder as processed", "error", err) - } -} diff --git a/internal/db/db.go b/internal/db/db.go index bdf9eaf..8cdce4a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "time" "golang.org/x/crypto/bcrypt" _ "modernc.org/sqlite" @@ -234,11 +233,7 @@ func (d *Database) GetChannelPlugins(channelID int64) ([]*model.ChannelPlugin, e if err != nil { return nil, err } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() + defer rows.Close() var plugins []*model.ChannelPlugin @@ -419,11 +414,7 @@ func (d *Database) GetAllChannels() ([]*model.Channel, error) { if err != nil { return nil, err } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() + defer rows.Close() var channels []*model.Channel @@ -462,9 +453,10 @@ func (d *Database) GetAllChannels() ([]*model.Channel, error) { continue // Skip this channel if plugins can't be retrieved } - // Add plugins to channel - for _, plugin := range plugins { - channel.Plugins[plugin.PluginID] = plugin + if plugins != nil { + for _, plugin := range plugins { + channel.Plugins[plugin.PluginID] = plugin + } } channels = append(channels, channel) @@ -599,124 +591,6 @@ func (d *Database) UpdateUserPassword(userID int64, newPassword string) error { return err } -// CreateReminder creates a new reminder -func (d *Database) CreateReminder(platform, channelID, messageID, replyToID, userID, username, content string, triggerAt time.Time) (*model.Reminder, error) { - query := ` - INSERT INTO reminders ( - platform, channel_id, message_id, reply_to_id, - user_id, username, created_at, trigger_at, - content, processed - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0) - ` - - createdAt := time.Now() - result, err := d.db.Exec( - query, - platform, channelID, messageID, replyToID, - userID, username, createdAt, triggerAt, - content, - ) - if err != nil { - return nil, err - } - - id, err := result.LastInsertId() - if err != nil { - return nil, err - } - - return &model.Reminder{ - ID: id, - Platform: platform, - ChannelID: channelID, - MessageID: messageID, - ReplyToID: replyToID, - UserID: userID, - Username: username, - CreatedAt: createdAt, - TriggerAt: triggerAt, - Content: content, - Processed: false, - }, nil -} - -// GetPendingReminders gets all pending reminders that need to be processed -func (d *Database) GetPendingReminders() ([]*model.Reminder, error) { - query := ` - SELECT id, platform, channel_id, message_id, reply_to_id, - user_id, username, created_at, trigger_at, content, processed - FROM reminders - WHERE processed = 0 AND trigger_at <= ? - ` - - rows, err := d.db.Query(query, time.Now()) - if err != nil { - return nil, err - } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() - - var reminders []*model.Reminder - - for rows.Next() { - var ( - id int64 - platform, channelID, messageID, replyToID string - userID, username, content string - createdAt, triggerAt time.Time - processed bool - ) - - if err := rows.Scan( - &id, &platform, &channelID, &messageID, &replyToID, - &userID, &username, &createdAt, &triggerAt, &content, &processed, - ); err != nil { - return nil, err - } - - reminder := &model.Reminder{ - ID: id, - Platform: platform, - ChannelID: channelID, - MessageID: messageID, - ReplyToID: replyToID, - UserID: userID, - Username: username, - CreatedAt: createdAt, - TriggerAt: triggerAt, - Content: content, - Processed: processed, - } - - reminders = append(reminders, reminder) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - if len(reminders) == 0 { - return make([]*model.Reminder, 0), nil - } - - return reminders, nil -} - -// MarkReminderAsProcessed marks a reminder as processed -func (d *Database) MarkReminderAsProcessed(id int64) error { - query := ` - UPDATE reminders - SET processed = 1 - WHERE id = ? - ` - - _, err := d.db.Exec(query, id) - return err -} - // Helper function to hash password func hashPassword(password string) (string, error) { // Use bcrypt for secure password hashing @@ -735,25 +609,25 @@ func initDatabase(db *sql.DB) error { if err := migration.EnsureMigrationTable(db); err != nil { return fmt.Errorf("failed to create migration table: %w", err) } - + // Get applied migrations applied, err := migration.GetAppliedMigrations(db) if err != nil { return fmt.Errorf("failed to get applied migrations: %w", err) } - + // Get all migration versions allMigrations := make([]int, 0, len(migration.Migrations)) for version := range migration.Migrations { allMigrations = append(allMigrations, version) } - + // Create a map of applied migrations for quick lookup appliedMap := make(map[int]bool) for _, version := range applied { appliedMap[version] = true } - + // Count pending migrations pendingCount := 0 for _, version := range allMigrations { @@ -761,7 +635,7 @@ func initDatabase(db *sql.DB) error { pendingCount++ } } - + // Run migrations if needed if pendingCount > 0 { fmt.Printf("Running %d pending database migrations...\n", pendingCount) @@ -772,6 +646,6 @@ func initDatabase(db *sql.DB) error { } else { fmt.Println("Database schema is up to date.") } - + return nil } diff --git a/internal/migration/migration.go b/internal/migration/migration.go index 63da5d8..44096f3 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -49,11 +49,7 @@ func GetAppliedMigrations(db *sql.DB) ([]int, error) { if err != nil { return nil, err } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() + defer rows.Close() var versions []int for rows.Next() { @@ -132,9 +128,7 @@ func Migrate(db *sql.DB) error { // Apply the migration if err := migration.Up(db); err != nil { - if err := tx.Rollback(); err != nil { - fmt.Printf("Error rolling back transaction: %v\n", err) - } + tx.Rollback() return fmt.Errorf("failed to apply migration %d: %w", version, err) } @@ -143,9 +137,7 @@ func Migrate(db *sql.DB) error { "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", version, time.Now(), ); err != nil { - if err := tx.Rollback(); err != nil { - fmt.Printf("Error rolling back transaction: %v\n", err) - } + tx.Rollback() return fmt.Errorf("failed to mark migration %d as applied: %w", version, err) } @@ -196,17 +188,13 @@ func MigrateDown(db *sql.DB, targetVersion int) error { // Apply the down migration if err := migration.Down(db); err != nil { - if err := tx.Rollback(); err != nil { - fmt.Printf("Error rolling back transaction: %v\n", err) - } + tx.Rollback() return fmt.Errorf("failed to roll back migration %d: %w", version, err) } // Remove from applied list if _, err := tx.Exec("DELETE FROM schema_migrations WHERE version = ?", version); err != nil { - if err := tx.Rollback(); err != nil { - fmt.Printf("Error rolling back transaction: %v\n", err) - } + tx.Rollback() return fmt.Errorf("failed to remove migration %d from applied list: %w", version, err) } @@ -220,4 +208,4 @@ func MigrateDown(db *sql.DB, targetVersion int) error { } return nil -} +} \ No newline at end of file diff --git a/internal/migration/migrations.go b/internal/migration/migrations.go index 8db229b..2852113 100644 --- a/internal/migration/migrations.go +++ b/internal/migration/migrations.go @@ -8,7 +8,6 @@ import ( func init() { // Register migrations Register(1, "Initial schema with bcrypt passwords", migrateInitialSchemaUp, migrateInitialSchemaDown) - Register(2, "Add reminders table", migrateRemindersUp, migrateRemindersDown) } // Initial schema creation with bcrypt passwords - version 1 @@ -61,14 +60,14 @@ func migrateInitialSchemaUp(db *sql.DB) error { if err != nil { return err } - + // Check if users table is empty before inserting var count int err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) if err != nil { return err } - + if count == 0 { _, err = db.Exec( "INSERT INTO users (username, password) VALUES (?, ?)", @@ -100,29 +99,4 @@ func migrateInitialSchemaDown(db *sql.DB) error { } return nil -} - -// Add reminders table - version 2 -func migrateRemindersUp(db *sql.DB) error { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS reminders ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - platform TEXT NOT NULL, - channel_id TEXT NOT NULL, - message_id TEXT NOT NULL, - reply_to_id TEXT NOT NULL, - user_id TEXT NOT NULL, - username TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - trigger_at TIMESTAMP NOT NULL, - content TEXT NOT NULL, - processed BOOLEAN NOT NULL DEFAULT 0 - ) - `) - return err -} - -func migrateRemindersDown(db *sql.DB) error { - _, err := db.Exec(`DROP TABLE IF EXISTS reminders`) - return err -} +} \ No newline at end of file diff --git a/internal/model/message.go b/internal/model/message.go index e6f86f6..fe8c5e4 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -6,25 +6,25 @@ import ( // 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 - Raw map[string]interface{} + Text string + Chat string + Channel *Channel + Author string + FromBot bool + Date time.Time + ID string + ReplyTo string + Raw map[string]interface{} } // Channel represents a chat channel type Channel struct { - ID int64 - Platform string + ID int64 + Platform string PlatformChannelID string - ChannelRaw map[string]interface{} - Enabled bool - Plugins map[string]*ChannelPlugin + ChannelRaw map[string]interface{} + Enabled bool + Plugins map[string]*ChannelPlugin } // HasEnabledPlugin checks if a plugin is enabled for this channel @@ -40,18 +40,18 @@ func (c *Channel) HasEnabledPlugin(pluginID string) bool { 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 @@ -65,7 +65,7 @@ func (c *Channel) ChannelName() string { return firstName } } - + return c.PlatformChannelID } @@ -83,19 +83,4 @@ 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 -} +} \ No newline at end of file diff --git a/internal/model/plugin.go b/internal/model/plugin.go index 9f2b34a..ffc3c2f 100644 --- a/internal/model/plugin.go +++ b/internal/model/plugin.go @@ -13,16 +13,16 @@ var ( type Plugin interface { // GetID returns the plugin ID GetID() string - + // GetName returns the plugin name GetName() string - + // GetHelp returns the plugin help text GetHelp() string - + // RequiresConfig indicates if the plugin requires configuration RequiresConfig() bool - + // OnMessage processes an incoming message and returns response messages OnMessage(msg *Message, config map[string]interface{}) []*Message -} +} \ No newline at end of file diff --git a/internal/platform/slack/slack.go b/internal/platform/slack/slack.go index 9c12b1f..3683ada 100644 --- a/internal/platform/slack/slack.go +++ b/internal/platform/slack/slack.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "io" + "io/ioutil" "net/http" "strings" "time" @@ -37,15 +37,11 @@ func (s *SlackPlatform) Init(_ *config.Config) error { // 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) + body, err := ioutil.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) - } - }() + defer r.Body.Close() // Parse JSON var requestData map[string]interface{} @@ -198,11 +194,7 @@ func (s *SlackPlatform) SendMessage(msg *model.Message) error { if err != nil { return err } - defer func() { - if err := resp.Body.Close(); err != nil { - fmt.Printf("Error closing response body: %v\n", err) - } - }() + defer resp.Body.Close() // Check response if resp.StatusCode != http.StatusOK { diff --git a/internal/platform/telegram/telegram.go b/internal/platform/telegram/telegram.go index 0edb729..a9ff2db 100644 --- a/internal/platform/telegram/telegram.go +++ b/internal/platform/telegram/telegram.go @@ -62,11 +62,7 @@ func (t *TelegramPlatform) Init(cfg *config.Config) error { t.log.Error("Failed to set webhook", "error", err) return fmt.Errorf("failed to set webhook: %w", err) } - defer func() { - if err := resp.Body.Close(); err != nil { - t.log.Error("Error closing response body", "error", err) - } - }() + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) @@ -89,11 +85,7 @@ func (t *TelegramPlatform) ParseIncomingMessage(r *http.Request) (*model.Message t.log.Error("Failed to read request body", "error", err) return nil, err } - defer func() { - if err := r.Body.Close(); err != nil { - t.log.Error("Error closing request body", "error", err) - } - }() + defer r.Body.Close() // Parse JSON var update struct { @@ -111,11 +103,8 @@ func (t *TelegramPlatform) ParseIncomingMessage(r *http.Request) (*model.Message Title string `json:"title,omitempty"` Username string `json:"username,omitempty"` } `json:"chat"` - Date int `json:"date"` - Text string `json:"text"` - ReplyToMessage struct { - MessageID int `json:"message_id"` - } `json:"reply_to_message"` + Date int `json:"date"` + Text string `json:"text"` } `json:"message"` } @@ -139,7 +128,6 @@ func (t *TelegramPlatform) ParseIncomingMessage(r *http.Request) (*model.Message FromBot: update.Message.From.IsBot, Date: time.Unix(int64(update.Message.Date), 0), ID: strconv.Itoa(update.Message.MessageID), - ReplyTo: strconv.Itoa(update.Message.ReplyToMessage.MessageID), Raw: raw, } @@ -259,11 +247,7 @@ func (t *TelegramPlatform) SendMessage(msg *model.Message) error { t.log.Error("Failed to send message", "error", err) return err } - defer func() { - if err := resp.Body.Close(); err != nil { - t.log.Error("Error closing response body", "error", err) - } - }() + defer resp.Body.Close() // Check response if resp.StatusCode != http.StatusOK { @@ -275,4 +259,4 @@ func (t *TelegramPlatform) SendMessage(msg *model.Message) error { t.log.Debug("Message sent successfully") return nil -} +} \ No newline at end of file diff --git a/internal/plugin/fun/dice.go b/internal/plugin/fun/dice.go index 2d5533b..00fc7cc 100644 --- a/internal/plugin/fun/dice.go +++ b/internal/plugin/fun/dice.go @@ -107,10 +107,9 @@ func (p *DicePlugin) rollDice(formula string) (int, error) { return 0, fmt.Errorf("invalid modifier") } - switch matches[3] { - case "+": + if matches[3] == "+" { total += modifier - case "-": + } else if matches[3] == "-" { total -= modifier } } diff --git a/internal/plugin/reminder/reminder.go b/internal/plugin/reminder/reminder.go deleted file mode 100644 index 5eb47f9..0000000 --- a/internal/plugin/reminder/reminder.go +++ /dev/null @@ -1,171 +0,0 @@ -package reminder - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" - - "git.nakama.town/fmartingr/butterrobot/internal/model" - "git.nakama.town/fmartingr/butterrobot/internal/plugin" -) - -// Duration regex patterns to match reminders -var ( - remindMePattern = regexp.MustCompile(`(?i)^!remindme\s(\d+)(y|mo|d|h|m|s)$`) -) - -// ReminderCreator is an interface for creating reminders -type ReminderCreator interface { - CreateReminder(platform, channelID, messageID, replyToID, userID, username, content string, triggerAt time.Time) (*model.Reminder, error) -} - -// Reminder is a plugin that sets reminders for messages -type Reminder struct { - plugin.BasePlugin - creator ReminderCreator -} - -// New creates a new Reminder plugin -func New(creator ReminderCreator) *Reminder { - return &Reminder{ - BasePlugin: plugin.BasePlugin{ - ID: "reminder.remindme", - Name: "Remind Me", - Help: "Reply to a message with `!remindme ` to set a reminder (e.g., `!remindme 2d` for 2 days, `!remindme 1y` for 1 year).", - ConfigRequired: false, - }, - creator: creator, - } -} - -// OnMessage processes incoming messages -func (r *Reminder) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message { - // Only process replies to messages - if msg.ReplyTo == "" { - return nil - } - - // Check if the message is a reminder command - match := remindMePattern.FindStringSubmatch(msg.Text) - if match == nil { - return nil - } - - // Parse the duration - amount, err := strconv.Atoi(match[1]) - if err != nil { - return []*model.Message{ - { - Text: "Invalid duration format. Please use a number followed by y (years), mo (months), d (days), h (hours), m (minutes), or s (seconds).", - Chat: msg.Chat, - Channel: msg.Channel, - Author: "bot", - FromBot: true, - Date: time.Now(), - ReplyTo: msg.ID, - }, - } - } - - // Calculate the trigger time - var duration time.Duration - unit := match[2] - switch strings.ToLower(unit) { - case "y": - duration = time.Duration(amount) * 365 * 24 * time.Hour - case "mo": - duration = time.Duration(amount) * 30 * 24 * time.Hour - case "d": - duration = time.Duration(amount) * 24 * time.Hour - case "h": - duration = time.Duration(amount) * time.Hour - case "m": - duration = time.Duration(amount) * time.Minute - case "s": - duration = time.Duration(amount) * time.Second - default: - return []*model.Message{ - { - Text: "Invalid duration unit. Please use y (years), mo (months), d (days), h (hours), m (minutes), or s (seconds).", - Chat: msg.Chat, - Channel: msg.Channel, - Author: "bot", - FromBot: true, - Date: time.Now(), - ReplyTo: msg.ID, - }, - } - } - - triggerAt := time.Now().Add(duration) - - // Determine the username for the reminder - username := msg.Author - if username == "" { - // Try to extract username from message raw data - if authorData, ok := msg.Raw["author"].(map[string]interface{}); ok { - if name, ok := authorData["username"].(string); ok { - username = name - } else if name, ok := authorData["name"].(string); ok { - username = name - } - } - } - - // Create the reminder - _, err = r.creator.CreateReminder( - msg.Channel.Platform, - msg.Chat, - msg.ID, - msg.ReplyTo, - msg.Author, - username, - "", // No additional content for now - triggerAt, - ) - - if err != nil { - return []*model.Message{ - { - Text: fmt.Sprintf("Failed to create reminder: %v", err), - Chat: msg.Chat, - Channel: msg.Channel, - Author: "bot", - FromBot: true, - Date: time.Now(), - ReplyTo: msg.ID, - }, - } - } - - // Format the acknowledgment message - var confirmText string - switch strings.ToLower(unit) { - case "y": - confirmText = fmt.Sprintf("I'll remind you about this message in %d year(s) on %s", amount, triggerAt.Format("Mon, Jan 2, 2006 at 15:04")) - case "mo": - confirmText = fmt.Sprintf("I'll remind you about this message in %d month(s) on %s", amount, triggerAt.Format("Mon, Jan 2 at 15:04")) - case "d": - confirmText = fmt.Sprintf("I'll remind you about this message in %d day(s) on %s", amount, triggerAt.Format("Mon, Jan 2 at 15:04")) - case "h": - confirmText = fmt.Sprintf("I'll remind you about this message in %d hour(s) at %s", amount, triggerAt.Format("15:04")) - case "m": - confirmText = fmt.Sprintf("I'll remind you about this message in %d minute(s) at %s", amount, triggerAt.Format("15:04")) - case "s": - confirmText = fmt.Sprintf("I'll remind you about this message in %d second(s)", amount) - } - - return []*model.Message{ - { - Text: confirmText, - Chat: msg.Chat, - Channel: msg.Channel, - Author: "bot", - FromBot: true, - Date: time.Now(), - ReplyTo: msg.ID, - }, - } -} diff --git a/internal/plugin/reminder/reminder_test.go b/internal/plugin/reminder/reminder_test.go deleted file mode 100644 index 3070918..0000000 --- a/internal/plugin/reminder/reminder_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package reminder - -import ( - "testing" - "time" - - "git.nakama.town/fmartingr/butterrobot/internal/model" -) - -// MockCreator is a mock implementation of ReminderCreator for testing -type MockCreator struct { - reminders []*model.Reminder -} - -func (m *MockCreator) CreateReminder(platform, channelID, messageID, replyToID, userID, username, content string, triggerAt time.Time) (*model.Reminder, error) { - reminder := &model.Reminder{ - ID: int64(len(m.reminders) + 1), - Platform: platform, - ChannelID: channelID, - MessageID: messageID, - ReplyToID: replyToID, - UserID: userID, - Username: username, - Content: content, - TriggerAt: triggerAt, - } - m.reminders = append(m.reminders, reminder) - return reminder, nil -} - -func TestReminderOnMessage(t *testing.T) { - creator := &MockCreator{reminders: make([]*model.Reminder, 0)} - plugin := New(creator) - - tests := []struct { - name string - message *model.Message - expectResponse bool - expectReminder bool - }{ - { - name: "Valid reminder command - years", - message: &model.Message{ - Text: "!remindme 1y", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Valid reminder command - months", - message: &model.Message{ - Text: "!remindme 3mo", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Valid reminder command - days", - message: &model.Message{ - Text: "!remindme 2d", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Valid reminder command - hours", - message: &model.Message{ - Text: "!remindme 5h", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Valid reminder command - minutes", - message: &model.Message{ - Text: "!remindme 30m", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Valid reminder command - seconds", - message: &model.Message{ - Text: "!remindme 60s", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: true, - expectReminder: true, - }, - { - name: "Not a reply", - message: &model.Message{ - Text: "!remindme 2d", - ReplyTo: "", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: false, - expectReminder: false, - }, - { - name: "Not a reminder command", - message: &model.Message{ - Text: "hello world", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: false, - expectReminder: false, - }, - { - name: "Invalid duration format", - message: &model.Message{ - Text: "!remindme abc", - ReplyTo: "original-message-id", - Author: "testuser", - Channel: &model.Channel{Platform: "test"}, - }, - expectResponse: false, - expectReminder: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - initialCount := len(creator.reminders) - responses := plugin.OnMessage(tt.message, nil) - - if tt.expectResponse && len(responses) == 0 { - t.Errorf("Expected response, but got none") - } - - if !tt.expectResponse && len(responses) > 0 { - t.Errorf("Expected no response, but got %d", len(responses)) - } - - if tt.expectReminder && len(creator.reminders) != initialCount+1 { - t.Errorf("Expected reminder to be created, but it wasn't") - } - - if !tt.expectReminder && len(creator.reminders) != initialCount { - t.Errorf("Expected no reminder to be created, but got %d", len(creator.reminders)-initialCount) - } - }) - } -} diff --git a/internal/plugin/social/instagram.go b/internal/plugin/social/instagram.go deleted file mode 100644 index 7ff74a5..0000000 --- a/internal/plugin/social/instagram.go +++ /dev/null @@ -1,74 +0,0 @@ -package social - -import ( - "net/url" - "regexp" - "strings" - - "git.nakama.town/fmartingr/butterrobot/internal/model" - "git.nakama.town/fmartingr/butterrobot/internal/plugin" -) - -// InstagramExpander transforms instagram.com links to ddinstagram.com links -type InstagramExpander struct { - plugin.BasePlugin -} - -// New creates a new InstagramExpander instance -func NewInstagramExpander() *InstagramExpander { - return &InstagramExpander{ - BasePlugin: plugin.BasePlugin{ - ID: "social.instagram", - Name: "Instagram Link Expander", - Help: "Automatically converts instagram.com links to ddinstagram.com links and removes tracking parameters", - }, - } -} - -// OnMessage handles incoming messages -func (p *InstagramExpander) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message { - // Skip empty messages - if strings.TrimSpace(msg.Text) == "" { - return nil - } - - // Regex to match instagram.com links - // Match both http://instagram.com and https://instagram.com formats - // Also match www.instagram.com - instagramRegex := regexp.MustCompile(`https?://(www\.)?(instagram\.com)/[^\s]+`) - - // Check if the message contains an Instagram link - if !instagramRegex.MatchString(msg.Text) { - return nil - } - - // Replace instagram.com with ddinstagram.com in the message and clean query parameters - transformed := instagramRegex.ReplaceAllStringFunc(msg.Text, func(link string) string { - // Parse the URL - parsedURL, err := url.Parse(link) - if err != nil { - // If parsing fails, just do the simple replacement - link = strings.Replace(link, "instagram.com", "ddinstagram.com", 1) - return link - } - - // Change the host - parsedURL.Host = strings.Replace(parsedURL.Host, "instagram.com", "ddinstagram.com", 1) - - // Remove query parameters - parsedURL.RawQuery = "" - - // Return the cleaned URL - return parsedURL.String() - }) - - // Create response message - response := &model.Message{ - Text: transformed, - Chat: msg.Chat, - ReplyTo: msg.ID, - Channel: msg.Channel, - } - - return []*model.Message{response} -} diff --git a/internal/plugin/social/twitter.go b/internal/plugin/social/twitter.go deleted file mode 100644 index 837b6c9..0000000 --- a/internal/plugin/social/twitter.go +++ /dev/null @@ -1,79 +0,0 @@ -package social - -import ( - "net/url" - "regexp" - "strings" - - "git.nakama.town/fmartingr/butterrobot/internal/model" - "git.nakama.town/fmartingr/butterrobot/internal/plugin" -) - -// TwitterExpander transforms twitter.com links to fxtwitter.com links -type TwitterExpander struct { - plugin.BasePlugin -} - -// New creates a new TwitterExpander instance -func NewTwitterExpander() *TwitterExpander { - return &TwitterExpander{ - BasePlugin: plugin.BasePlugin{ - ID: "social.twitter", - Name: "Twitter Link Expander", - Help: "Automatically converts twitter.com links to fxtwitter.com links and removes tracking parameters", - }, - } -} - -// OnMessage handles incoming messages -func (p *TwitterExpander) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message { - // Skip empty messages - if strings.TrimSpace(msg.Text) == "" { - return nil - } - - // Regex to match twitter.com links - // Match both http://twitter.com and https://twitter.com formats - // Also match www.twitter.com - twitterRegex := regexp.MustCompile(`https?://(www\.)?(twitter\.com|x\.com)/[^\s]+`) - - // Check if the message contains a Twitter link - if !twitterRegex.MatchString(msg.Text) { - return nil - } - - // Replace twitter.com with fxtwitter.com in the message and clean query parameters - transformed := twitterRegex.ReplaceAllStringFunc(msg.Text, func(link string) string { - // Parse the URL - parsedURL, err := url.Parse(link) - if err != nil { - // If parsing fails, just do the simple replacement - link = strings.Replace(link, "twitter.com", "fxtwitter.com", 1) - link = strings.Replace(link, "x.com", "fxtwitter.com", 1) - return link - } - - // Change the host - if strings.Contains(parsedURL.Host, "twitter.com") { - parsedURL.Host = strings.Replace(parsedURL.Host, "twitter.com", "fxtwitter.com", 1) - } else if strings.Contains(parsedURL.Host, "x.com") { - parsedURL.Host = strings.Replace(parsedURL.Host, "x.com", "fxtwitter.com", 1) - } - - // Remove query parameters - parsedURL.RawQuery = "" - - // Return the cleaned URL - return parsedURL.String() - }) - - // Create response message - response := &model.Message{ - Text: transformed, - Chat: msg.Chat, - ReplyTo: msg.ID, - Channel: msg.Channel, - } - - return []*model.Message{response} -} diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 692816e..668bf60 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -3,9 +3,6 @@ package queue import ( "log/slog" "sync" - "time" - - "git.nakama.town/fmartingr/butterrobot/internal/model" ) // Item represents a queue item @@ -17,19 +14,14 @@ type Item struct { // HandlerFunc defines a function that processes queue items type HandlerFunc func(item Item) -// ReminderHandlerFunc defines a function that processes reminder items -type ReminderHandlerFunc func(reminder *model.Reminder) - // Queue represents a message queue type Queue struct { - items chan Item - wg sync.WaitGroup - quit chan struct{} - logger *slog.Logger - running bool - runMutex sync.Mutex - reminderTicker *time.Ticker - reminderHandler ReminderHandlerFunc + items chan Item + wg sync.WaitGroup + quit chan struct{} + logger *slog.Logger + running bool + runMutex sync.Mutex } // New creates a new Queue instance @@ -57,24 +49,6 @@ func (q *Queue) Start(handler HandlerFunc) { go q.worker(handler) } -// StartReminderScheduler starts the reminder scheduler -func (q *Queue) StartReminderScheduler(handler ReminderHandlerFunc) { - q.runMutex.Lock() - defer q.runMutex.Unlock() - - if q.reminderTicker != nil { - return - } - - q.reminderHandler = handler - - // Check for reminders every minute - q.reminderTicker = time.NewTicker(1 * time.Minute) - - q.wg.Add(1) - go q.reminderWorker() -} - // Stop stops processing queue items func (q *Queue) Stop() { q.runMutex.Lock() @@ -85,12 +59,6 @@ func (q *Queue) Stop() { } q.running = false - - // Stop reminder ticker if it exists - if q.reminderTicker != nil { - q.reminderTicker.Stop() - } - close(q.quit) q.wg.Wait() } @@ -128,34 +96,4 @@ func (q *Queue) worker(handler HandlerFunc) { return } } -} - -// reminderWorker processes reminder items on a schedule -func (q *Queue) reminderWorker() { - defer q.wg.Done() - - for { - select { - case <-q.reminderTicker.C: - // This is triggered every minute to check for pending reminders - q.logger.Debug("Checking for pending reminders") - - if q.reminderHandler != nil { - // The handler is responsible for fetching and processing reminders - func() { - defer func() { - if r := recover(); r != nil { - q.logger.Error("Panic in reminder worker", "error", r) - } - }() - - // Call the handler with a nil reminder to indicate it should check the database - q.reminderHandler(nil) - }() - } - case <-q.quit: - // Quit worker - return - } - } -} +} \ No newline at end of file