1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"sync"
"sync/atomic"
"time"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
)
type QRChannelItem struct {
// The type of event, "code" for new QR codes (see Code field) and "error" for pairing errors (see Error) field.
// For non-code/error events, you can just compare the whole item to the event variables (like QRChannelSuccess).
Event string
// If the item is a pair error, then this field contains the error message.
Error error
// If the item is a new code, then this field contains the raw data.
Code string
// The timeout after which the next code will be sent down the channel.
Timeout time.Duration
}
const QRChannelEventCode = "code"
const QRChannelEventError = "error"
// Possible final items in the QR channel. In addition to these, an `error` event may be emitted,
// in which case the Error field will have the error that occurred during pairing.
var (
// QRChannelSuccess is emitted from GetQRChannel when the pairing is successful.
QRChannelSuccess = QRChannelItem{Event: "success"}
// QRChannelTimeout is emitted from GetQRChannel if the socket gets disconnected by the server before the pairing is successful.
QRChannelTimeout = QRChannelItem{Event: "timeout"}
// QRChannelErrUnexpectedEvent is emitted from GetQRChannel if an unexpected connection event is received,
// as that likely means that the pairing has already happened before the channel was set up.
QRChannelErrUnexpectedEvent = QRChannelItem{Event: "err-unexpected-state"}
// QRChannelClientOutdated is emitted from GetQRChannel if events.ClientOutdated is received.
QRChannelClientOutdated = QRChannelItem{Event: "err-client-outdated"}
// QRChannelScannedWithoutMultidevice is emitted from GetQRChannel if events.QRScannedWithoutMultidevice is received.
QRChannelScannedWithoutMultidevice = QRChannelItem{Event: "err-scanned-without-multidevice"}
)
type qrChannel struct {
sync.Mutex
cli *Client
log waLog.Logger
ctx context.Context
handlerID uint32
closed uint32
output chan<- QRChannelItem
stopQRs chan struct{}
}
func (qrc *qrChannel) emitQRs(evt *events.QR) {
var nextCode string
for {
if len(evt.Codes) == 0 {
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
qrc.log.Debugf("Ran out of QR codes, closing channel with status %s and disconnecting client", QRChannelTimeout)
qrc.output <- QRChannelTimeout
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
} else {
qrc.log.Debugf("Ran out of QR codes, but channel is already closed")
}
return
} else if atomic.LoadUint32(&qrc.closed) == 1 {
qrc.log.Debugf("QR code channel is closed, exiting QR emitter")
return
}
timeout := 20 * time.Second
if len(evt.Codes) == 6 {
timeout = 60 * time.Second
}
nextCode, evt.Codes = evt.Codes[0], evt.Codes[1:]
qrc.log.Debugf("Emitting QR code %s", nextCode)
select {
case qrc.output <- QRChannelItem{Code: nextCode, Timeout: timeout, Event: QRChannelEventCode}:
default:
qrc.log.Debugf("Output channel didn't accept code, exiting QR emitter")
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
}
return
}
select {
case <-time.After(timeout):
case <-qrc.stopQRs:
qrc.log.Debugf("Got signal to stop QR emitter")
return
case <-qrc.ctx.Done():
qrc.log.Debugf("Context is done, stopping QR emitter")
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
}
}
}
}
func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
if atomic.LoadUint32(&qrc.closed) == 1 {
qrc.log.Debugf("Dropping event of type %T, channel is closed", rawEvt)
return
}
var outputType QRChannelItem
switch evt := rawEvt.(type) {
case *events.QR:
qrc.log.Debugf("Received QR code event, starting to emit codes to channel")
go qrc.emitQRs(evt)
return
case *events.QRScannedWithoutMultidevice:
qrc.log.Debugf("QR code scanned without multidevice enabled")
qrc.output <- QRChannelScannedWithoutMultidevice
return
case *events.ClientOutdated:
outputType = QRChannelClientOutdated
case *events.PairSuccess:
outputType = QRChannelSuccess
case *events.PairError:
outputType = QRChannelItem{
Event: QRChannelEventError,
Error: evt.Error,
}
case *events.Disconnected:
outputType = QRChannelTimeout
case *events.Connected, *events.ConnectFailure, *events.LoggedOut, *events.TemporaryBan:
outputType = QRChannelErrUnexpectedEvent
default:
return
}
close(qrc.stopQRs)
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
qrc.log.Debugf("Closing channel with status %+v", outputType)
qrc.output <- outputType
close(qrc.output)
} else {
qrc.log.Debugf("Got status %+v, but channel is already closed", outputType)
}
// Has to be done in background because otherwise there's a deadlock with eventHandlersLock
go qrc.cli.RemoveEventHandler(qrc.handlerID)
}
// GetQRChannel returns a channel that automatically outputs a new QR code when the previous one expires.
//
// This must be called *before* Connect(). It will then listen to all the relevant events from the client.
//
// The last value to be emitted will be a special event like "success", "timeout" or another error code
// depending on the result of the pairing. The channel will be closed immediately after one of those.
func (cli *Client) GetQRChannel(ctx context.Context) (<-chan QRChannelItem, error) {
if cli.IsConnected() {
return nil, ErrQRAlreadyConnected
} else if cli.Store.ID != nil {
return nil, ErrQRStoreContainsID
}
ch := make(chan QRChannelItem, 8)
qrc := qrChannel{
output: ch,
stopQRs: make(chan struct{}),
cli: cli,
log: cli.Log.Sub("QRChannel"),
ctx: ctx,
}
qrc.handlerID = cli.AddEventHandler(qrc.handleEvent)
return ch, nil
}
|