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_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") + } + }) + } +}