+// Send returns the answer of whatsapp_send for the send id of text to chat, as
+// whatsappSend says.
+func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, error) {
+ return whatsappSend(ctx, w.link(), w.groups, w.state, w.clock, id, chat, text)
+}
+
+func (w *WhatsApp) sessionPath() string {
+ return filepath.Join(w.dir, whatsappSessionFileName)
+}
+
+// openSession opens the session database and its first device. The caller holds
+// the mutex, or no other goroutine uses w yet.
+func (w *WhatsApp) openSession(ctx context.Context) error {
+ address, err := whatsappSessionAddress(w.sessionPath())
+ if err != nil {
+ return err
+ }
+
+ container, err := sqlstore.New(ctx, "sqlite", address, waLog.Noop)
+ if err != nil {
+ return fmt.Errorf("open WhatsApp session: %w", err)
+ }
+
+ device, err := container.GetFirstDevice(ctx)
+ if err != nil {
+ return errors.Join(fmt.Errorf("read WhatsApp device: %w", err), container.Close())
+ }
+
+ w.container = container
+ w.device = device
+
+ return nil
+}
+
+func (w *WhatsApp) isLinked() bool {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ return w.linked
+}
+
+// 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 makes the client of the linked device and connects it.
+func (w *WhatsApp) connect(ctx context.Context) {
+ w.mutex.Lock()
+ w.client = w.newClient(w.device, w.handle)
+ client := w.client
+ w.mutex.Unlock()
+
+ err := client.ConnectContext(ctx)
+ if err != nil {
+ w.logger.Printf("WhatsApp messages do not come, because the connection failed, and it is tried again only at a restart of the daemon: %s", w.redact(err.Error()))
+ }
+}
+
+// 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.Print("WhatsApp messages stop, because the phone logged out this device (run /whatsapp pair PHONE to link it again)")
+ err := w.inbox.deliver(ctx, l, w.link(), true)
+ if err != nil {
+ return err
+ }
+
+ err = w.removeSession()
+ if err != nil {
+ w.logger.Printf("the session of the logged out device stays on the disk, because its delete failed: %v", err)
+ }
+
+ err = l.note("WhatsApp was unlinked from the phone.")
+ if err != nil {
+ w.logger.Printf("the chat has no note of the logout, because its write failed: %v", 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, whatsappLogoutTime)
+ defer cancel()
+
+ logoutErr = client.Logout(ctx)
+ }
+
+ err = w.removeSession()
+ if err != nil {
+ answer("The unlink failed: " + w.redact(err.Error()))
+ return nil
+ }
+
+ w.logger.Print("WhatsApp messages stop, because the user unlinked WhatsApp")
+ err = l.note("WhatsApp is unlinked.")
+ if err != nil {
+ w.logger.Printf("the chat has no note of the unlink, because its write failed: %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 nil
+ }
+
+ answer("WhatsApp is unlinked.")
+
+ return nil
+}
+
+// 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
+ 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
+}
+
+// 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())
+ 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(runCtx)
+ }
+
+ 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
+ }
+
+ // The server accepts only a display name of the form "Browser (OS)"
+ // with a common browser and OS, and refuses the pairing otherwise.
+ 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)
+ w.logger.Print("WhatsApp is linked")
+
+ err = l.note("WhatsApp is linked.")
+ if err != nil {
+ w.logger.Printf("the chat has no note of the link, because its write failed: %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) {