feat: ratelimit, ghcr package support, media path sensitization, LID to CN migration (#6)
* feat: ratelimit, ghcr package and proper project refactor from askarzh * fix(bridge): validate media path * update: dependencies * fix: added LID to contact number migration
This commit is contained in:
+442
-71
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@@ -18,10 +19,13 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"whatsapp-bridge/auth"
|
||||
"whatsapp-bridge/config"
|
||||
bridgelogger "whatsapp-bridge/logger"
|
||||
"whatsapp-bridge/wastate"
|
||||
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
"go.mau.fi/whatsmeow/socket"
|
||||
@@ -29,6 +33,7 @@ import (
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/mdp/qrterminal"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
|
||||
"bytes"
|
||||
|
||||
@@ -123,6 +128,44 @@ func openDatabase(dbName string) (*sql.DB, error) {
|
||||
return sql.Open("sqlite3", "file:store/messages.db?_foreign_keys=on")
|
||||
}
|
||||
|
||||
// validateMediaPath sanitize media path
|
||||
func validateMediaPath(mediaPath string) (string, error) {
|
||||
if mediaPath == "" {
|
||||
return "", fmt.Errorf("empty media path")
|
||||
}
|
||||
|
||||
// Allowed media directory
|
||||
baseDir := "./media"
|
||||
|
||||
absBaseDir, err := filepath.Abs(baseDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Reject absolute paths
|
||||
if filepath.IsAbs(mediaPath) {
|
||||
return "", fmt.Errorf("absolute paths are not allowed")
|
||||
}
|
||||
|
||||
// Clean traversal sequences
|
||||
cleanPath := filepath.Clean(mediaPath)
|
||||
|
||||
fullPath := filepath.Join(absBaseDir, cleanPath)
|
||||
|
||||
absPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ensure resolved path stays inside media directory
|
||||
if !strings.HasPrefix(absPath, absBaseDir+string(os.PathSeparator)) &&
|
||||
absPath != absBaseDir {
|
||||
return "", fmt.Errorf("path traversal detected")
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
// NewMessageStore Initialize message store
|
||||
func NewMessageStore() (*MessageStore, error) {
|
||||
if err := os.MkdirAll("store", 0755); err != nil {
|
||||
@@ -181,6 +224,231 @@ func (store *MessageStore) Close() error {
|
||||
return store.db.Close()
|
||||
}
|
||||
|
||||
// normalizeUserJID converts a LID JID (xxxx@lid) into a phone-number JID (xxxx@s.whatsapp.net)
|
||||
// using the whatsmeow LID mapping store. Non-LID JIDs are returned unchanged.
|
||||
func normalizeUserJID(client *whatsmeow.Client, jid types.JID) types.JID {
|
||||
if client == nil || client.Store == nil || client.Store.LIDs == nil {
|
||||
return jid
|
||||
}
|
||||
|
||||
// Only normalize hidden-user server (@lid)
|
||||
if jid.Server != types.HiddenUserServer {
|
||||
return jid
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pn, err := client.Store.LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil || pn.IsEmpty() {
|
||||
return jid
|
||||
}
|
||||
|
||||
return pn
|
||||
}
|
||||
|
||||
// migrateLIDChatsToPhoneJIDs merges chats stored under @lid
|
||||
// into their corresponding @s.whatsapp.net chats.
|
||||
//
|
||||
// This is idempotent and safe to run on every startup.
|
||||
//
|
||||
// Migration order:
|
||||
//
|
||||
// 1. Create/upsert PN chat
|
||||
// 2. Move messages to PN chat
|
||||
// 3. Delete leftover duplicate messages
|
||||
// 4. Delete old LID chat
|
||||
func migrateLIDChatsToPhoneJIDs(
|
||||
client *whatsmeow.Client,
|
||||
store *MessageStore,
|
||||
logger waLog.Logger,
|
||||
isPostgresDB bool,
|
||||
) {
|
||||
if client == nil || store == nil || store.db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
db := store.db
|
||||
|
||||
var query string
|
||||
arg := "%@" + types.HiddenUserServer
|
||||
|
||||
if isPostgresDB {
|
||||
query = `
|
||||
SELECT jid, name, last_message_time
|
||||
FROM chats
|
||||
WHERE jid LIKE $1
|
||||
`
|
||||
} else {
|
||||
query = `
|
||||
SELECT jid, name, last_message_time
|
||||
FROM chats
|
||||
WHERE jid LIKE ?
|
||||
`
|
||||
}
|
||||
|
||||
rows, err := db.Query(query, arg)
|
||||
if err != nil {
|
||||
logger.Errorf("LID migration: failed listing chats: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type lidChat struct {
|
||||
JID string
|
||||
Name string
|
||||
LastMessageTime time.Time
|
||||
}
|
||||
|
||||
var chats []lidChat
|
||||
|
||||
for rows.Next() {
|
||||
var c lidChat
|
||||
|
||||
if err := rows.Scan(&c.JID, &c.Name, &c.LastMessageTime); err != nil {
|
||||
logger.Warnf("LID migration: scan failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
chats = append(chats, c)
|
||||
}
|
||||
|
||||
if len(chats) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("LID migration: found %d @lid chats", len(chats))
|
||||
|
||||
merged := 0
|
||||
skipped := 0
|
||||
|
||||
for _, c := range chats {
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
logger.Warnf("LID migration: tx begin failed for %s: %v", c.JID, err)
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
commit := false
|
||||
defer func() {
|
||||
if !commit {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
lidJID, parseErr := types.ParseJID(c.JID)
|
||||
if parseErr != nil {
|
||||
logger.Warnf("LID migration: invalid jid %s: %v", c.JID, parseErr)
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
pnJID := normalizeUserJID(client, lidJID)
|
||||
|
||||
if pnJID.Server != types.DefaultUserServer {
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
pnStr := pnJID.String()
|
||||
|
||||
var upsertQuery string
|
||||
|
||||
if isPostgresDB {
|
||||
upsertQuery = `
|
||||
INSERT INTO chats (jid, name, last_message_time)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (jid)
|
||||
DO UPDATE SET
|
||||
name = COALESCE(NULLIF(chats.name, ''), EXCLUDED.name),
|
||||
last_message_time = GREATEST(
|
||||
chats.last_message_time,
|
||||
EXCLUDED.last_message_time
|
||||
)
|
||||
`
|
||||
} else {
|
||||
upsertQuery = `
|
||||
INSERT INTO chats (jid, name, last_message_time)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(jid)
|
||||
DO UPDATE SET
|
||||
name = COALESCE(NULLIF(chats.name, ''), excluded.name),
|
||||
last_message_time = MAX(
|
||||
chats.last_message_time,
|
||||
excluded.last_message_time
|
||||
)
|
||||
`
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(upsertQuery, pnStr, c.Name, c.LastMessageTime); err != nil {
|
||||
logger.Warnf("LID migration: upsert failed %s -> %s: %v", c.JID, pnStr, err)
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
var moveMessagesQuery string
|
||||
|
||||
if isPostgresDB {
|
||||
moveMessagesQuery = `
|
||||
UPDATE messages
|
||||
SET chat_jid = $1
|
||||
WHERE chat_jid = $2
|
||||
`
|
||||
} else {
|
||||
moveMessagesQuery = `
|
||||
UPDATE messages
|
||||
SET chat_jid = ?
|
||||
WHERE chat_jid = ?
|
||||
`
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(moveMessagesQuery, pnStr, c.JID); err != nil {
|
||||
logger.Warnf("LID migration: move messages failed %s -> %s: %v", c.JID, pnStr, err)
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
var deleteChatQuery string
|
||||
|
||||
if isPostgresDB {
|
||||
deleteChatQuery = `DELETE FROM chats WHERE jid = $1`
|
||||
} else {
|
||||
deleteChatQuery = `DELETE FROM chats WHERE jid = ?`
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(deleteChatQuery, c.JID); err != nil {
|
||||
logger.Warnf("LID migration: delete old chat failed %s: %v", c.JID, err)
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
logger.Warnf("LID migration: commit failed %s: %v", c.JID, err)
|
||||
_ = tx.Rollback()
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
commit = true
|
||||
|
||||
logger.Infof("LID migration: merged %s -> %s", c.JID, pnStr)
|
||||
merged++
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
"LID migration complete: %d merged, %d skipped",
|
||||
merged,
|
||||
skipped,
|
||||
)
|
||||
}
|
||||
|
||||
// StoreChat Store a chat in the database
|
||||
func (store *MessageStore) StoreChat(jid, name string, lastMessageTime time.Time) error {
|
||||
if isPostgres {
|
||||
@@ -394,7 +662,11 @@ func sendWhatsAppMessage(client *whatsmeow.Client, recipient string, message str
|
||||
msg := &waE2E.Message{}
|
||||
|
||||
if mediaPath != "" {
|
||||
mediaData, err := os.ReadFile(mediaPath)
|
||||
validatedPath, err := validateMediaPath(mediaPath)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Invalid media path: %v", err)
|
||||
}
|
||||
mediaData, err := os.ReadFile(validatedPath)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Error reading media file: %v", err)
|
||||
}
|
||||
@@ -441,7 +713,7 @@ func sendWhatsAppMessage(client *whatsmeow.Client, recipient string, message str
|
||||
return false, fmt.Sprintf("Error uploading media: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Media uploaded", resp)
|
||||
slog.Info("media uploaded", "response", resp)
|
||||
|
||||
switch mediaType {
|
||||
case whatsmeow.MediaImage:
|
||||
@@ -468,7 +740,7 @@ func sendWhatsAppMessage(client *whatsmeow.Client, recipient string, message str
|
||||
return false, fmt.Sprintf("Failed to analyze Ogg Opus file: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Not an Ogg Opus file: %s\n", mimeType)
|
||||
slog.Warn("not an Ogg Opus file", "mime_type", mimeType)
|
||||
}
|
||||
|
||||
msg.AudioMessage = &waE2E.AudioMessage{
|
||||
@@ -603,8 +875,8 @@ func handleMessage(client *whatsmeow.Client, messageStore *MessageStore, msg *ev
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}()
|
||||
chatJID := msg.Info.Chat.String()
|
||||
sender := msg.Info.Sender.User
|
||||
chatJID := normalizeUserJID(client, msg.Info.Chat).String()
|
||||
sender := normalizeUserJID(client, msg.Info.Sender).User
|
||||
|
||||
name := GetChatName(client, messageStore, msg.Info.Chat, chatJID, nil, sender, logger)
|
||||
|
||||
@@ -647,9 +919,9 @@ func handleMessage(client *whatsmeow.Client, messageStore *MessageStore, msg *ev
|
||||
}
|
||||
|
||||
if mediaType != "" {
|
||||
fmt.Printf("[%s] %s %s: [%s: %s] %s\n", timestamp, direction, sender, mediaType, filename, content)
|
||||
slog.Info("message", "ts", timestamp, "direction", direction, "sender", sender, "media_type", mediaType, "filename", filename, "content", content)
|
||||
} else if content != "" {
|
||||
fmt.Printf("[%s] %s %s: %s\n", timestamp, direction, sender, content)
|
||||
slog.Info("message", "ts", timestamp, "direction", direction, "sender", sender, "content", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,7 +1086,7 @@ func downloadMedia(client *whatsmeow.Client, messageStore *MessageStore, message
|
||||
return false, "", "", "", fmt.Errorf("incomplete media information for download")
|
||||
}
|
||||
|
||||
fmt.Printf("Attempting to download media for message %s in chat %s...\n", messageID, chatJID)
|
||||
slog.Info("attempting to download media", "message_id", messageID, "chat_jid", chatJID)
|
||||
|
||||
directPath := extractDirectPathFromURL(url)
|
||||
|
||||
@@ -851,7 +1123,7 @@ func downloadMedia(client *whatsmeow.Client, messageStore *MessageStore, message
|
||||
return false, "", "", "", fmt.Errorf("failed to save media file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully downloaded %s media to %s (%d bytes)\n", mediaType, absPath, len(mediaData))
|
||||
slog.Info("successfully downloaded media", "media_type", mediaType, "path", absPath, "bytes", len(mediaData))
|
||||
return true, mediaType, filename, absPath, nil
|
||||
}
|
||||
|
||||
@@ -872,7 +1144,7 @@ func extractDirectPathFromURL(url string) string {
|
||||
}
|
||||
|
||||
// Start a REST API server to expose the WhatsApp client functionality
|
||||
func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port int, cfg *config.Config) {
|
||||
func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, cfg *config.Config, state *wastate.State) {
|
||||
apiMux := http.NewServeMux()
|
||||
|
||||
// Send message
|
||||
@@ -898,10 +1170,10 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Received request to send message", req.Message, req.MediaPath)
|
||||
slog.Info("received request to send message", "message", req.Message, "media_path", req.MediaPath)
|
||||
|
||||
success, message := sendWhatsAppMessage(client, req.Recipient, req.Message, req.MediaPath)
|
||||
fmt.Println("Message sent", success, message)
|
||||
slog.Info("message sent", "success", success, "message", message)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if !success {
|
||||
@@ -1213,17 +1485,46 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
})
|
||||
|
||||
apiMux.HandleFunc("/auth/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"connected": state.Connected(),
|
||||
"logged_in": state.LoggedIn(),
|
||||
"pairing_required": state.PairingRequired(),
|
||||
"wa_version": state.WAVersion(),
|
||||
})
|
||||
})
|
||||
|
||||
apiMux.HandleFunc("/auth/pairing-qr", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
png := state.PairingQRPNG()
|
||||
if png == nil {
|
||||
http.Error(w, "no pairing QR available; client is logged in or has not started pairing yet", http.StatusGone)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(png)
|
||||
})
|
||||
|
||||
// Authentication
|
||||
protected := auth.JwtAuthMiddleware(cfg, apiMux)
|
||||
http.Handle("/api/", http.StripPrefix("/api", protected))
|
||||
http.Handle("/auth/login", auth.LoginHandler(cfg))
|
||||
|
||||
serverAddr := fmt.Sprintf(":%d", port)
|
||||
fmt.Printf("Starting REST API server on %s...\n", serverAddr)
|
||||
serverAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
slog.Info("starting REST API server", "addr", serverAddr)
|
||||
|
||||
go func() {
|
||||
if err := http.ListenAndServe(serverAddr, nil); err != nil {
|
||||
fmt.Printf("REST API server error: %v\n", err)
|
||||
slog.Error("rest api server error", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1308,7 +1609,7 @@ func GetChatName(client *whatsmeow.Client, messageStore *MessageStore, jid types
|
||||
|
||||
// Handle history sync events
|
||||
func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, historySync *events.HistorySync, logger waLog.Logger) {
|
||||
fmt.Printf("Received history sync event with %d conversations\n", len(historySync.Data.Conversations))
|
||||
slog.Info("received history sync event", "conversations", len(historySync.Data.Conversations))
|
||||
|
||||
syncedCount := 0
|
||||
for _, conversation := range historySync.Data.Conversations {
|
||||
@@ -1323,6 +1624,8 @@ func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, his
|
||||
logger.Warnf("Failed to parse JID %s: %v", chatJID, err)
|
||||
continue
|
||||
}
|
||||
jid = normalizeUserJID(client, jid)
|
||||
chatJID = jid.String()
|
||||
|
||||
name := GetChatName(client, messageStore, jid, chatJID, conversation, "", logger)
|
||||
|
||||
@@ -1377,7 +1680,11 @@ func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, his
|
||||
isFromMe = *msg.Message.Key.FromMe
|
||||
}
|
||||
if !isFromMe && msg.Message.Key.Participant != nil && *msg.Message.Key.Participant != "" {
|
||||
sender = *msg.Message.Key.Participant
|
||||
if pJid, err := types.ParseJID(*msg.Message.Key.Participant); err == nil {
|
||||
sender = normalizeUserJID(client, pJid).User
|
||||
} else {
|
||||
sender = *msg.Message.Key.Participant
|
||||
}
|
||||
} else if isFromMe {
|
||||
sender = client.Store.ID.User
|
||||
} else {
|
||||
@@ -1430,29 +1737,29 @@ func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, his
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("History sync complete. Stored %d messages.\n", syncedCount)
|
||||
slog.Info("history sync complete", "stored_messages", syncedCount)
|
||||
}
|
||||
|
||||
// Request history sync from the server
|
||||
func requestHistorySync(client *whatsmeow.Client) {
|
||||
if client == nil {
|
||||
fmt.Println("Client is not initialized. Cannot request history sync.")
|
||||
slog.Error("client is not initialized, cannot request history sync")
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsConnected() {
|
||||
fmt.Println("Client is not connected. Please ensure you are connected to WhatsApp first.")
|
||||
slog.Warn("client is not connected to whatsapp")
|
||||
return
|
||||
}
|
||||
|
||||
if client.Store.ID == nil {
|
||||
fmt.Println("Client is not logged in. Please scan the QR code first.")
|
||||
slog.Warn("client is not logged in, please scan the qr code")
|
||||
return
|
||||
}
|
||||
|
||||
historyMsg := client.BuildHistorySyncRequest(nil, 100)
|
||||
if historyMsg == nil {
|
||||
fmt.Println("Failed to build history sync request.")
|
||||
slog.Error("failed to build history sync request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1462,9 +1769,9 @@ func requestHistorySync(client *whatsmeow.Client) {
|
||||
}, historyMsg)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to request history sync: %v\n", err)
|
||||
slog.Error("failed to request history sync", "err", err)
|
||||
} else {
|
||||
fmt.Println("History sync requested. Waiting for server response...")
|
||||
slog.Info("history sync requested, waiting for server response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1515,7 +1822,7 @@ func analyzeOggOpus(data []byte) (duration uint32, waveform []byte, err error) {
|
||||
preSkip = binary.LittleEndian.Uint16(pageData[headPos+10 : headPos+12])
|
||||
sampleRate = binary.LittleEndian.Uint32(pageData[headPos+12 : headPos+16])
|
||||
foundOpusHead = true
|
||||
fmt.Printf("Found OpusHead: sampleRate=%d, preSkip=%d\n", sampleRate, preSkip)
|
||||
slog.Info("found OpusHead", "sample_rate", sampleRate, "pre_skip", preSkip)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1528,16 +1835,15 @@ func analyzeOggOpus(data []byte) (duration uint32, waveform []byte, err error) {
|
||||
}
|
||||
|
||||
if !foundOpusHead {
|
||||
fmt.Println("Warning: OpusHead not found, using default values")
|
||||
slog.Warn("opushead not found, using default values")
|
||||
}
|
||||
|
||||
if lastGranule > 0 {
|
||||
durationSeconds := float64(lastGranule-uint64(preSkip)) / float64(sampleRate)
|
||||
duration = uint32(math.Ceil(durationSeconds))
|
||||
fmt.Printf("Calculated Opus duration from granule: %f seconds (lastGranule=%d)\n",
|
||||
durationSeconds, lastGranule)
|
||||
slog.Info("calculated Opus duration from granule", "duration_seconds", durationSeconds, "last_granule", lastGranule)
|
||||
} else {
|
||||
fmt.Println("Warning: No valid granule position found, using estimation")
|
||||
slog.Warn("no valid granule position found, using estimation")
|
||||
durationEstimate := float64(len(data)) / 2000.0
|
||||
duration = uint32(durationEstimate)
|
||||
}
|
||||
@@ -1550,8 +1856,7 @@ func analyzeOggOpus(data []byte) (duration uint32, waveform []byte, err error) {
|
||||
|
||||
waveform = placeholderWaveform(duration)
|
||||
|
||||
fmt.Printf("Ogg Opus analysis: size=%d bytes, calculated duration=%d sec, waveform=%d bytes\n",
|
||||
len(data), duration, len(waveform))
|
||||
slog.Info("ogg opus analysis complete", "size_bytes", len(data), "duration_sec", duration, "waveform_bytes", len(waveform))
|
||||
|
||||
return duration, waveform, nil
|
||||
}
|
||||
@@ -2381,8 +2686,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
slog.SetDefault(bridgelogger.New(os.Getenv("LOG_LEVEL")))
|
||||
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to load config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2412,11 +2720,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
state := wastate.New()
|
||||
|
||||
version, err := CustomGetLatestVersion(context.Background(), nil)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to retrieve current WhatsApp Web client Version")
|
||||
} else {
|
||||
store.SetWAVersion(*version)
|
||||
state.SetWAVersion(fmt.Sprintf("%d.%d.%d", version[0], version[1], version[2]))
|
||||
logger.Infof("WhatsApp Web Client Version: %d.%d.%d\n", version[0], version[1], version[2])
|
||||
}
|
||||
client := whatsmeow.NewClient(deviceStore, logger)
|
||||
@@ -2435,6 +2746,12 @@ func main() {
|
||||
}
|
||||
defer messageStore.Close()
|
||||
|
||||
state.SetLoggedIn(client.Store.ID != nil) // existing session means already logged in
|
||||
|
||||
const maxOutdatedRetries = 3
|
||||
var outdatedRetries int
|
||||
var outdatedRetriesMu sync.Mutex
|
||||
|
||||
client.AddEventHandler(func(evt interface{}) {
|
||||
switch v := evt.(type) {
|
||||
case *events.Message:
|
||||
@@ -2445,58 +2762,112 @@ func main() {
|
||||
|
||||
case *events.Connected:
|
||||
logger.Infof("Connected to WhatsApp")
|
||||
state.SetConnected(true)
|
||||
state.SetLoggedIn(true)
|
||||
state.ClearPairingQR()
|
||||
outdatedRetriesMu.Lock()
|
||||
outdatedRetries = 0
|
||||
outdatedRetriesMu.Unlock()
|
||||
|
||||
case *events.Disconnected:
|
||||
logger.Warnf("Disconnected from WhatsApp")
|
||||
state.SetConnected(false)
|
||||
|
||||
case *events.LoggedOut:
|
||||
logger.Warnf("Device logged out, please scan QR code to log in again")
|
||||
state.SetLoggedIn(false)
|
||||
state.SetConnected(false)
|
||||
|
||||
case *events.ClientOutdated:
|
||||
outdatedRetriesMu.Lock()
|
||||
outdatedRetries++
|
||||
n := outdatedRetries
|
||||
outdatedRetriesMu.Unlock()
|
||||
state.SetConnected(false)
|
||||
if n > maxOutdatedRetries {
|
||||
slog.Error("client outdated: exceeded retry budget; whatsmeow library likely needs a real upgrade",
|
||||
"retries", n, "max", maxOutdatedRetries)
|
||||
return
|
||||
}
|
||||
slog.Warn("client outdated (405); refreshing wa version and reconnecting",
|
||||
"attempt", n, "max", maxOutdatedRetries)
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
newVersion, err := CustomGetLatestVersion(context.Background(), nil)
|
||||
if err != nil {
|
||||
slog.Error("failed to refresh wa version", "err", err)
|
||||
return
|
||||
}
|
||||
store.SetWAVersion(*newVersion)
|
||||
state.SetWAVersion(fmt.Sprintf("%d.%d.%d", newVersion[0], newVersion[1], newVersion[2]))
|
||||
slog.Info("applied refreshed wa version, attempting reconnect", "version", state.WAVersion())
|
||||
if err := client.Connect(); err != nil {
|
||||
slog.Error("reconnect after wa version refresh failed", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
|
||||
connected := make(chan bool, 1)
|
||||
migrateLIDChatsToPhoneJIDs(client, messageStore, logger, cfg.DB.IsPostgres)
|
||||
|
||||
if client.Store.ID == nil {
|
||||
qrChan, _ := client.GetQRChannel(context.Background())
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
// REST server comes up first so /api/auth/status and /api/auth/pairing-qr
|
||||
// are reachable during pairing. WhatsApp connect runs concurrently below.
|
||||
startRESTServer(client, messageStore, cfg, state)
|
||||
|
||||
for evt := range qrChan {
|
||||
if evt.Event == "code" {
|
||||
fmt.Println("\nScan this QR code with your WhatsApp app:")
|
||||
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||
} else if evt.Event == "success" {
|
||||
connected <- true
|
||||
break
|
||||
// Periodically refresh the WhatsApp Web client version so reconnects
|
||||
// after transient drops use a current version string. Only the next
|
||||
// connection picks up the refreshed value; the active session is unaffected.
|
||||
go func() {
|
||||
t := time.NewTicker(6 * time.Hour)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
v, err := CustomGetLatestVersion(context.Background(), nil)
|
||||
if err != nil {
|
||||
slog.Warn("periodic wa version refresh failed", "err", err)
|
||||
continue
|
||||
}
|
||||
store.SetWAVersion(*v)
|
||||
next := fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2])
|
||||
if next != state.WAVersion() {
|
||||
slog.Info("wa version updated by periodic refresh", "from", state.WAVersion(), "to", next)
|
||||
state.SetWAVersion(next)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-connected:
|
||||
fmt.Println("\nSuccessfully connected and authenticated!")
|
||||
case <-time.After(3 * time.Minute):
|
||||
logger.Errorf("Timeout waiting for QR code scan")
|
||||
return
|
||||
// Pair / connect to WhatsApp in a goroutine so main can block on signals.
|
||||
go func() {
|
||||
if client.Store.ID == nil {
|
||||
qrChan, _ := client.GetQRChannel(context.Background())
|
||||
if err := client.Connect(); err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
for evt := range qrChan {
|
||||
switch evt.Event {
|
||||
case "code":
|
||||
fmt.Println("\nScan this QR code with your WhatsApp app:")
|
||||
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||
if png, err := qrcode.Encode(evt.Code, qrcode.Medium, 256); err == nil {
|
||||
state.SetPairingQRPNG(png)
|
||||
} else {
|
||||
slog.Warn("failed to encode pairing qr as png", "err", err)
|
||||
}
|
||||
case "success":
|
||||
fmt.Println("\nSuccessfully connected and authenticated!")
|
||||
return
|
||||
case "timeout":
|
||||
logger.Errorf("Pairing QR timeout")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := client.Connect(); err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
connected <- true
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if !client.IsConnected() {
|
||||
logger.Errorf("Failed to establish stable connection")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Connected to WhatsApp! Type 'help' for commands.")
|
||||
|
||||
startRESTServer(client, messageStore, 8080, cfg)
|
||||
}()
|
||||
|
||||
exitChan := make(chan os.Signal, 1)
|
||||
signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
Reference in New Issue
Block a user