-// link returns a copy of the link of the device: whether it is linked, the
-// device, the client, whether it is connected, and the own IDs.
-func (w *WhatsApp) link() WhatsAppLink {
- w.mutex.Lock()
- defer w.mutex.Unlock()
-
- return WhatsAppLink{
- linked: w.linked,
- device: w.device,
- client: w.client,
- connected: w.connected,
- own: slices.Clone(w.own),
- }
-}
-
-// connect tries the starting connection of the linked device with one client.
-// It returns whether Run must retry. A failed call starts the shared outage
-// state and logs its redacted cause once, because a returned ConnectContext
-// error does not emit Disconnected. Connected logs recovery after
-// authentication.
-func (w *WhatsApp) connect(ctx context.Context) bool {
- w.mutex.Lock()
-
- if ctx.Err() != nil || !w.linked || !w.retrying || w.closed {
- w.mutex.Unlock()
-
- return false
- }
-
- if w.client == nil {
- w.client = w.newClient(w.device, w.handle)
- }
-
- client := w.client
- w.mutex.Unlock()
-
- err := client.ConnectContext(ctx)
-
- w.mutex.Lock()
-
- if err == nil || ctx.Err() != nil || !w.retrying || !w.linked {
- w.retrying = false
- w.mutex.Unlock()
-
- return false
- }
-
- first := w.startOutage()
- cause := whatsappRedact(w.own, err.Error())
- w.mutex.Unlock()
-
- if first {
- w.logger.Warn("first connect failed, retrying", "err", cause)
- }
-
- return true
-}
-
-// startOutage starts the shared outage time and notice timer once. The caller
-// holds the mutex. Its result tells whether this is the first failure.
-func (w *WhatsApp) startOutage() bool {
- first := w.linked && (w.connected || w.disconnectedAt.IsZero())
- if first {
- w.disconnectedAt = w.clock()
- w.disconnections++
- disconnections := w.disconnections
- w.timer = time.AfterFunc(whatsappOutageNoticeDelay, func() {
- w.disconnectedLong(disconnections)
- })
- }
-
- w.connected = false
-
- return first
-}
-
-// close ends the connection and the timer of the notice, and stops all notices.
-func (w *WhatsApp) close() {
- w.mutex.Lock()
-
- w.closed = true
- if w.timer != nil {
- w.timer.Stop()
- }
-
- client := w.client
- w.client = nil
- w.mutex.Unlock()
-
- w.inbox.close()
-
- if client != nil {
- client.Disconnect()
- }
-}
-
-// loggedOut logs the open batches, deletes the session after a logout from the
-// phone, notes it in the log of l, and tells all clients one time.
-func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error {
- if !w.isLinked() {
- return nil
- }
-
- w.logger.Error("logged out by the phone, run /whatsapp pair PHONE")
-
- err := w.inbox.deliver(ctx, l, w.link(), true)
- if err != nil {
- return err
- }
-
- err = w.removeSession()
- if err != nil {
- w.logger.Error("session delete failed", "err", err)
- }
-
- err = l.note("WhatsApp was unlinked from the phone.")
- if err != nil {
- w.logger.Error("chat note failed", "err", err)
- }
-
- w.broadcast("WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again.")
-
- return nil
-}
-
-// unlink logs the open batches, logs out the linked device, and deletes the
-// session. When the logout call fails, as it does while WhatsApp is
-// disconnected, the session is deleted all the same, and answer tells the user
-// to remove the device in the phone. Its error is one of the log or the state
-// file.
-func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text string)) error {
- w.mutex.Lock()
-
- linked := w.linked
- client := w.client
- w.mutex.Unlock()
-
- if !linked {
- answer("WhatsApp is not linked.")
-
- return nil
- }
-
- err := w.inbox.deliver(ctx, l, w.link(), true)
- if err != nil {
- return err
- }
-
- var logoutErr error
-
- if client != nil {
- ctx, cancel := context.WithTimeout(ctx, whatsappLogoutTimeout)
- defer cancel()
-
- logoutErr = client.Logout(ctx)
- }
-
- err = w.removeSession()
- if err != nil {
- answer("The unlink failed: " + w.redact(err.Error()))
-
- return nil
- }
-
- w.logger.Info("unlinked")
-
- err = l.note("WhatsApp is unlinked.")
- if err != nil {
- w.logger.Error("chat note failed", "err", err)
- }
-
- if client == nil || logoutErr != nil {
- answer("WhatsApp is unlinked here, but the phone did not get the logout. In WhatsApp on your phone, open Linked devices and remove the device.")
-
- return nil
- }
-
- answer("WhatsApp is unlinked.")
-
- return nil
-}
-
-// removeSession ends the connection, closes the session database, and deletes
-// its files. The session lock covers the cleanup after mutex is released, so a
-// snapshot or a new session cannot open files that cleanup removes.
-func (w *WhatsApp) removeSession() error {
- w.sessionMutex.Lock()
- defer w.sessionMutex.Unlock()
-
- w.mutex.Lock()
-
- client := w.client
- w.client = nil
- w.linked = false
- w.retrying = false
- w.own = nil
- w.connected = false
- w.disconnectedAt = time.Time{}
- if w.timer != nil {
- w.timer.Stop()
- }
-
- w.noticed = false
- container := w.container
- w.container = nil
- w.device = nil
- w.mutex.Unlock()
-
- w.inbox.stopCatchup()
-
- if client != nil {
- client.Disconnect()
- }
-
- var err error
-
- if container != nil {
- err = container.Close()
- }
-
- for _, suffix := range []string{"", "-wal", "-shm"} {
- removeErr := os.Remove(w.sessionPath() + suffix)
- if !errors.Is(removeErr, os.ErrNotExist) {
- err = errors.Join(err, removeErr)
- }
- }
-
- return err
-}
-