Compare commits

..

No commits in common. "c9edb57505314aa573d3bdddd1aa2e96abdc76de" and "323ea4e8cdfc7ac6c2226317629fbd1ff0782f7e" have entirely different histories.

11 changed files with 55 additions and 87 deletions

View file

@ -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

View file

@ -194,7 +194,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 +249,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
@ -323,10 +334,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")

View file

@ -152,9 +152,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 +175,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 +184,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 +200,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

View file

@ -234,11 +234,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 +415,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 +454,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)
@ -653,11 +646,7 @@ func (d *Database) GetPendingReminders() ([]*model.Reminder, 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 reminders []*model.Reminder

View file

@ -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)
}

View file

@ -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 {

View file

@ -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 {
@ -259,11 +251,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 {

View file

@ -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
}
}

View file

@ -44,7 +44,14 @@ func New(creator ReminderCreator) *Reminder {
func (r *Reminder) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
// Only process replies to messages
if msg.ReplyTo == "" {
return nil
return []*model.Message{
{
Text: "Please reply to a message with `!remindme <duration>` to set a reminder.",
Chat: msg.Chat,
Channel: msg.Channel,
ReplyTo: msg.ID,
},
}
}
// Check if the message is a reminder command

View file

@ -161,4 +161,4 @@ func TestReminderOnMessage(t *testing.T) {
}
})
}
}
}

View file

@ -53,7 +53,9 @@ func (p *InstagramExpander) OnMessage(msg *model.Message, config map[string]inte
}
// Change the host
parsedURL.Host = strings.Replace(parsedURL.Host, "instagram.com", "ddinstagram.com", 1)
if strings.Contains(parsedURL.Host, "instagram.com") {
parsedURL.Host = strings.Replace(parsedURL.Host, "instagram.com", "ddinstagram.com", 1)
}
// Remove query parameters
parsedURL.RawQuery = ""