4hr cookie refresh + message_uuid changes

This commit is contained in:
Salastil
2026-02-28 18:37:52 -05:00
parent fba2b0e449
commit 4b455eb58e
5 changed files with 79 additions and 31 deletions

View File

@@ -20,7 +20,7 @@ A high-performance bridge written in Go that synchronizes messages between Kiwi
## Requirements ## Requirements
- **Go 1.23 or higher** - **Go 1.25.6 or higher**
- **Discord Bot Token** with proper permissions - **Discord Bot Token** with proper permissions
- **Discord Webhook URL** - **Discord Webhook URL**
- **Kiwi Farms account** with Sneedchat access - **Kiwi Farms account** with Sneedchat access
@@ -35,7 +35,7 @@ sudo apt update
sudo apt install golang git sudo apt install golang git
# Verify installation # Verify installation
go version # Should show 1.23 or higher go version # Should show 1.25.6 or higher
``` ```
### 2. Clone and Build ### 2. Clone and Build
@@ -271,10 +271,3 @@ If messages aren't appearing:
## License ## License
This bridge is provided as-is. Use responsibly and in accordance with Kiwi Farms and Discord Terms of Service. This bridge is provided as-is. Use responsibly and in accordance with Kiwi Farms and Discord Terms of Service.
## Credits
Built with:
- [discordgo](https://github.com/bwmarrin/discordgo) - Discord API
- [gorilla/websocket](https://github.com/gorilla/websocket) - WebSocket client
- [godotenv](https://github.com/joho/godotenv) - Environment loading

View File

@@ -13,6 +13,7 @@ import (
"slices" "slices"
"strings" "strings"
"sync" "sync"
"time"
) )
// SessionService manages XenForo session cookies as plain strings. // SessionService manages XenForo session cookies as plain strings.
@@ -25,6 +26,7 @@ type SessionService struct {
username string username string
password string password string
tr *http.Transport // shared transport, TLS config applied once tr *http.Transport // shared transport, TLS config applied once
stopCh chan struct{}
} }
// NewSessionService creates a service, performs initial login, and returns. // NewSessionService creates a service, performs initial login, and returns.
@@ -40,6 +42,7 @@ func NewSessionService(ctx context.Context, host, username, password string) (*S
username: username, username: username,
password: password, password: password,
tr: tr, tr: tr,
stopCh: make(chan struct{}),
} }
log.Println("⏳ Logging in to Kiwi Farms...") log.Println("⏳ Logging in to Kiwi Farms...")
@@ -47,9 +50,45 @@ func NewSessionService(ctx context.Context, host, username, password string) (*S
return nil, fmt.Errorf("initial login: %w", err) return nil, fmt.Errorf("initial login: %w", err)
} }
log.Println("✅ Login successful") log.Println("✅ Login successful")
go s.refreshLoop(ctx)
return s, nil return s, nil
} }
// Close stops the background refresh loop. Call at shutdown after
// sneedClient.Disconnect().
func (s *SessionService) Close() {
close(s.stopCh)
}
// refreshLoop proactively renews all session cookies every 4 hours.
// Prevents xf_session and xf_user from expiring mid-run so reconnect
// attempts always have valid credentials. ttrs_clearance is cleared
// so the next WebSocket dial solves it fresh.
func (s *SessionService) refreshLoop(ctx context.Context) {
ticker := time.NewTicker(4 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
log.Println("🔄 Proactive session refresh (4h timer)...")
s.mu.Lock()
s.deleteCookie("ttrs_clearance")
if err := s.login(ctx); err != nil {
log.Printf("⚠️ Proactive session refresh failed: %v", err)
} else {
log.Println("✅ Proactive session refresh complete")
}
s.mu.Unlock()
case <-s.stopCh:
return
case <-ctx.Done():
return
}
}
}
// tlsConfig mirrors sockchat's socketTLSConfig exactly: // tlsConfig mirrors sockchat's socketTLSConfig exactly:
// concatenate secure + insecure cipher suites so KiwiFlare TLS fingerprinting // concatenate secure + insecure cipher suites so KiwiFlare TLS fingerprinting
// doesn't trigger. "The insecure ones appear to be necessary for consistently // doesn't trigger. "The insecure ones appear to be necessary for consistently

