Skip to content

Commit

Permalink
feat: Add chat storage auto-flush mechanism
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
aldinokemal committed Feb 7, 2025
1 parent 0947c67 commit dedd021
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 65 deletions.
4 changes: 4 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().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().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) {
Expand Down Expand Up @@ -148,6 +149,9 @@ func runRest(_ *cobra.Command, _ []string) {
go helpers.SetAutoConnectAfterBooting(appService)
// Set auto reconnect checking
go helpers.SetAutoReconnectChecking(cli)
// Start auto flush chat csv
go helpers.StartAutoFlushChatStorage()

if err = app.Listen(":" + config.AppPort); err != nil {
log.Fatalln("Failed to start: ", err.Error())
}
Expand Down
15 changes: 8 additions & 7 deletions src/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import (
)

var (
AppVersion = "v5.1.0"
AppPort = "3000"
AppDebug = false
AppOs = "AldinoKemal"
AppPlatform = waCompanionReg.DeviceProps_PlatformType(1)
AppBasicAuthCredential []string
AppVersion = "v5.1.0"

This comment has been minimized.

Copy link
@ilanbenb

ilanbenb Feb 10, 2025

The new version is 5.1.1

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"
PathSendItems = "statics/senditems"
PathMedia = "statics/media"
PathStorages = "storages"
PathChatStorage = "storages/chat.txt"
PathChatStorage = "storages/chat.csv"

DBURI = "file:storages/whatsapp.db?_foreign_keys=off"

Expand Down
46 changes: 46 additions & 0 deletions src/internal/rest/helpers/flushChatCsv.go
Original file line number Diff line number Diff line change
@@ -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 changes: 51 additions & 53 deletions src/pkg/utils/chat_storage.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package utils

import (
"encoding/csv"
"fmt"
"os"
"strings"
"sync"

"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/gofiber/fiber/v2/log"
)

type RecordedMessage struct {
Expand All @@ -15,84 +15,82 @@ type RecordedMessage struct {
MessageContent string `json:"message_content,omitempty"`
}

// mutex to prevent concurrent file access
var fileMutex sync.Mutex

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 {
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{
MessageID: parts[0],
JID: parts[1],
MessageContent: parts[2],
MessageID: record[0],
JID: record[1],
MessageContent: record[2],
}, nil
}
}
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{
MessageID: messageID,
JID: senderJID,
MessageContent: messageContent,
}

// 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 {
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 changes: 1 addition & 5 deletions src/views/components/SendMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,8 @@ export default {

// Validate message is not empty and has reasonable length
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() {
// Add validation check here to prevent submission when form is invalid
Expand Down

0 comments on commit dedd021

Please sign in to comment.