diff --git a/cmd/whasapo/main.go b/cmd/whasapo/main.go index a3ba66f..f0a607d 100644 --- a/cmd/whasapo/main.go +++ b/cmd/whasapo/main.go @@ -200,6 +200,8 @@ func cmdPair() { func cmdServe() { dbPath := getDBPath() + releaseLock := acquireServeLock() + defer releaseLock() var err error wa, err = whatsapp.NewClient(dbPath) @@ -327,6 +329,20 @@ func cmdServe() { } } +func acquireServeLock() func() { + lockPath := filepath.Join(dataDir(), "serve.lock") + lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "whasapo: another serve process is already running; close/restart your AI app or remove stale lock %s\n", lockPath) + os.Exit(1) + } + fmt.Fprintf(lock, "%d\n", os.Getpid()) + lock.Close() + return func() { + os.Remove(lockPath) + } +} + // --- status command --- func cmdStatus() { diff --git a/whatsapp/client.go b/whatsapp/client.go index 5a04de0..8a4fafd 100644 --- a/whatsapp/client.go +++ b/whatsapp/client.go @@ -12,7 +12,6 @@ import ( "sync/atomic" "time" - _ "modernc.org/sqlite" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waHistorySync" @@ -22,6 +21,7 @@ import ( "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" + _ "modernc.org/sqlite" ) // StoredMessage holds a message. @@ -55,7 +55,7 @@ type ChatDetail struct { Topic string `json:"topic,omitempty"` Participants []ParticipantInfo `json:"participants,omitempty"` CreatedAt string `json:"created_at,omitempty"` - MessageCount int `json:"message_count"` + MessageCount int `json:"message_count"` } // ParticipantInfo holds group participant info. @@ -67,9 +67,9 @@ type ParticipantInfo struct { // Client wraps whatsmeow with persistent message storage. type Client struct { - WM *whatsmeow.Client - Container *sqlstore.Container - db *sql.DB + WM *whatsmeow.Client + Container *sqlstore.Container + db *sql.DB firstConn chan struct{} // closed on first successful connection ready atomic.Bool loggedOut atomic.Bool @@ -77,6 +77,7 @@ type Client struct { names map[string]string namesMu sync.RWMutex mediaDir string + reconnectMu sync.Mutex } const dsn = "file:%s?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)" @@ -235,6 +236,7 @@ func (c *Client) eventHandler(evt interface{}) { // Only log on transition from connected → disconnected (debounce) c.disconnectAt.Store(time.Now().Unix()) fmt.Fprintf(os.Stderr, "whasapo: disconnected, reconnecting...\n") + c.scheduleReconnect() } case *events.LoggedOut: c.ready.Store(false) @@ -244,6 +246,7 @@ func (c *Client) eventHandler(evt interface{}) { c.ready.Store(false) c.disconnectAt.Store(time.Now().Unix()) fmt.Fprintf(os.Stderr, "whasapo: connection replaced by another client\n") + c.scheduleReconnect() case *events.HistorySync: c.handleHistorySync(v.Data) case *events.Message: @@ -260,6 +263,33 @@ func (c *Client) eventHandler(evt interface{}) { } } +func (c *Client) scheduleReconnect() { + go func() { + c.reconnectMu.Lock() + defer c.reconnectMu.Unlock() + if c.loggedOut.Load() || c.ready.Load() { + return + } + for attempt := 1; attempt <= 6; attempt++ { + time.Sleep(time.Duration(attempt*2) * time.Second) + if c.loggedOut.Load() || c.ready.Load() { + return + } + if err := c.WM.Connect(); err != nil { + fmt.Fprintf(os.Stderr, "whasapo: reconnect attempt %d failed: %v\n", attempt, err) + continue + } + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + if c.ready.Load() { + return + } + time.Sleep(250 * time.Millisecond) + } + } + }() +} + func (c *Client) messageToStored(info types.MessageInfo, msg *waE2E.Message) StoredMessage { text, mediaType := extractTextAndMedia(msg) sm := StoredMessage{