View File

@@ -15,16 +15,21 @@ import (
func main() { func main() {
envFile := ".env" envFile := ".env"
debugFlag := false
for i, a := range os.Args { for i, a := range os.Args {
if a == "--env" && i+1 < len(os.Args) { if a == "--env" && i+1 < len(os.Args) {
envFile = os.Args[i+1] envFile = os.Args[i+1]
} }
if a == "--debug" {
debugFlag = true
}
} }
cfg, err := config.Load(envFile) cfg, err := config.Load(envFile)
if err != nil { if err != nil {
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
} }
cfg.Debug = cfg.Debug || debugFlag
log.Printf("Using .env file: %s", envFile) log.Printf("Using .env file: %s", envFile)
log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID) log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID)
log.Printf("Bridge username: %s", cfg.BridgeUsername) log.Printf("Bridge username: %s", cfg.BridgeUsername)
@@ -63,5 +68,6 @@ func main() {
log.Println("Shutdown signal received, cleaning up...") log.Println("Shutdown signal received, cleaning up...")
bridge.Stop() bridge.Stop()
sneedClient.Disconnect() sneedClient.Disconnect()
session.Close()
log.Println("Bridge stopped successfully") log.Println("Bridge stopped successfully")
} }

View File

@@ -54,7 +54,7 @@ type Client struct {
wg sync.WaitGroup wg sync.WaitGroup
processedMu sync.Mutex processedMu sync.Mutex
processedMessageIDs []int processedUUIDs []string
messageEditDates *utils.BoundedMap messageEditDates *utils.BoundedMap
OnMessage func(map[string]interface{}) OnMessage func(map[string]interface{})
@@ -86,7 +86,7 @@ func NewClient(roomID int, session *cookie.SessionService, debug bool) *Client {
TLSClientConfig: tr.TLSClientConfig, TLSClientConfig: tr.TLSClientConfig,
}, },
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
processedMessageIDs: make([]int, 0, ProcessedCacheSize), processedUUIDs: make([]string, 0, ProcessedCacheSize),
messageEditDates: utils.NewBoundedMap(MappingCacheSize, MappingMaxAge), messageEditDates: utils.NewBoundedMap(MappingCacheSize, MappingMaxAge),
lastMessage: time.Now(), lastMessage: time.Now(),
} }
@@ -381,7 +381,6 @@ func (c *Client) handleIncoming(raw string) {
} }
for _, id := range ids { for _, id := range ids {
c.messageEditDates.Delete(id) c.messageEditDates.Delete(id)
c.removeFromProcessed(id)
if c.OnDelete != nil { if c.OnDelete != nil {
c.OnDelete(id) c.OnDelete(id)
} }
@@ -415,11 +414,12 @@ func (c *Client) processMessage(m SneedMessage) {
} }
messageText = html.UnescapeString(messageText) messageText = html.UnescapeString(messageText)
uuid := m.MessageUUID
editDate := m.MessageEditDate editDate := m.MessageEditDate
deleted := m.Deleted || m.IsDeleted deleted := m.Deleted || m.IsDeleted
if deleted { if deleted {
c.messageEditDates.Delete(m.MessageID) c.messageEditDates.Delete(m.MessageID)
c.removeFromProcessed(m.MessageID) c.removeFromProcessed(uuid)
if c.OnDelete != nil { if c.OnDelete != nil {
c.OnDelete(m.MessageID) c.OnDelete(m.MessageID)
} }
@@ -428,7 +428,7 @@ func (c *Client) processMessage(m SneedMessage) {
if (c.bridgeUserID > 0 && userID == c.bridgeUserID) || if (c.bridgeUserID > 0 && userID == c.bridgeUserID) ||
(c.bridgeUsername != "" && username == c.bridgeUsername) { (c.bridgeUsername != "" && username == c.bridgeUsername) {
if m.MessageID > 0 && c.recentOutboundIter != nil && c.mapDiscordSneed != nil { if uuid != "" && c.recentOutboundIter != nil && c.mapDiscordSneed != nil {
now := time.Now() now := time.Now()
for _, entry := range c.recentOutboundIter() { for _, entry := range c.recentOutboundIter() {
if mapped, ok := entry["mapped"].(bool); ok && mapped { if mapped, ok := entry["mapped"].(bool); ok && mapped {
@@ -446,12 +446,12 @@ func (c *Client) processMessage(m SneedMessage) {
} }
} }
} }
c.addToProcessed(m.MessageID) c.addToProcessed(uuid)
c.messageEditDates.Set(m.MessageID, editDate) c.messageEditDates.Set(m.MessageID, editDate)
return return
} }
if c.isProcessed(m.MessageID) { if c.isProcessed(uuid) {
if prev, exists := c.messageEditDates.Get(m.MessageID); exists { if prev, exists := c.messageEditDates.Get(m.MessageID); exists {
if editDate > prev.(int) { if editDate > prev.(int) {
c.messageEditDates.Set(m.MessageID, editDate) c.messageEditDates.Set(m.MessageID, editDate)
@@ -463,7 +463,7 @@ func (c *Client) processMessage(m SneedMessage) {
return return
} }
c.addToProcessed(m.MessageID) c.addToProcessed(uuid)
c.messageEditDates.Set(m.MessageID, editDate) c.messageEditDates.Set(m.MessageID, editDate)
if c.OnMessage != nil { if c.OnMessage != nil {
@@ -477,33 +477,42 @@ func (c *Client) processMessage(m SneedMessage) {
} }
} }
func (c *Client) isProcessed(id int) bool { func (c *Client) isProcessed(uuid string) bool {
if uuid == "" {
return false
}
c.processedMu.Lock() c.processedMu.Lock()
defer c.processedMu.Unlock() defer c.processedMu.Unlock()
for _, x := range c.processedMessageIDs { for _, x := range c.processedUUIDs {
if x == id { if x == uuid {
return true return true
} }
} }
return false return false
} }
func (c *Client) addToProcessed(id int) { func (c *Client) addToProcessed(uuid string) {
if uuid == "" {
return
}
c.processedMu.Lock() c.processedMu.Lock()
defer c.processedMu.Unlock() defer c.processedMu.Unlock()
c.processedMessageIDs = append(c.processedMessageIDs, id) c.processedUUIDs = append(c.processedUUIDs, uuid)
if len(c.processedMessageIDs) > ProcessedCacheSize { if len(c.processedUUIDs) > ProcessedCacheSize {
excess := len(c.processedMessageIDs) - ProcessedCacheSize excess := len(c.processedUUIDs) - ProcessedCacheSize
c.processedMessageIDs = c.processedMessageIDs[excess:] c.processedUUIDs = c.processedUUIDs[excess:]
} }
} }
func (c *Client) removeFromProcessed(id int) { func (c *Client) removeFromProcessed(uuid string) {
if uuid == "" {
return
}
c.processedMu.Lock() c.processedMu.Lock()
defer c.processedMu.Unlock() defer c.processedMu.Unlock()
for i, x := range c.processedMessageIDs { for i, x := range c.processedUUIDs {
if x == id { if x == uuid {
c.processedMessageIDs = append(c.processedMessageIDs[:i], c.processedMessageIDs[i+1:]...) c.processedUUIDs = append(c.processedUUIDs[:i], c.processedUUIDs[i+1:]...)
return return
} }
} }

View File

@@ -2,6 +2,7 @@ package sneed
type SneedMessage struct { type SneedMessage struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
MessageUUID string `json:"message_uuid"`
Message string `json:"message"` Message string `json:"message"`
MessageRaw string `json:"message_raw"` MessageRaw string `json:"message_raw"`
MessageEditDate int `json:"message_edit_date"` MessageEditDate int `json:"message_edit_date"`