From e0ae0c2a0b5aba03c5f08bf52e96081479113c37 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Mon, 21 Apr 2025 15:50:28 +0200 Subject: [PATCH 1/4] fix: missing ca-certs --- .goreleaser.yml | 2 +- cmd/butterrobot/main.go | 2 ++ go.mod | 1 + go.sum | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index c89e189..a3836e9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -93,7 +93,7 @@ docker_manifests: nfpms: - maintainer: Felipe Martin - description: SMTP server to forward messages to shoutrrr endpoints + description: A chatbot server with customizable commands and triggers homepage: https://git.nakama.town/fmartingr/butterrobot license: AGPL-3.0 formats: diff --git a/cmd/butterrobot/main.go b/cmd/butterrobot/main.go index 2982a96..3bc56cb 100644 --- a/cmd/butterrobot/main.go +++ b/cmd/butterrobot/main.go @@ -8,6 +8,8 @@ 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/go.mod b/go.mod index 3f17f0d..cd1bee5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ 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 f331cb5..00c4a3c 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ 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= From c920eb94a035913bf4f5dc98fe6ee26ff0c3f66f Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Mon, 21 Apr 2025 18:03:07 +0200 Subject: [PATCH 2/4] feat: added twitter and instagram link expanders --- internal/app/app.go | 3 ++ internal/plugin/social/instagram.go | 76 +++++++++++++++++++++++++++ internal/plugin/social/twitter.go | 79 +++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 internal/plugin/social/instagram.go create mode 100644 internal/plugin/social/twitter.go diff --git a/internal/app/app.go b/internal/app/app.go index 8d4ffcd..2e15bf6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -20,6 +20,7 @@ import ( "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/social" "git.nakama.town/fmartingr/butterrobot/internal/queue" ) @@ -72,6 +73,8 @@ 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()) // Initialize routes a.initializeRoutes() diff --git a/internal/plugin/social/instagram.go b/internal/plugin/social/instagram.go new file mode 100644 index 0000000..a4f758a --- /dev/null +++ b/internal/plugin/social/instagram.go @@ -0,0 +1,76 @@ +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 + if strings.Contains(parsedURL.Host, "instagram.com") { + 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 new file mode 100644 index 0000000..837b6c9 --- /dev/null +++ b/internal/plugin/social/twitter.go @@ -0,0 +1,79 @@ +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} +} From a0f12efd65c0d1999cbeaf39b47727a284c02f01 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Mon, 21 Apr 2025 18:08:40 +0200 Subject: [PATCH 3/4] feat: show version in admin page --- internal/admin/admin.go | 6 ++++- internal/admin/templates/_base.html | 13 +++++++++++ internal/app/app.go | 36 ++++++++++++++++++----------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/internal/admin/admin.go b/internal/admin/admin.go index 045d980..822495a 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -46,6 +46,7 @@ type TemplateData struct { Channels []*model.Channel Channel *model.Channel ChannelPlugin *model.ChannelPlugin + Version string } // Admin represents the admin interface @@ -55,10 +56,11 @@ 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) *Admin { +func New(cfg *config.Config, database *db.Database, version string) *Admin { // Create session store with appropriate options store := sessions.NewCookieStore([]byte(cfg.SecretKey)) store.Options = &sessions.Options{ @@ -126,6 +128,7 @@ func New(cfg *config.Config, database *db.Database) *Admin { store: store, templates: templates, baseTemplate: baseTemplate, + version: version, } } @@ -264,6 +267,7 @@ 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] diff --git a/internal/admin/templates/_base.html b/internal/admin/templates/_base.html index 4a414e3..3ebdf85 100644 --- a/internal/admin/templates/_base.html +++ b/internal/admin/templates/_base.html @@ -117,6 +117,19 @@ +
+
+
+
+
    +
  • + ButterRobot {{if .Version}}v{{.Version}}{{else}}(development){{end}} +
  • +
+
+
+
+
diff --git a/internal/app/app.go b/internal/app/app.go index 2e15bf6..5126672 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "runtime/debug" "strings" "syscall" "time" @@ -26,12 +27,13 @@ import ( // 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 + config *config.Config + logger *slog.Logger + db *db.Database + router *http.ServeMux + queue *queue.Queue + admin *admin.Admin + version string } // New creates a new App instance @@ -48,16 +50,24 @@ 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) + adminInterface := admin.New(cfg, database, version) return &App{ - config: cfg, - logger: logger, - db: database, - router: router, - queue: messageQueue, - admin: adminInterface, + config: cfg, + logger: logger, + db: database, + router: router, + queue: messageQueue, + admin: adminInterface, + version: version, }, nil } From 21e4c434fd8ce549ee128ab2b1d1795a4a6007ef Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Mon, 21 Apr 2025 18:10:30 +0200 Subject: [PATCH 4/4] docs: updated plugin docs --- docs/creating-a-plugin.md | 105 +++++++++++++++++++++++++++++++++++++- docs/plugins.md | 5 ++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/docs/creating-a-plugin.md b/docs/creating-a-plugin.md index 945d03c..469491a 100644 --- a/docs/creating-a-plugin.md +++ b/docs/creating-a-plugin.md @@ -1,6 +1,18 @@ # Creating a Plugin -## Example +## 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 This simple "Marco Polo" plugin will answer _Polo_ to the user that says _Marco_: @@ -47,6 +59,92 @@ 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 @@ -55,7 +153,10 @@ func (a *App) Run() error { // ... // Register plugins - plugin.Register(myplugin.New()) + 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 // ... } diff --git a/docs/plugins.md b/docs/plugins.md index 11e3d16..2988f80 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -9,3 +9,8 @@ - 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. + +### 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.