Compare commits
No commits in common. "c9edb57505314aa573d3bdddd1aa2e96abdc76de" and "323ea4e8cdfc7ac6c2226317629fbd1ff0782f7e" have entirely different histories.
c9edb57505
...
323ea4e8cd
11 changed files with 55 additions and 87 deletions
|
@ -1,6 +1,9 @@
|
|||
# Butter Robot
|
||||
|
||||

|
||||
| Stable | 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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue