287 lines
7.3 KiB
Markdown
287 lines
7.3 KiB
Markdown
# Creating a Plugin
|
|
|
|
## 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
|
|
- **Security**: Moderation and protection features like domain blocking
|
|
|
|
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_:
|
|
|
|
```go
|
|
package myplugin
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.nakama.town/fmartingr/butterrobot/internal/model"
|
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
|
)
|
|
|
|
// MarcoPlugin is a simple Marco/Polo plugin
|
|
type MarcoPlugin struct {
|
|
plugin.BasePlugin
|
|
}
|
|
|
|
// New creates a new MarcoPlugin instance
|
|
func New() *MarcoPlugin {
|
|
return &MarcoPlugin{
|
|
BasePlugin: plugin.BasePlugin{
|
|
ID: "test.marco",
|
|
Name: "Marco/Polo",
|
|
Help: "Responds to 'Marco' with 'Polo'",
|
|
},
|
|
}
|
|
}
|
|
|
|
// OnMessage handles incoming messages
|
|
func (p *MarcoPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
|
|
if !strings.EqualFold(strings.TrimSpace(msg.Text), "Marco") {
|
|
return nil
|
|
}
|
|
|
|
response := &model.Message{
|
|
Text: "Polo",
|
|
Chat: msg.Chat,
|
|
ReplyTo: msg.ID,
|
|
Channel: msg.Channel,
|
|
}
|
|
|
|
return []*model.Message{response}
|
|
}
|
|
```
|
|
|
|
### Configuration-Enabled Plugin
|
|
|
|
This plugin requires configuration to be set in the admin interface. It demonstrates how to create plugins that need channel-specific configuration:
|
|
|
|
```go
|
|
package security
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.nakama.town/fmartingr/butterrobot/internal/model"
|
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
|
)
|
|
|
|
// DomainBlockPlugin is a plugin that blocks messages containing links from specific domains
|
|
type DomainBlockPlugin struct {
|
|
plugin.BasePlugin
|
|
}
|
|
|
|
// New creates a new DomainBlockPlugin instance
|
|
func New() *DomainBlockPlugin {
|
|
return &DomainBlockPlugin{
|
|
BasePlugin: plugin.BasePlugin{
|
|
ID: "security.domainblock",
|
|
Name: "Domain Blocker",
|
|
Help: "Blocks messages containing links from configured domains",
|
|
ConfigRequired: true, // Mark this plugin as requiring configuration
|
|
},
|
|
}
|
|
}
|
|
|
|
// OnMessage processes incoming messages
|
|
func (p *DomainBlockPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
|
|
// Get blocked domains from config
|
|
blockedDomainsStr, ok := config["blocked_domains"].(string)
|
|
if !ok || blockedDomainsStr == "" {
|
|
return nil // No blocked domains configured
|
|
}
|
|
|
|
// Split and clean blocked domains
|
|
blockedDomains := strings.Split(blockedDomainsStr, ",")
|
|
for i, domain := range blockedDomains {
|
|
blockedDomains[i] = strings.ToLower(strings.TrimSpace(domain))
|
|
}
|
|
|
|
// Extract domains from message
|
|
urlRegex := regexp.MustCompile(`https?://([^\s/$.?#].[^\s]*)`)
|
|
matches := urlRegex.FindAllStringSubmatch(msg.Text, -1)
|
|
|
|
// Check if any extracted domains are blocked
|
|
for _, match := range matches {
|
|
if len(match) < 2 {
|
|
continue
|
|
}
|
|
|
|
domain := strings.ToLower(match[1])
|
|
|
|
for _, blockedDomain := range blockedDomains {
|
|
if blockedDomain == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasSuffix(domain, blockedDomain) || domain == blockedDomain {
|
|
// Domain is blocked, create warning message
|
|
response := &model.Message{
|
|
Text: fmt.Sprintf("⚠️ Message contained a link to blocked domain: %s", blockedDomain),
|
|
Chat: msg.Chat,
|
|
ReplyTo: msg.ID,
|
|
Channel: msg.Channel,
|
|
}
|
|
return []*model.Message{response}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
plugin.Register(New())
|
|
}
|
|
```
|
|
|
|
### 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}
|
|
}
|
|
```
|
|
|
|
## Enabling Configuration for Plugins
|
|
|
|
To indicate that your plugin requires configuration:
|
|
|
|
1. Set `ConfigRequired: true` in the BasePlugin struct:
|
|
```go
|
|
BasePlugin: plugin.BasePlugin{
|
|
ID: "myplugin.id",
|
|
Name: "Plugin Name",
|
|
Help: "Help text",
|
|
ConfigRequired: true,
|
|
},
|
|
```
|
|
|
|
2. Access the configuration in the OnMessage method:
|
|
```go
|
|
func (p *MyPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
|
|
// Extract configuration values
|
|
configValue, ok := config["some_config_key"].(string)
|
|
if !ok || configValue == "" {
|
|
// Handle missing or empty configuration
|
|
return nil
|
|
}
|
|
|
|
// Use the configuration...
|
|
}
|
|
```
|
|
|
|
3. The admin interface will show a "Configure" button for plugins that require configuration.
|
|
|
|
## Registering Plugins
|
|
|
|
To use the plugin, register it in your application:
|
|
|
|
```go
|
|
// In app.go or similar initialization file
|
|
func (a *App) Run() error {
|
|
// ...
|
|
|
|
// Register plugins
|
|
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
|
|
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Alternatively, you can register your plugin in its init() function:
|
|
|
|
```go
|
|
func init() {
|
|
plugin.Register(New())
|
|
}
|
|
```
|