+// 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
+}
+
+// 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()
+ }
+}
+
+// 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),
+ }
+}
+
+// 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")
+
+ 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
+}
+
+func (w *WhatsApp) isLinked() bool {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ return w.linked
+}
+
+// pair links a new device to the account of phone with a pairing code. It gives
+// the steps and the code to answer, and then the end of the pairing: the linked
+// number, an expired code, or a failure. A pairing that a new one ends gives no
+// answer. A link notes "WhatsApp is linked." in the log of l, with no number.
+// The client of a pairing that does not link is disconnected.
+//
+// ctx limits the pairing, but the connection uses runCtx, the context of Run.
+// The library keeps the context of the connection for its keep-alive, its event
+// handler, and its reconnections, and after the link the server ends the stream
+// and the library connects again. With ctx, the end of the pairing would stop
+// that reconnection, and the linked device would never connect.
+func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, answer func(text string)) {
+ device, err := w.newDevice(ctx)
+ if err != nil {
+ answer("The pairing failed: " + err.Error())
+