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.
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 @@
+
diff --git a/internal/app/app.go b/internal/app/app.go
index 8d4ffcd..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"
@@ -20,17 +21,19 @@ 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"
)
// 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
@@ -47,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
}
@@ -72,6 +83,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}
+}