]> git.giacomolli.uk - onechat/commitdiff
Keep the connection of a new WhatsApp link
authorAnderson A. Giacomolli <redacted>
Sun, 8 Nov 2026 17:50:26 +0000 (14:50 -0300)
committerAnderson A. Giacomolli <redacted>
Sun, 8 Nov 2026 17:50:26 +0000 (14:50 -0300)
After a pairing, the linked device never connected again. The pairing
client connected with the context of the pairing, and the library keeps
the context of ConnectContext for its keep-alive loop, its event handler
loop, and its reconnections. After PairSuccess the server ends the
stream and the library connects again with that context, but the end of
the pairing canceled it, so the reconnection stopped. The session got no
prekeys, no Signal sessions, and no contacts, and no message came.

The pairing client now connects with the context of Run, which lives
until the daemon stops, as the connection at the start of the daemon
does. The context of the pairing still limits the QR channel, the wait
for the code, and the wait for the link, and a pairing that does not
link disconnects its client.

The fake client keeps the context of each connection, and the pairing
test checks that the context stays alive after the link. The test fails
without the fix.

Issue: 6f69ca1

whatsapp.go
whatsapp_fake_helper_test.go
whatsapp_test.go

index 4268a168fa1d4069f79b61b46b60119f6dc8fb32..42b95a8b42bf08f7313f87dfc27bfc36ecf8966f 100644 (file)
@@ -330,7 +330,7 @@ func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) err
                                pairing.Go(func() {
                                        defer cancelPair()
 
-                                       w.pair(pairCtx, l, request.phone, request.answer)
+                                       w.pair(pairCtx, ctx, l, request.phone, request.answer)
                                })
                        }
                }
@@ -797,7 +797,13 @@ func (w *WhatsApp) removeSession() error {
 // 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)) {
+//
+// 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())
@@ -814,7 +820,7 @@ func (w *WhatsApp) pair(ctx context.Context, l *TurnLoop, phone string, answer f
 
        qr, err := client.GetQRChannel(ctx)
        if err == nil {
-               err = client.ConnectContext(ctx)
+               err = client.ConnectContext(runCtx)
        }
 
        if err != nil {
index e463cd094605600ec6a11497baced15929f03a38..c9bc8357efc79e527fb8ffe7f746f57b5aec1c77 100644 (file)
@@ -67,28 +67,33 @@ var (
 
 // WhatsAppFake is a fake WhatsApp client. It keeps the event handler that the
 // daemon gives it, and Emit gives events to the handler, as the library does.
-// Each connection sends to connects, and each pairing sends its channel to
-// pairings, so that the test gives the items of the pairing. A connection fails
-// with connectErr, a logout with logoutErr, and a send with sendErr. groups are
-// the groups of the account. A send that does not fail gets the time
-// whatsappTestSendTime.
+// Each connection keeps its context and sends to connects, and each pairing
+// sends its channel to pairings, so that the test gives the items of the
+// pairing. A connection fails with connectErr, a logout with logoutErr, and a
+// send with sendErr. groups are the groups of the account. A send that does not
+// fail gets the time whatsappTestSendTime.
 type WhatsAppFake struct {
        connects chan struct{}
        pairings chan chan whatsmeow.QRChannelItem
 
-       mutex      sync.Mutex
-       handle     func(event any) bool
-       connectErr error
-       logoutErr  error
-       groups     []*types.GroupInfo
-       sendErr    error
-       phones     []string
-       logouts    int
-       sent       []WhatsAppFakeSend
+       mutex       sync.Mutex
+       handle      func(event any) bool
+       connectErr  error
+       logoutErr   error
+       groups      []*types.GroupInfo
+       sendErr     error
+       phones      []string
+       logouts     int
+       sent        []WhatsAppFakeSend
+       connections []context.Context
 }
 
-// ConnectContext tells the test that the daemon connected.
+// ConnectContext keeps ctx and tells the test that the daemon connected.
 func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
+       f.mutex.Lock()
+       f.connections = append(f.connections, ctx)
+       f.mutex.Unlock()
+
        f.connects <- struct{}{}
 
        f.mutex.Lock()
@@ -97,6 +102,14 @@ func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
        return f.connectErr
 }
 
+// Connections returns the contexts of the connections, in order.
+func (f *WhatsAppFake) Connections() []context.Context {
+       f.mutex.Lock()
+       defer f.mutex.Unlock()
+
+       return slices.Clone(f.connections)
+}
+
 // Disconnect does nothing.
 func (f *WhatsAppFake) Disconnect() {
 }
index 0ba27146bfb62d35c1991c33c58a82cc6dbef6de..791e3ca9d41e7f80f001478fd51c2bf6f14dd588 100644 (file)
@@ -140,6 +140,14 @@ func TestWhatsAppPairsWithACode(t *testing.T) {
        items <- whatsmeow.QRChannelSuccess
        asking.WaitFor(t, "WhatsApp is linked to +15550001111.")
 
+       // The library connects again after the link with the context of the
+       // connection, so the end of the pairing must not end it.
+       connections := fake.Connections()
+       require.Len(t, connections, 1)
+       require.Never(t, func() bool {
+               return connections[0].Err() != nil
+       }, 200*time.Millisecond, time.Millisecond)
+
        require.Equal(t, []string{whatsappTestNumber}, fake.Phones())
        require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
        require.Equal(t, "[connected to OneChat]\n", other.Output())