diff --git a/internal/app/app.go b/internal/app/app.go index becd5ea..4614325 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -24,7 +24,6 @@ import ( "git.nakama.town/fmartingr/butterrobot/internal/plugin/fun" "git.nakama.town/fmartingr/butterrobot/internal/plugin/ping" "git.nakama.town/fmartingr/butterrobot/internal/plugin/reminder" - "git.nakama.town/fmartingr/butterrobot/internal/plugin/searchreplace" "git.nakama.town/fmartingr/butterrobot/internal/plugin/social" "git.nakama.town/fmartingr/butterrobot/internal/queue" ) @@ -91,7 +90,6 @@ func (a *App) Run() error { plugin.Register(social.NewInstagramExpander()) plugin.Register(reminder.New(a.db)) plugin.Register(domainblock.New()) - plugin.Register(searchreplace.New()) // Initialize routes a.initializeRoutes() diff --git a/internal/plugin/searchreplace/README.md b/internal/plugin/searchreplace/README.md deleted file mode 100644 index c7b7786..0000000 --- a/internal/plugin/searchreplace/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Search and Replace Plugin - -This plugin allows users to perform search and replace operations on messages by replying to a message with a search/replace command. - -## Usage - -To use the plugin, reply to any message with a command in the following format: - -``` -s/search/replace/[flags] -``` - -Where: -- `search` is the text you want to find (case-sensitive by default) -- `replace` is the text you want to substitute in place of the search term -- `flags` (optional) control the behavior of the replacement - -### Supported Flags - -- `g` - Global: Replace all occurrences of the search term (without this flag, only the first occurrence is replaced) -- `i` - Case insensitive: Match regardless of case -- `n` - Treat search pattern as a regular expression (advanced users) - -### Examples - -1. Basic replacement (replaces first occurrence): - ``` - s/hello/hi/ - ``` - -2. Global replacement (replaces all occurrences): - ``` - s/hello/hi/g - ``` - -3. Case-insensitive replacement: - ``` - s/Hello/hi/i - ``` - -4. Combined flags (global and case-insensitive): - ``` - s/hello/hi/gi - ``` - -## Limitations - -- The plugin can only access the text content of the original message -- Regular expression support is available with the `n` flag, but should be used carefully as invalid regex patterns will cause errors -- The plugin does not modify the original message; it creates a new message with the replaced text \ No newline at end of file diff --git a/internal/plugin/searchreplace/searchreplace.go b/internal/plugin/searchreplace/searchreplace.go deleted file mode 100644 index 876e880..0000000 --- a/internal/plugin/searchreplace/searchreplace.go +++ /dev/null @@ -1,182 +0,0 @@ -package searchreplace - -import ( - "fmt" - "regexp" - "strings" - - "git.nakama.town/fmartingr/butterrobot/internal/model" - "git.nakama.town/fmartingr/butterrobot/internal/plugin" -) - -// Regex pattern for search and replace operations: s/search/replace/[flags] -var searchReplacePattern = regexp.MustCompile(`^s/([^/]*)/([^/]*)(?:/([gimnsuy]*))?$`) - -// SearchReplacePlugin is a plugin for performing search and replace operations on messages -type SearchReplacePlugin struct { - plugin.BasePlugin -} - -// New creates a new SearchReplacePlugin instance -func New() *SearchReplacePlugin { - return &SearchReplacePlugin{ - BasePlugin: plugin.BasePlugin{ - ID: "util.searchreplace", - Name: "Search and Replace", - Help: "Reply to a message with a search and replace pattern (s/search/replace/[flags]) to create a modified message. " + - "Supported flags: g (global), i (case insensitive)", - }, - } -} - -// OnMessage handles incoming messages -func (p *SearchReplacePlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.MessageAction { - // Only process replies to messages - if msg.ReplyTo == "" { - return nil - } - - // Check if the message matches the search/replace pattern - match := searchReplacePattern.FindStringSubmatch(strings.TrimSpace(msg.Text)) - if match == nil { - return nil - } - - // Get the original message text from the reply_to_message structure in Telegram messages - var originalText string - - // For Telegram messages - if msgData, ok := msg.Raw["message"].(map[string]interface{}); ok { - if replyMsg, ok := msgData["reply_to_message"].(map[string]interface{}); ok { - if text, ok := replyMsg["text"].(string); ok { - originalText = text - } - } - } - - // Generic fallback for other platforms or if the above method fails - if originalText == "" && msg.Raw["original_message"] != nil { - if original, ok := msg.Raw["original_message"].(map[string]interface{}); ok { - if text, ok := original["text"].(string); ok { - originalText = text - } - } - } - - if originalText == "" { - // If we couldn't find the original message text, inform the user - return []*model.MessageAction{ - { - Type: model.ActionSendMessage, - Message: &model.Message{ - Text: "Sorry, I couldn't find the original message text to perform the replacement.", - Chat: msg.Chat, - Channel: msg.Channel, - ReplyTo: msg.ID, - }, - Chat: msg.Chat, - Channel: msg.Channel, - }, - } - } - - // Extract search pattern, replacement and flags - searchPattern := match[1] - replacement := match[2] - flags := "" - if len(match) > 3 { - flags = match[3] - } - - // Process the replacement - result, err := p.performReplacement(originalText, searchPattern, replacement, flags) - if err != nil { - return []*model.MessageAction{ - { - Type: model.ActionSendMessage, - Message: &model.Message{ - Text: fmt.Sprintf("Error performing replacement: %s", err.Error()), - Chat: msg.Chat, - Channel: msg.Channel, - ReplyTo: msg.ID, - }, - Chat: msg.Chat, - Channel: msg.Channel, - }, - } - } - - // Only send a response if the text actually changed - if result == originalText { - return []*model.MessageAction{ - { - Type: model.ActionSendMessage, - Message: &model.Message{ - Text: "No changes were made to the original message.", - Chat: msg.Chat, - Channel: msg.Channel, - ReplyTo: msg.ID, - }, - Chat: msg.Chat, - Channel: msg.Channel, - }, - } - } - - // Create a response with the modified text - return []*model.MessageAction{ - { - Type: model.ActionSendMessage, - Message: &model.Message{ - Text: result, - Chat: msg.Chat, - Channel: msg.Channel, - ReplyTo: msg.ReplyTo, // Reply to the original message - }, - Chat: msg.Chat, - Channel: msg.Channel, - }, - } -} - -// performReplacement performs the search and replace operation on the given text -func (p *SearchReplacePlugin) performReplacement(text, search, replace, flags string) (string, error) { - // Process flags - globalReplace := strings.Contains(flags, "g") - caseInsensitive := strings.Contains(flags, "i") - - // Create the regex pattern - pattern := search - regexFlags := "" - if caseInsensitive { - regexFlags += "(?i)" - } - - // Escape special characters if we're not in a regular expression - if !strings.Contains(flags, "n") { - pattern = regexp.QuoteMeta(pattern) - } - - // Compile the regex - reg, err := regexp.Compile(regexFlags + pattern) - if err != nil { - return "", fmt.Errorf("invalid search pattern: %v", err) - } - - // Perform the replacement - var result string - if globalReplace { - result = reg.ReplaceAllString(text, replace) - } else { - // For non-global replace, only replace the first occurrence - indices := reg.FindStringIndex(text) - if indices == nil { - // No match found - return text, nil - } - - result = text[:indices[0]] + replace + text[indices[1]:] - } - - return result, nil -} diff --git a/internal/plugin/searchreplace/searchreplace_test.go b/internal/plugin/searchreplace/searchreplace_test.go deleted file mode 100644 index 415610c..0000000 --- a/internal/plugin/searchreplace/searchreplace_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package searchreplace - -import ( - "testing" - "time" - - "git.nakama.town/fmartingr/butterrobot/internal/model" -) - -func TestSearchReplace(t *testing.T) { - // Create plugin instance - p := New() - - // Test cases - tests := []struct { - name string - command string - originalText string - expectedResult string - expectActions bool - }{ - { - name: "Simple replacement", - command: "s/hello/world/", - originalText: "hello everyone", - expectedResult: "world everyone", - expectActions: true, - }, - { - name: "Case-insensitive replacement", - command: "s/HELLO/world/i", - originalText: "Hello everyone", - expectedResult: "world everyone", - expectActions: true, - }, - { - name: "Global replacement", - command: "s/a/X/g", - originalText: "banana", - expectedResult: "bXnXnX", - expectActions: true, - }, - { - name: "No change", - command: "s/nothing/something/", - originalText: "test message", - expectedResult: "test message", - expectActions: true, // We send a "no changes" message - }, - { - name: "Not a search/replace command", - command: "hello", - originalText: "test message", - expectedResult: "", - expectActions: false, - }, - { - name: "Invalid pattern", - command: "s/(/)/", - originalText: "test message", - expectedResult: "error", - expectActions: true, // We send an error message - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Create message - msg := &model.Message{ - Text: tc.command, - Chat: "test-chat", - ReplyTo: "original-message-id", - Date: time.Now(), - Channel: &model.Channel{ - Platform: "test", - }, - Raw: map[string]interface{}{ - "message": map[string]interface{}{ - "reply_to_message": map[string]interface{}{ - "text": tc.originalText, - }, - }, - }, - } - - // Process message - actions := p.OnMessage(msg, nil) - - // Check results - if tc.expectActions { - if len(actions) == 0 { - t.Fatalf("Expected actions but got none") - } - - action := actions[0] - if action.Type != model.ActionSendMessage { - t.Fatalf("Expected send message action but got %v", action.Type) - } - - if tc.expectedResult == "error" { - // Just checking that we got an error message - if action.Message == nil || action.Message.Text == "" { - t.Fatalf("Expected error message but got empty message") - } - } else if tc.originalText == tc.expectedResult { - // Check if we got the "no changes" message - if action.Message == nil || action.Message.Text != "No changes were made to the original message." { - t.Fatalf("Expected 'no changes' message but got: %s", action.Message.Text) - } - } else { - // Check actual replacement result - if action.Message == nil || action.Message.Text != tc.expectedResult { - t.Fatalf("Expected result: %s, got: %s", tc.expectedResult, action.Message.Text) - } - } - } else if len(actions) > 0 { - t.Fatalf("Expected no actions but got %d", len(actions)) - } - }) - } -} - -func TestPerformReplacement(t *testing.T) { - p := New() - - // Test cases for the performReplacement function - tests := []struct { - name string - text string - search string - replace string - flags string - expected string - expectErr bool - }{ - { - name: "Simple replacement", - text: "Hello World", - search: "Hello", - replace: "Hi", - flags: "", - expected: "Hi World", - expectErr: false, - }, - { - name: "Case insensitive", - text: "Hello World", - search: "hello", - replace: "Hi", - flags: "i", - expected: "Hi World", - expectErr: false, - }, - { - name: "Global replacement", - text: "one two one two", - search: "one", - replace: "1", - flags: "g", - expected: "1 two 1 two", - expectErr: false, - }, - { - name: "No match", - text: "Hello World", - search: "Goodbye", - replace: "Hi", - flags: "", - expected: "Hello World", - expectErr: false, - }, - { - name: "Invalid regex", - text: "Hello World", - search: "(", - replace: "Hi", - flags: "n", // treat as regex - expected: "", - expectErr: true, - }, - { - name: "Escape special chars by default", - text: "Hello (World)", - search: "(World)", - replace: "[Earth]", - flags: "", - expected: "Hello [Earth]", - expectErr: false, - }, - { - name: "Regex mode with n flag", - text: "Hello (World)", - search: "\\(World\\)", - replace: "[Earth]", - flags: "n", - expected: "Hello [Earth]", - expectErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := p.performReplacement(tc.text, tc.search, tc.replace, tc.flags) - - if tc.expectErr { - if err == nil { - t.Fatalf("Expected error but got none") - } - } else if err != nil { - t.Fatalf("Unexpected error: %v", err) - } else if result != tc.expected { - t.Fatalf("Expected result: %s, got: %s", tc.expected, result) - } - }) - } -} diff --git a/internal/plugin/social/instagram.go b/internal/plugin/social/instagram.go index 0b4ff55..c1dbe1f 100644 --- a/internal/plugin/social/instagram.go +++ b/internal/plugin/social/instagram.go @@ -57,7 +57,7 @@ func (p *InstagramExpander) OnMessage(msg *model.Message, config map[string]inte } // Change the host - parsedURL.Host = "d.ddinstagram.com" + parsedURL.Host = strings.Replace(parsedURL.Host, "instagram.com", "d.ddinstagram.com", 1) // Remove query parameters parsedURL.RawQuery = ""