Browse Source

feat: Add chat storage auto-flush mechanism

- Introduce new configuration option `--chat-flush-interval` to control chat storage cleanup
- Implement periodic chat storage flushing with configurable interval (default 7 days)
- Migrate chat storage from text to CSV format for better data management
- Add thread-safe file handling for chat storage operations
- Update root command to support new chat flush interval flag
pull/271/head
Aldino Kemal 1 year ago
parent
commit
dedd021cca
  1. 4
      src/cmd/root.go
  2. 15
      src/config/settings.go
  3. 46
      src/internal/rest/helpers/flushChatCsv.go
  4. 104
      src/pkg/utils/chat_storage.go
  5. 6
      src/views/components/SendMessage.js

4
src/cmd/root.go

@ -52,6 +52,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&config.WhatsappWebhookSecret, "webhook-secret", "", config.WhatsappWebhookSecret, `secure webhook request --webhook-secret <string> | example: --webhook-secret="super-secret-key"`) rootCmd.PersistentFlags().StringVarP(&config.WhatsappWebhookSecret, "webhook-secret", "", config.WhatsappWebhookSecret, `secure webhook request --webhook-secret <string> | example: --webhook-secret="super-secret-key"`)
rootCmd.PersistentFlags().BoolVarP(&config.WhatsappAccountValidation, "account-validation", "", config.WhatsappAccountValidation, `enable or disable account validation --account-validation <true/false> | example: --account-validation=true`) rootCmd.PersistentFlags().BoolVarP(&config.WhatsappAccountValidation, "account-validation", "", config.WhatsappAccountValidation, `enable or disable account validation --account-validation <true/false> | example: --account-validation=true`)
rootCmd.PersistentFlags().StringVarP(&config.DBURI, "db-uri", "", config.DBURI, `the database uri to store the connection data database uri (by default, we'll use sqlite3 under storages/whatsapp.db). database uri --db-uri <string> | example: --db-uri="file:storages/whatsapp.db?_foreign_keys=off or postgres://user:password@localhost:5432/whatsapp"`) rootCmd.PersistentFlags().StringVarP(&config.DBURI, "db-uri", "", config.DBURI, `the database uri to store the connection data database uri (by default, we'll use sqlite3 under storages/whatsapp.db). database uri --db-uri <string> | example: --db-uri="file:storages/whatsapp.db?_foreign_keys=off or postgres://user:password@localhost:5432/whatsapp"`)
rootCmd.PersistentFlags().IntVarP(&config.AppChatFlushIntervalDays, "chat-flush-interval", "", config.AppChatFlushIntervalDays, `the interval to flush the chat storage --chat-flush-interval <number> | example: --chat-flush-interval=7`)
} }
func runRest(_ *cobra.Command, _ []string) { func runRest(_ *cobra.Command, _ []string) {
@ -148,6 +149,9 @@ func runRest(_ *cobra.Command, _ []string) {
go helpers.SetAutoConnectAfterBooting(appService) go helpers.SetAutoConnectAfterBooting(appService)
// Set auto reconnect checking // Set auto reconnect checking
go helpers.SetAutoReconnectChecking(cli) go helpers.SetAutoReconnectChecking(cli)
// Start auto flush chat csv
go helpers.StartAutoFlushChatStorage()
if err = app.Listen(":" + config.AppPort); err != nil { if err = app.Listen(":" + config.AppPort); err != nil {
log.Fatalln("Failed to start: ", err.Error()) log.Fatalln("Failed to start: ", err.Error())
} }

15
src/config/settings.go

@ -5,18 +5,19 @@ import (
) )
var ( var (
AppVersion = "v5.1.0"
AppPort = "3000"
AppDebug = false
AppOs = "AldinoKemal"
AppPlatform = waCompanionReg.DeviceProps_PlatformType(1)
AppBasicAuthCredential []string
AppVersion = "v5.1.0"
AppPort = "3000"
AppDebug = false
AppOs = "AldinoKemal"
AppPlatform = waCompanionReg.DeviceProps_PlatformType(1)
AppBasicAuthCredential []string
AppChatFlushIntervalDays = 7 // Number of days before flushing chat.csv
PathQrCode = "statics/qrcode" PathQrCode = "statics/qrcode"
PathSendItems = "statics/senditems" PathSendItems = "statics/senditems"
PathMedia = "statics/media" PathMedia = "statics/media"
PathStorages = "storages" PathStorages = "storages"
PathChatStorage = "storages/chat.txt"
PathChatStorage = "storages/chat.csv"
DBURI = "file:storages/whatsapp.db?_foreign_keys=off" DBURI = "file:storages/whatsapp.db?_foreign_keys=off"

46
src/internal/rest/helpers/flushChatCsv.go

@ -0,0 +1,46 @@
package helpers
import (
"os"
"sync"
"time"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/sirupsen/logrus"
)
var flushMutex sync.Mutex
func FlushChatCsv() error {
flushMutex.Lock()
defer flushMutex.Unlock()
// Create an empty file (truncating any existing content)
file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()
return nil
}
// StartAutoFlushChatStorage starts a goroutine that periodically flushes the chat storage
func StartAutoFlushChatStorage() {
interval := time.Duration(config.AppChatFlushIntervalDays) * 24 * time.Hour
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if err := FlushChatCsv(); err != nil {
logrus.Errorf("Error flushing chat storage: %v", err)
} else {
logrus.Info("Successfully flushed chat storage")
}
}
}()
logrus.Infof("Auto flush for chat storage started (your account chat still safe). Will flush every %d days", config.AppChatFlushIntervalDays)
}

104
src/pkg/utils/chat_storage.go

@ -1,12 +1,12 @@
package utils package utils
import ( import (
"encoding/csv"
"fmt" "fmt"
"os" "os"
"strings"
"sync"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" "github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/gofiber/fiber/v2/log"
) )
type RecordedMessage struct { type RecordedMessage struct {
@ -15,30 +15,41 @@ type RecordedMessage struct {
MessageContent string `json:"message_content,omitempty"` MessageContent string `json:"message_content,omitempty"`
} }
// mutex to prevent concurrent file access
var fileMutex sync.Mutex
func FindRecordFromStorage(messageID string) (RecordedMessage, error) { func FindRecordFromStorage(messageID string) (RecordedMessage, error) {
data, err := os.ReadFile(config.PathChatStorage)
fileMutex.Lock()
defer fileMutex.Unlock()
file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil { if err != nil {
return RecordedMessage{}, err
return RecordedMessage{}, fmt.Errorf("failed to open storage file: %w", err)
} }
defer file.Close()
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, ",")
if len(parts) == 3 && parts[0] == messageID {
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return RecordedMessage{}, fmt.Errorf("failed to read CSV records: %w", err)
}
for _, record := range records {
if len(record) == 3 && record[0] == messageID {
return RecordedMessage{ return RecordedMessage{
MessageID: parts[0],
JID: parts[1],
MessageContent: parts[2],
MessageID: record[0],
JID: record[1],
MessageContent: record[2],
}, nil }, nil
} }
} }
return RecordedMessage{}, fmt.Errorf("message ID %s not found in storage", messageID) return RecordedMessage{}, fmt.Errorf("message ID %s not found in storage", messageID)
} }
func RecordMessage(messageID string, senderJID string, messageContent string) {
func RecordMessage(messageID string, senderJID string, messageContent string) error {
fileMutex.Lock()
defer fileMutex.Unlock()
message := RecordedMessage{ message := RecordedMessage{
MessageID: messageID, MessageID: messageID,
JID: senderJID, JID: senderJID,
@ -46,53 +57,40 @@ func RecordMessage(messageID string, senderJID string, messageContent string) {
} }
// Read existing messages // Read existing messages
var messages []RecordedMessage
if data, err := os.ReadFile(config.PathChatStorage); err == nil {
// Split file by newlines and parse each line
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, ",")
var records [][]string
if file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644); err == nil {
defer file.Close()
reader := csv.NewReader(file)
records, err = reader.ReadAll()
if err != nil {
return fmt.Errorf("failed to read existing records: %w", err)
}
msg := RecordedMessage{
MessageID: parts[0],
JID: parts[1],
MessageContent: parts[2],
// Check for duplicates
for _, record := range records {
if len(record) == 3 && record[0] == messageID {
return nil // Skip if duplicate found
} }
messages = append(messages, msg)
} }
} }
// Check for duplicates
for _, msg := range messages {
if msg.MessageID == message.MessageID {
return // Skip if duplicate found
}
}
// Prepare the new record
newRecord := []string{message.MessageID, message.JID, message.MessageContent}
records = append([][]string{newRecord}, records...) // Prepend new message
// Write new message at the top
f, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
// Write all records back to file
file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil { if err != nil {
log.Errorf("Failed to open received-chat.txt: %v", err)
return
return fmt.Errorf("failed to open file for writing: %w", err)
} }
defer f.Close()
defer file.Close()
// Write new message first
csvLine := fmt.Sprintf("%s,%s,%s\n", message.MessageID, message.JID, message.MessageContent)
if _, err := f.WriteString(csvLine); err != nil {
log.Errorf("Failed to write to received-chat.txt: %v", err)
return
}
writer := csv.NewWriter(file)
defer writer.Flush()
// Write existing messages after
for _, msg := range messages {
csvLine := fmt.Sprintf("%s,%s,%s\n", msg.MessageID, msg.JID, msg.MessageContent)
if _, err := f.WriteString(csvLine); err != nil {
log.Errorf("Failed to write to received-chat.txt: %v", err)
return
}
if err := writer.WriteAll(records); err != nil {
return fmt.Errorf("failed to write CSV records: %w", err)
} }
return nil
} }

6
src/views/components/SendMessage.js

@ -37,11 +37,7 @@ export default {
// Validate message is not empty and has reasonable length // Validate message is not empty and has reasonable length
const isMessageValid = this.text.trim().length > 0 && this.text.length <= 4096; const isMessageValid = this.text.trim().length > 0 && this.text.length <= 4096;
// Validate reply_message_id format if provided
const isReplyIdValid = this.reply_message_id === '' ||
/^[A-F0-9]{32}\/[A-F0-9]{20}$/.test(this.reply_message_id);
return isPhoneValid && isMessageValid && isReplyIdValid;
return isPhoneValid && isMessageValid
}, },
async handleSubmit() { async handleSubmit() {
// Add validation check here to prevent submission when form is invalid // Add validation check here to prevent submission when form is invalid

Loading…
Cancel
Save