diff --git a/cookie/fetcher.go b/cookie/fetcher.go index b87e41d..d3ea53e 100644 --- a/cookie/fetcher.go +++ b/cookie/fetcher.go @@ -70,6 +70,8 @@ func (s *CookieRefreshService) Start() { s.wg.Add(1) go func() { defer s.wg.Done() + + // Initial fetch log.Println("⏳ Fetching initial cookie...") c, err := s.FetchFreshCookie() if err != nil { @@ -81,6 +83,30 @@ func (s *CookieRefreshService) Start() { s.currentCookie = c s.mu.Unlock() s.readyOnce.Do(func() { close(s.readyCh) }) + log.Println("✅ Initial cookie obtained") + + // Continuous refresh loop + ticker := time.NewTicker(CookieRefreshInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Println("🔄 Auto-refreshing cookie...") + newCookie, err := s.FetchFreshCookie() + if err != nil { + log.Printf("⚠️ Cookie auto-refresh failed: %v", err) + continue + } + s.mu.Lock() + s.currentCookie = newCookie + s.mu.Unlock() + log.Println("✅ Cookie auto-refresh successful") + case <-s.stopCh: + log.Println("Cookie refresh service stopping") + return + } + } }() } @@ -98,6 +124,7 @@ func (s *CookieRefreshService) GetCurrentCookie() string { defer s.mu.RUnlock() return s.currentCookie } + func (s *CookieRefreshService) FetchFreshCookie() (string, error) { if s.debug { log.Println("💡 Stage: Starting FetchFreshCookie") @@ -464,4 +491,4 @@ func abbreviate(s string, n int) string { return s } return s[:n] -} +} \ No newline at end of file diff --git a/main.go b/main.go index 6ec0030..9234e99 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "os" "os/signal" "syscall" - "time" "local/sneedchatbridge/config" "local/sneedchatbridge/cookie" @@ -29,7 +28,7 @@ func main() { log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID) log.Printf("Bridge username: %s", cfg.BridgeUsername) - // Cookie service + // Cookie service (now handles its own refresh loop) cookieSvc, err := cookie.NewCookieRefreshService(cfg.BridgeUsername, cfg.BridgePassword, "kiwifarms.st") if err != nil { log.Fatalf("Failed to create cookie service: %v", err) @@ -53,20 +52,6 @@ func main() { } log.Println("🌉 Discord-Sneedchat Bridge started successfully") - // Background 4h refresh loop - go func() { - t := time.NewTicker(4 * time.Hour) - defer t.Stop() - for range t.C { - log.Println("🔄 Starting automatic cookie refresh") - if _, err := cookieSvc.FetchFreshCookie(); err != nil { - log.Printf("⚠️ Cookie refresh failed: %v", err) - continue - } - log.Println("✅ Cookie refresh completed") - } - }() - // Connect to Sneedchat go func() { if err := sneedClient.Connect(); err != nil { @@ -84,4 +69,4 @@ func main() { sneedClient.Disconnect() cookieSvc.Stop() log.Println("Bridge stopped successfully") -} +} \ No newline at end of file diff --git a/sneed/client.go b/sneed/client.go index f196a82..32028b2 100644 --- a/sneed/client.go +++ b/sneed/client.go @@ -16,7 +16,7 @@ import ( ) const ( - ProcessedCacheSize = 250 + ProcessedCacheSize = 1000 // Increased from 250 ReconnectInterval = 7 * time.Second MappingCacheSize = 1000 MappingCleanupInterval = 5 * time.Minute @@ -218,25 +218,45 @@ func (c *Client) handleDisconnect() { c.OnDisconnect() } - time.Sleep(ReconnectInterval) + // Reconnection loop with exponential backoff + delay := ReconnectInterval + maxDelay := 2 * time.Minute + attempt := 0 - if err := c.Connect(); err != nil { - log.Printf("Reconnect attempt failed: %v", err) - return + for { + select { + case <-c.stopCh: + log.Println("Reconnection cancelled - bridge stopping") + return + case <-time.After(delay): + attempt++ + log.Printf("🔄 Reconnection attempt #%d...", attempt) + + if err := c.Connect(); err != nil { + log.Printf("⚠️ Reconnect attempt #%d failed: %v", attempt, err) + + // Exponential backoff + delay *= 2 + if delay > maxDelay { + delay = maxDelay + } + continue + } + + log.Println("🟢 Reconnected successfully") + + // Allow websocket to stabilize + time.Sleep(2 * time.Second) + + // Re-join room + c.joinRoom() + c.Send("/ping") + log.Printf("📍 Rejoined Sneedchat room %d after reconnect", c.roomID) + return + } } - - log.Println("🟢 Reconnected successfully") - - // 🚦 Allow websocket handshake to stabilize - time.Sleep(2 * time.Second) - - // 🔁 Re-join chat room and verify connection - c.joinRoom() - c.Send("/ping") - log.Printf("🔁 Rejoined Sneedchat room %d after reconnect", c.roomID) } - func (c *Client) Disconnect() { close(c.stopCh) c.mu.Lock() @@ -378,9 +398,18 @@ func (c *Client) isProcessed(id int) bool { func (c *Client) addToProcessed(id int) { c.processedMu.Lock() defer c.processedMu.Unlock() + c.processedMessageIDs = append(c.processedMessageIDs, id) + + // Hard cap: keep only the most recent 1000 messages (FIFO) if len(c.processedMessageIDs) > ProcessedCacheSize { - c.processedMessageIDs = c.processedMessageIDs[1:] + excess := len(c.processedMessageIDs) - ProcessedCacheSize + c.processedMessageIDs = c.processedMessageIDs[excess:] + + // Log when significant eviction happens + if excess > 50 { + log.Printf("⚠️ Processed message cache full, evicted %d old entries", excess) + } } } @@ -407,6 +436,6 @@ func ReplaceBridgeMention(content, bridgeUsername, pingID string) string { if bridgeUsername == "" || pingID == "" { return content } - pat := regexp.MustCompile(fmt.Sprintf(`(?i)@%s(?:\\W|$)`, regexp.QuoteMeta(bridgeUsername))) + pat := regexp.MustCompile(fmt.Sprintf(`(?i)@%s(?:\W|$)`, regexp.QuoteMeta(bridgeUsername))) return pat.ReplaceAllString(content, fmt.Sprintf("<@%s>", pingID)) -} +} \ No newline at end of file diff --git a/utils/bbcode.go b/utils/bbcode.go index 448cb31..2fbccbc 100644 --- a/utils/bbcode.go +++ b/utils/bbcode.go @@ -5,6 +5,27 @@ import ( "strings" ) +// Pre-compiled regex patterns (Go 1.19 compatible) +var ( + imgPattern = regexp.MustCompile(`(?i)\[img\](.*?)\[/img\]`) + videoPattern = regexp.MustCompile(`(?i)\[video\](.*?)\[/video\]`) + urlPattern = regexp.MustCompile(`(?i)\[url=(.*?)\](.*?)\[/url\]`) + urlSimplePattern = regexp.MustCompile(`(?i)\[url\](.*?)\[/url\]`) + boldPattern = regexp.MustCompile(`(?i)\[(?:b|strong)\](.*?)\[/\s*(?:b|strong)\]`) + italicPattern = regexp.MustCompile(`(?i)\[(?:i|em)\](.*?)\[/\s*(?:i|em)\]`) + underlinePattern = regexp.MustCompile(`(?i)\[u\](.*?)\[/\s*u\]`) + strikePattern = regexp.MustCompile(`(?i)\[(?:s|strike)\](.*?)\[/\s*(?:s|strike)\]`) + codePattern = regexp.MustCompile(`(?i)\[code\](.*?)\[/code\]`) + codeBlockPattern = regexp.MustCompile(`(?i)\[(?:php|plain|code=\w+)\](.*?)\[/(?:php|plain|code)\]`) + quotePattern = regexp.MustCompile(`(?i)\[quote\](.*?)\[/quote\]`) + spoilerPattern = regexp.MustCompile(`(?i)\[spoiler\](.*?)\[/spoiler\]`) + colorSizePattern = regexp.MustCompile(`(?i)\[(?:color|size)=.*?\](.*?)\[/\s*(?:color|size)\]`) + listItemPattern = regexp.MustCompile(`(?m)^\[\*\]\s*`) + listTagPattern = regexp.MustCompile(`(?i)\[/?list\]`) + genericTagPattern = regexp.MustCompile(`\[/?[A-Za-z0-9\-=_]+\]`) + httpPattern = regexp.MustCompile(`(?i)^https?://`) +) + func BBCodeToMarkdown(text string) string { if text == "" { return "" @@ -12,10 +33,9 @@ func BBCodeToMarkdown(text string) string { text = strings.ReplaceAll(text, "\r\n", "\n") text = strings.ReplaceAll(text, "\r", "\n") - text = regexp.MustCompile(`(?i)\[img\](.*?)\[/img\]`).ReplaceAllString(text, "$1") - text = regexp.MustCompile(`(?i)\[video\](.*?)\[/video\]`).ReplaceAllString(text, "$1") + text = imgPattern.ReplaceAllString(text, "$1") + text = videoPattern.ReplaceAllString(text, "$1") - urlPattern := regexp.MustCompile(`(?i)\[url=(.*?)\](.*?)\[/url\]`) text = urlPattern.ReplaceAllStringFunc(text, func(match string) string { parts := urlPattern.FindStringSubmatch(match) if len(parts) < 3 { @@ -23,21 +43,20 @@ func BBCodeToMarkdown(text string) string { } link := strings.TrimSpace(parts[1]) txt := strings.TrimSpace(parts[2]) - if regexp.MustCompile(`(?i)^https?://`).MatchString(txt) { + if httpPattern.MatchString(txt) { return txt } return "[" + txt + "](" + link + ")" }) - text = regexp.MustCompile(`(?i)\[url\](.*?)\[/url\]`).ReplaceAllString(text, "$1") - text = regexp.MustCompile(`(?i)\[(?:b|strong)\](.*?)\[/\s*(?:b|strong)\]`).ReplaceAllString(text, "**$1**") - text = regexp.MustCompile(`(?i)\[(?:i|em)\](.*?)\[/\s*(?:i|em)\]`).ReplaceAllString(text, "*$1*") - text = regexp.MustCompile(`(?i)\[u\](.*?)\[/\s*u\]`).ReplaceAllString(text, "__$1__") - text = regexp.MustCompile(`(?i)\[(?:s|strike)\](.*?)\[/\s*(?:s|strike)\]`).ReplaceAllString(text, "~~$1~~") - text = regexp.MustCompile(`(?i)\[code\](.*?)\[/code\]`).ReplaceAllString(text, "`$1`") - text = regexp.MustCompile(`(?i)\[(?:php|plain|code=\w+)\](.*?)\[/(?:php|plain|code)\]`).ReplaceAllString(text, "```$1```") + text = urlSimplePattern.ReplaceAllString(text, "$1") + text = boldPattern.ReplaceAllString(text, "**$1**") + text = italicPattern.ReplaceAllString(text, "*$1*") + text = underlinePattern.ReplaceAllString(text, "__$1__") + text = strikePattern.ReplaceAllString(text, "~~$1~~") + text = codePattern.ReplaceAllString(text, "`$1`") + text = codeBlockPattern.ReplaceAllString(text, "```$1```") - quotePattern := regexp.MustCompile(`(?i)\[quote\](.*?)\[/quote\]`) text = quotePattern.ReplaceAllStringFunc(text, func(match string) string { parts := quotePattern.FindStringSubmatch(match) if len(parts) < 2 { @@ -51,11 +70,11 @@ func BBCodeToMarkdown(text string) string { return strings.Join(lines, "\n") }) - text = regexp.MustCompile(`(?i)\[spoiler\](.*?)\[/spoiler\]`).ReplaceAllString(text, "||$1||") - text = regexp.MustCompile(`(?i)\[(?:color|size)=.*?\](.*?)\[/\s*(?:color|size)\]`).ReplaceAllString(text, "$1") - text = regexp.MustCompile(`(?m)^\[\*\]\s*`).ReplaceAllString(text, "• ") - text = regexp.MustCompile(`(?i)\[/?list\]`).ReplaceAllString(text, "") - text = regexp.MustCompile(`\[/?[A-Za-z0-9\-=_]+\]`).ReplaceAllString(text, "") + text = spoilerPattern.ReplaceAllString(text, "||$1||") + text = colorSizePattern.ReplaceAllString(text, "$1") + text = listItemPattern.ReplaceAllString(text, "• ") + text = listTagPattern.ReplaceAllString(text, "") + text = genericTagPattern.ReplaceAllString(text, "") return strings.TrimSpace(text) -} +} \ No newline at end of file