diff --git a/CLAUDE.md b/CLAUDE.md
index 2191848..cb4e70a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,10 +18,12 @@ When creating, modifying, or removing plugins:
## Testing
-Before committing plugin changes:
+**CRITICAL**: After making ANY changes to code files, you MUST run these commands in order:
-1. Check files are properly formatted: Run `make format`
-2. Check code style and linting: Run `make lint`
-3. Test the plugin functionality: Run `make test`
+1. **Format code**: `make format` - Format all code according to project standards
+2. **Lint code**: `make lint` - Check code style and quality (must show "0 issues")
+3. **Run tests**: `make test` - Run all tests to ensure functionality works
4. Verify documentation accuracy
5. Ensure all examples work as described
+
+**These commands are MANDATORY after every code change, no exceptions.**
diff --git a/internal/admin/templates/channel_detail.html b/internal/admin/templates/channel_detail.html
index 9f9a78d..7e12d57 100644
--- a/internal/admin/templates/channel_detail.html
+++ b/internal/admin/templates/channel_detail.html
@@ -32,7 +32,7 @@
Enable All Plugins
-
+
When enabled, all registered plugins will be automatically enabled for this channel. Individual plugin settings will be ignored.
@@ -124,4 +124,4 @@
-{{end}}
\ No newline at end of file
+{{end}}
diff --git a/internal/db/db_test.go b/internal/db/db_test.go
index 88ab470..beb485d 100644
--- a/internal/db/db_test.go
+++ b/internal/db/db_test.go
@@ -200,4 +200,4 @@ func TestEnableAllPlugins(t *testing.T) {
t.Errorf("EnableAllPlugins should be true after update")
}
})
-}
\ No newline at end of file
+}
diff --git a/internal/model/message_test.go b/internal/model/message_test.go
index d94c5ea..d2dfedc 100644
--- a/internal/model/message_test.go
+++ b/internal/model/message_test.go
@@ -231,4 +231,4 @@ func TestChannelName(t *testing.T) {
t.Errorf("Expected channel name to fallback to 'fallback-id', got '%s'", result)
}
})
-}
\ No newline at end of file
+}
diff --git a/internal/platform/telegram/telegram.go b/internal/platform/telegram/telegram.go
index 24714f2..b015793 100644
--- a/internal/platform/telegram/telegram.go
+++ b/internal/platform/telegram/telegram.go
@@ -233,9 +233,17 @@ func (t *TelegramPlatform) SendMessage(msg *model.Message) error {
// Prepare payload
payload := map[string]interface{}{
- "chat_id": chatID,
- "text": msg.Text,
- "parse_mode": "Markdown",
+ "chat_id": chatID,
+ "text": msg.Text,
+ }
+
+ // Set parse_mode based on plugin preference or default to empty string
+ if msg.Raw != nil && msg.Raw["parse_mode"] != nil {
+ // Plugin explicitly set parse_mode
+ payload["parse_mode"] = msg.Raw["parse_mode"]
+ } else {
+ // Default to empty string (no formatting)
+ payload["parse_mode"] = ""
}
// Add reply if needed
diff --git a/internal/plugin/fun/hltb.go b/internal/plugin/fun/hltb.go
index 227d637..f94f2ba 100644
--- a/internal/plugin/fun/hltb.go
+++ b/internal/plugin/fun/hltb.go
@@ -131,12 +131,15 @@ func (p *HLTBPlugin) OnMessage(msg *model.Message, config map[string]interface{}
Channel: msg.Channel,
}
+ // Set parse mode for markdown formatting
+ if responseMsg.Raw == nil {
+ responseMsg.Raw = make(map[string]interface{})
+ }
+ responseMsg.Raw["parse_mode"] = "Markdown"
+
// Add game cover as attachment if available
if game.GameImage != "" {
imageURL := p.getFullImageURL(game.GameImage)
- if responseMsg.Raw == nil {
- responseMsg.Raw = make(map[string]interface{})
- }
responseMsg.Raw["image_url"] = imageURL
}
diff --git a/internal/plugin/help/help.go b/internal/plugin/help/help.go
index 88b25dd..4e6215a 100644
--- a/internal/plugin/help/help.go
+++ b/internal/plugin/help/help.go
@@ -74,6 +74,7 @@ func (p *HelpPlugin) OnMessage(msg *model.Message, config map[string]interface{}
Chat: msg.Chat,
ReplyTo: msg.ID,
Channel: msg.Channel,
+ Raw: map[string]interface{}{"parse_mode": "Markdown"},
}
return []*model.MessageAction{
@@ -151,6 +152,7 @@ func (p *HelpPlugin) OnMessage(msg *model.Message, config map[string]interface{}
Chat: msg.Chat,
ReplyTo: msg.ID,
Channel: msg.Channel,
+ Raw: map[string]interface{}{"parse_mode": "Markdown"},
}
return []*model.MessageAction{
diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go
index e0b68d3..0bfd207 100644
--- a/internal/plugin/plugin_test.go
+++ b/internal/plugin/plugin_test.go
@@ -328,4 +328,4 @@ func TestPluginRegistry(t *testing.T) {
t.Errorf("Expected error when getting plugin after clearing registry, got nil")
}
})
-}
\ No newline at end of file
+}
diff --git a/internal/plugin/social/twitter.go b/internal/plugin/social/twitter.go
index d98eee6..f2c6cc9 100644
--- a/internal/plugin/social/twitter.go
+++ b/internal/plugin/social/twitter.go
@@ -54,17 +54,12 @@ func (p *TwitterExpander) OnMessage(msg *model.Message, config map[string]interf
// 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", replacementDomain, 1)
- link = strings.Replace(link, "x.com", replacementDomain, 1)
return link
}
// Change the host to the configured domain
- if strings.Contains(parsedURL.Host, "twitter.com") {
- parsedURL.Host = strings.Replace(parsedURL.Host, "twitter.com", replacementDomain, 1)
- } else if strings.Contains(parsedURL.Host, "x.com") {
- parsedURL.Host = strings.Replace(parsedURL.Host, "x.com", replacementDomain, 1)
+ if strings.Contains(parsedURL.Host, "twitter.com") || strings.Contains(parsedURL.Host, "x.com") {
+ parsedURL.Host = replacementDomain
}
// Remove query parameters
diff --git a/internal/plugin/social/twitter_test.go b/internal/plugin/social/twitter_test.go
new file mode 100644
index 0000000..c0e1681
--- /dev/null
+++ b/internal/plugin/social/twitter_test.go
@@ -0,0 +1,120 @@
+package social
+
+import (
+ "testing"
+
+ "git.nakama.town/fmartingr/butterrobot/internal/model"
+)
+
+func TestTwitterExpander_OnMessage(t *testing.T) {
+ plugin := NewTwitterExpander()
+
+ tests := []struct {
+ name string
+ input string
+ config map[string]interface{}
+ expected string
+ hasReply bool
+ }{
+ {
+ name: "Twitter URL with default domain",
+ input: "https://twitter.com/user/status/123456789",
+ config: map[string]interface{}{},
+ expected: "https://fxtwitter.com/user/status/123456789",
+ hasReply: true,
+ },
+ {
+ name: "X.com URL with custom domain",
+ input: "https://x.com/elonmusk/status/987654321",
+ config: map[string]interface{}{"domain": "vxtwitter.com"},
+ expected: "https://vxtwitter.com/elonmusk/status/987654321",
+ hasReply: true,
+ },
+ {
+ name: "Twitter URL with tracking parameters",
+ input: "https://twitter.com/openai/status/555?ref_src=twsrc%5Etfw&s=20",
+ config: map[string]interface{}{},
+ expected: "https://fxtwitter.com/openai/status/555",
+ hasReply: true,
+ },
+ {
+ name: "www.twitter.com URL",
+ input: "https://www.twitter.com/user/status/789",
+ config: map[string]interface{}{"domain": "nitter.net"},
+ expected: "https://nitter.net/user/status/789",
+ hasReply: true,
+ },
+ {
+ name: "Mixed text with Twitter URL",
+ input: "Check this out: https://twitter.com/user/status/123 amazing!",
+ config: map[string]interface{}{},
+ expected: "Check this out: https://fxtwitter.com/user/status/123 amazing!",
+ hasReply: true,
+ },
+ {
+ name: "No Twitter URLs",
+ input: "Just some regular text https://youtube.com/watch?v=abc",
+ config: map[string]interface{}{},
+ expected: "",
+ hasReply: false,
+ },
+ {
+ name: "Empty message",
+ input: "",
+ config: map[string]interface{}{},
+ expected: "",
+ hasReply: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ msg := &model.Message{
+ ID: "test_msg",
+ Text: tt.input,
+ Chat: "test_chat",
+ Channel: &model.Channel{
+ ID: 1,
+ Platform: "telegram",
+ PlatformChannelID: "test_chat",
+ },
+ }
+
+ actions := plugin.OnMessage(msg, tt.config, nil)
+
+ if !tt.hasReply {
+ if len(actions) != 0 {
+ t.Errorf("Expected no actions, got %d", len(actions))
+ }
+ return
+ }
+
+ if len(actions) != 1 {
+ t.Errorf("Expected 1 action, got %d", len(actions))
+ return
+ }
+
+ action := actions[0]
+ if action.Type != model.ActionSendMessage {
+ t.Errorf("Expected ActionSendMessage, got %s", action.Type)
+ }
+
+ if action.Message == nil {
+ t.Error("Expected message in action, got nil")
+ return
+ }
+
+ if action.Message.Text != tt.expected {
+ t.Errorf("Expected '%s', got '%s'", tt.expected, action.Message.Text)
+ }
+
+ if action.Message.ReplyTo != msg.ID {
+ t.Errorf("Expected ReplyTo '%s', got '%s'", msg.ID, action.Message.ReplyTo)
+ }
+
+ if action.Message.Raw == nil || action.Message.Raw["parse_mode"] != "" {
+ t.Error("Expected parse_mode to be empty string to disable markdown parsing")
+ }
+ })
+ }
+}