Compare commits

...

2 commits

Author SHA1 Message Date
21e4c434fd
docs: updated plugin docs
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2025-04-21 18:10:30 +02:00
a0f12efd65
feat: show version in admin page 2025-04-21 18:08:40 +02:00
5 changed files with 149 additions and 16 deletions

View file

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

View file

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

View file

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

View file

@ -117,6 +117,19 @@
</div>
</div>
<footer class="footer footer-transparent d-print-none">
<div class="container-xl">
<div class="row text-center align-items-center flex-row-reverse">
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item">
ButterRobot {{if .Version}}v{{.Version}}{{else}}(development){{end}}
</li>
</ul>
</div>
</div>
</div>
</footer>
</div>
<script src="https://unpkg.com/@tabler/core@latest/dist/js/tabler.min.js"></script>

View file

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