diff --git a/src/cmd/root.go b/src/cmd/root.go index 23215ab..858a85c 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -52,6 +52,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&config.WhatsappWebhookSecret, "webhook-secret", "", config.WhatsappWebhookSecret, `secure webhook request --webhook-secret | example: --webhook-secret="super-secret-key"`) rootCmd.PersistentFlags().BoolVarP(&config.WhatsappAccountValidation, "account-validation", "", config.WhatsappAccountValidation, `enable or disable account validation --account-validation | 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 | 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 | example: --chat-flush-interval=7`) } func runRest(_ *cobra.Command, _ []string) { @@ -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()) } diff --git a/src/config/settings.go b/src/config/settings.go index f35b027..4de0fd3 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -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" + 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" diff --git a/src/internal/rest/helpers/flushChatCsv.go b/src/internal/rest/helpers/flushChatCsv.go new file mode 100644 index 0000000..5f1c9a3 --- /dev/null +++ b/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) +} diff --git a/src/pkg/utils/chat_storage.go b/src/pkg/utils/chat_storage.go index 9d8ea89..e717e29 100644 --- a/src/pkg/utils/chat_storage.go +++ b/src/pkg/utils/chat_storage.go @@ -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 { @@ -15,30 +15,41 @@ 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, @@ -46,53 +57,40 @@ func RecordMessage(messageID string, senderJID string, messageContent string) { } // 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 } diff --git a/src/views/components/SendMessage.js b/src/views/components/SendMessage.js index 15cfd1d..2fffa25 100644 --- a/src/views/components/SendMessage.js +++ b/src/views/components/SendMessage.js @@ -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