+// unlink 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.
+func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text string)) {
+ w.mutex.Lock()
+ linked := w.linked
+ client := w.client
+ w.mutex.Unlock()
+
+ if !linked {
+ answer("WhatsApp is not linked.")
+ return
+ }
+
+ var logoutErr error
+ if client != nil {
+ ctx, cancel := context.WithTimeout(ctx, whatsappLogoutTime)
+ defer cancel()
+
+ logoutErr = client.Logout(ctx)
+ }
+
+ err := w.removeSession()
+ if err != nil {
+ answer("The unlink failed: " + w.redact(err.Error()))
+ return
+ }
+
+ err = l.note("WhatsApp is unlinked.")
+ if err != nil {
+ w.logger.Printf("note the unlink: %v", 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
+ }
+
+ answer("WhatsApp is unlinked.")
+}
+
+// removeSession ends the connection, closes the session database, and deletes
+// its files.
+func (w *WhatsApp) removeSession() error {
+ w.mutex.Lock()
+ client := w.client
+ w.client = nil
+ w.linked = false
+ w.own = nil
+ w.connected = false
+ if w.timer != nil {
+ w.timer.Stop()
+ }
+
+ w.noticed = false
+ container := w.container
+ w.container = nil
+ w.device = nil
+ w.mutex.Unlock()
+
+ 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
+}
+
+// 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.
+func (w *WhatsApp) pair(ctx context.Context, l *TurnLoop, phone string, answer func(text string)) {
+ device, err := w.newDevice(ctx)
+ if err != nil {
+ answer("The pairing failed: " + err.Error())
+ return
+ }
+
+ client := w.newClient(device, w.handle)
+ linked := false
+ defer func() {
+ if !linked {
+ client.Disconnect()
+ }
+ }()
+
+ qr, err := client.GetQRChannel(ctx)
+ if err == nil {
+ err = client.ConnectContext(ctx)
+ }
+
+ if err != nil {
+ answer("The connection to WhatsApp failed: " + err.Error())
+ return
+ }
+
+ select {
+ case item, open := <-qr:
+ if !open || item.Event != whatsmeow.QRChannelEventCode {
+ answer("WhatsApp did not start the pairing. Try again later.")
+ return
+ }
+ case <-ctx.Done():
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+ answer("The connection to WhatsApp did not start the pairing in time. Try again later.")
+ }
+
+ return
+ }
+
+ code, err := client.PairPhone(ctx, phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
+ if err != nil {
+ answer("WhatsApp refused the pairing: " + err.Error() + ". Check the phone number and run /whatsapp pair PHONE again.")
+ return
+ }
+
+ answer(whatsappPairSteps)
+ answer(code)
+
+ for {
+ var item whatsmeow.QRChannelItem
+ open := true
+ select {
+ case item, open = <-qr:
+ case <-ctx.Done():
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+ answer(whatsappPairExpired)
+ }
+
+ return
+ }
+
+ switch {
+ case !open || item.Event == whatsmeow.QRChannelTimeout.Event:
+ answer(whatsappPairExpired)
+ return
+ case item.Event == whatsmeow.QRChannelEventCode:
+ continue
+ case item.Event == whatsmeow.QRChannelSuccess.Event:
+ linked = true
+ w.linkedBy(client)
+
+ err = l.note("WhatsApp is linked.")
+ if err != nil {
+ w.logger.Printf("note the link: %v", err)
+ }
+
+ answer("WhatsApp is linked to +" + phone + ".")
+
+ return
+ default:
+ answer("The pairing failed: " + whatsappItemError(item) + ". Run /whatsapp pair PHONE again.")
+ return
+ }
+ }
+}
+
+// newDevice returns a new device in the session database. It makes the
+// directory and the file of the session first, with their modes, when they do
+// not exist.
+func (w *WhatsApp) newDevice(ctx context.Context) (*store.Device, error) {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ if w.container == nil {
+ err := os.MkdirAll(w.dir, 0o700)
+ if err != nil {
+ return nil, fmt.Errorf("create WhatsApp directory: %w", err)
+ }
+
+ file, err := os.OpenFile(w.sessionPath(), os.O_CREATE|os.O_RDWR, 0o600)
+ if err != nil {
+ return nil, fmt.Errorf("create WhatsApp session: %w", err)
+ }
+
+ err = file.Close()
+ if err != nil {
+ return nil, fmt.Errorf("create WhatsApp session: %w", err)
+ }
+
+ err = w.openSession(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ w.device = w.container.NewDevice()
+
+ return w.device, nil
+}
+
+// linkedBy makes client the client of the linked device.
+func (w *WhatsApp) linkedBy(client WhatsAppClient) {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ w.client = client
+ w.linked = true