4hr cookie refresh + message_uuid changes
This commit is contained in:
11
README.md
11
README.md
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
6
main.go
6
main.go
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user