slidge-whatsapp 0.2.2__cp312-cp312-manylinux_2_36_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidge_whatsapp/__init__.py +17 -0
- slidge_whatsapp/__main__.py +3 -0
- slidge_whatsapp/command.py +143 -0
- slidge_whatsapp/config.py +32 -0
- slidge_whatsapp/contact.py +77 -0
- slidge_whatsapp/event.go +1175 -0
- slidge_whatsapp/gateway.go +181 -0
- slidge_whatsapp/gateway.py +82 -0
- slidge_whatsapp/generated/__init__.py +0 -0
- slidge_whatsapp/generated/_whatsapp.cpython-312-aarch64-linux-gnu.h +606 -0
- slidge_whatsapp/generated/_whatsapp.cpython-312-aarch64-linux-gnu.so +0 -0
- slidge_whatsapp/generated/build.py +395 -0
- slidge_whatsapp/generated/go.py +1632 -0
- slidge_whatsapp/generated/whatsapp.c +6887 -0
- slidge_whatsapp/generated/whatsapp.go +3572 -0
- slidge_whatsapp/generated/whatsapp.py +2911 -0
- slidge_whatsapp/generated/whatsapp_go.h +606 -0
- slidge_whatsapp/go.mod +29 -0
- slidge_whatsapp/go.sum +62 -0
- slidge_whatsapp/group.py +256 -0
- slidge_whatsapp/media/ffmpeg.go +72 -0
- slidge_whatsapp/media/media.go +542 -0
- slidge_whatsapp/media/mupdf.go +47 -0
- slidge_whatsapp/media/stub.go +19 -0
- slidge_whatsapp/session.go +855 -0
- slidge_whatsapp/session.py +745 -0
- slidge_whatsapp-0.2.2.dist-info/LICENSE +661 -0
- slidge_whatsapp-0.2.2.dist-info/METADATA +744 -0
- slidge_whatsapp-0.2.2.dist-info/RECORD +31 -0
- slidge_whatsapp-0.2.2.dist-info/WHEEL +4 -0
- slidge_whatsapp-0.2.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
package whatsapp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
// Standard library.
|
|
5
|
+
"bytes"
|
|
6
|
+
"context"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"image/jpeg"
|
|
10
|
+
"math/rand"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
// Internal packages.
|
|
14
|
+
"codeberg.org/slidge/slidge-whatsapp/slidge_whatsapp/media"
|
|
15
|
+
|
|
16
|
+
// Third-party libraries.
|
|
17
|
+
_ "github.com/mattn/go-sqlite3"
|
|
18
|
+
"go.mau.fi/whatsmeow"
|
|
19
|
+
"go.mau.fi/whatsmeow/appstate"
|
|
20
|
+
"go.mau.fi/whatsmeow/proto/waCommon"
|
|
21
|
+
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
22
|
+
"go.mau.fi/whatsmeow/proto/waHistorySync"
|
|
23
|
+
"go.mau.fi/whatsmeow/store"
|
|
24
|
+
"go.mau.fi/whatsmeow/types"
|
|
25
|
+
"go.mau.fi/whatsmeow/types/events"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const (
|
|
29
|
+
// The default host part for user JIDs on WhatsApp.
|
|
30
|
+
DefaultUserServer = types.DefaultUserServer
|
|
31
|
+
|
|
32
|
+
// The default host part for group JIDs on WhatsApp.
|
|
33
|
+
DefaultGroupServer = types.GroupServer
|
|
34
|
+
|
|
35
|
+
// The number of times keep-alive checks can fail before attempting to re-connect the session.
|
|
36
|
+
keepAliveFailureThreshold = 3
|
|
37
|
+
|
|
38
|
+
// The minimum and maximum wait interval between connection retries after keep-alive check failure.
|
|
39
|
+
keepAliveMinRetryInterval = 5 * time.Second
|
|
40
|
+
keepAliveMaxRetryInterval = 5 * time.Minute
|
|
41
|
+
|
|
42
|
+
// The amount of time to wait before re-requesting contact presences WhatsApp. This is required
|
|
43
|
+
// since otherwise WhatsApp will assume that you're inactive, and will stop sending presence
|
|
44
|
+
// updates for contacts and groups. By default, this interval has a jitter of ± half its value
|
|
45
|
+
// (e.g. for an initial interval of 2 hours, the final value will range from 1 to 3 hours) in
|
|
46
|
+
// order to provide a more natural interaction with remote WhatsApp servers.
|
|
47
|
+
presenceRefreshInterval = 12 * time.Hour
|
|
48
|
+
|
|
49
|
+
// The maximum number of messages to request at a time when performing on-demand history
|
|
50
|
+
// synchronization.
|
|
51
|
+
maxHistorySyncMessages = 50
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// A Session represents a connection (active or not) between a linked device and WhatsApp. Active
|
|
55
|
+
// sessions need to be established by logging in, after which incoming events will be forwarded to
|
|
56
|
+
// the adapter event handler, and outgoing events will be forwarded to WhatsApp.
|
|
57
|
+
type Session struct {
|
|
58
|
+
device LinkedDevice // The linked device this session corresponds to.
|
|
59
|
+
client *whatsmeow.Client // The concrete client connection to WhatsApp for this session.
|
|
60
|
+
gateway *Gateway // The Gateway this Session is attached to.
|
|
61
|
+
eventHandler HandleEventFunc // The handler function to use for propagating events to the adapter.
|
|
62
|
+
presenceChan chan PresenceKind // A channel used for periodically refreshing contact presences.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Login attempts to authenticate the given [Session], either by re-using the [LinkedDevice] attached
|
|
66
|
+
// or by initiating a pairing session for a new linked device. Callers are expected to have set an
|
|
67
|
+
// event handler in order to receive any incoming events from the underlying WhatsApp session.
|
|
68
|
+
func (s *Session) Login() error {
|
|
69
|
+
var err error
|
|
70
|
+
var store *store.Device
|
|
71
|
+
|
|
72
|
+
// Try to fetch existing device from given device JID.
|
|
73
|
+
if s.device.ID != "" {
|
|
74
|
+
store, err = s.gateway.container.GetDevice(s.device.JID())
|
|
75
|
+
if err != nil {
|
|
76
|
+
return err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if store == nil {
|
|
81
|
+
store = s.gateway.container.NewDevice()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
s.client = whatsmeow.NewClient(store, s.gateway.logger)
|
|
85
|
+
s.client.AddEventHandler(s.handleEvent)
|
|
86
|
+
s.client.AutomaticMessageRerequestFromPhone = true
|
|
87
|
+
|
|
88
|
+
// Refresh contact presences on a set interval, to avoid issues with WhatsApp dropping them
|
|
89
|
+
// entirely. Contact presences are refreshed only if our current status is set to "available";
|
|
90
|
+
// otherwise, a refresh is queued up for whenever our status changes back to "available".
|
|
91
|
+
s.presenceChan = make(chan PresenceKind, 1)
|
|
92
|
+
go func() {
|
|
93
|
+
var newTimer = func(d time.Duration) *time.Timer {
|
|
94
|
+
return time.NewTimer(d + time.Duration(rand.Int63n(int64(d))-int64(d/2)))
|
|
95
|
+
}
|
|
96
|
+
var timer, timerStopped = newTimer(presenceRefreshInterval), false
|
|
97
|
+
var presence = PresenceAvailable
|
|
98
|
+
for {
|
|
99
|
+
select {
|
|
100
|
+
case <-timer.C:
|
|
101
|
+
if presence == PresenceAvailable {
|
|
102
|
+
_, _ = s.GetContacts(false)
|
|
103
|
+
timer, timerStopped = newTimer(presenceRefreshInterval), false
|
|
104
|
+
} else {
|
|
105
|
+
timerStopped = true
|
|
106
|
+
}
|
|
107
|
+
case p, ok := <-s.presenceChan:
|
|
108
|
+
if !ok && !timerStopped {
|
|
109
|
+
if !timer.Stop() {
|
|
110
|
+
<-timer.C
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
} else if timerStopped && p == PresenceAvailable {
|
|
114
|
+
_, _ = s.GetContacts(false)
|
|
115
|
+
timer, timerStopped = newTimer(presenceRefreshInterval), false
|
|
116
|
+
}
|
|
117
|
+
presence = p
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}()
|
|
121
|
+
|
|
122
|
+
// Simply connect our client if already registered.
|
|
123
|
+
if s.client.Store.ID != nil {
|
|
124
|
+
return s.client.Connect()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Attempt out-of-band registration of client via QR code.
|
|
128
|
+
qrChan, _ := s.client.GetQRChannel(context.Background())
|
|
129
|
+
if err = s.client.Connect(); err != nil {
|
|
130
|
+
return err
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
go func() {
|
|
134
|
+
for e := range qrChan {
|
|
135
|
+
if !s.client.IsConnected() {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
switch e.Event {
|
|
139
|
+
case whatsmeow.QRChannelEventCode:
|
|
140
|
+
s.propagateEvent(EventQRCode, &EventPayload{QRCode: e.Code})
|
|
141
|
+
case whatsmeow.QRChannelEventError:
|
|
142
|
+
s.propagateEvent(EventConnect, &EventPayload{Connect: Connect{Error: e.Error.Error()}})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}()
|
|
146
|
+
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Logout disconnects and removes the current linked device locally and initiates a logout remotely.
|
|
151
|
+
func (s *Session) Logout() error {
|
|
152
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
153
|
+
return nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
err := s.client.Logout()
|
|
157
|
+
s.client = nil
|
|
158
|
+
close(s.presenceChan)
|
|
159
|
+
|
|
160
|
+
return err
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Disconnects detaches the current connection to WhatsApp without removing any linked device state.
|
|
164
|
+
func (s *Session) Disconnect() error {
|
|
165
|
+
if s.client != nil {
|
|
166
|
+
s.client.Disconnect()
|
|
167
|
+
s.client = nil
|
|
168
|
+
close(s.presenceChan)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return nil
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// PairPhone returns a one-time code from WhatsApp, used for pairing this [Session] against the
|
|
175
|
+
// user's primary device, as identified by the given phone number. This will return an error if the
|
|
176
|
+
// [Session] is already paired, or if the phone number given is empty or invalid.
|
|
177
|
+
func (s *Session) PairPhone(phone string) (string, error) {
|
|
178
|
+
if s.client == nil {
|
|
179
|
+
return "", fmt.Errorf("Cannot pair for uninitialized session")
|
|
180
|
+
} else if s.client.Store.ID != nil {
|
|
181
|
+
return "", fmt.Errorf("Refusing to pair for connected session")
|
|
182
|
+
} else if phone == "" {
|
|
183
|
+
return "", fmt.Errorf("Cannot pair for empty phone number")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
code, err := s.client.PairPhone(phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
|
187
|
+
if err != nil {
|
|
188
|
+
return "", fmt.Errorf("Failed to pair with phone number: %s", err)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return code, nil
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// SendMessage processes the given Message and sends a WhatsApp message for the kind and contact JID
|
|
195
|
+
// specified within. In general, different message kinds require different fields to be set; see the
|
|
196
|
+
// documentation for the [Message] type for more information.
|
|
197
|
+
func (s *Session) SendMessage(message Message) error {
|
|
198
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
199
|
+
return fmt.Errorf("Cannot send message for unauthenticated session")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
jid, err := types.ParseJID(message.JID)
|
|
203
|
+
if err != nil {
|
|
204
|
+
return fmt.Errorf("Could not parse sender JID for message: %s", err)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
var payload *waE2E.Message
|
|
208
|
+
var extra whatsmeow.SendRequestExtra
|
|
209
|
+
|
|
210
|
+
switch message.Kind {
|
|
211
|
+
case MessageAttachment:
|
|
212
|
+
// Handle message with attachment, if any.
|
|
213
|
+
if len(message.Attachments) == 0 {
|
|
214
|
+
return nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Upload attachment into WhatsApp before sending message.
|
|
218
|
+
if payload, err = uploadAttachment(s.client, &message.Attachments[0]); err != nil {
|
|
219
|
+
return fmt.Errorf("Failed uploading attachment: %s", err)
|
|
220
|
+
}
|
|
221
|
+
extra.ID = message.ID
|
|
222
|
+
case MessageEdit:
|
|
223
|
+
// Edit existing message by ID.
|
|
224
|
+
payload = s.client.BuildEdit(s.device.JID().ToNonAD(), message.ID, s.getMessagePayload(message))
|
|
225
|
+
case MessageRevoke:
|
|
226
|
+
// Don't send message, but revoke existing message by ID.
|
|
227
|
+
var originJID types.JID
|
|
228
|
+
if message.OriginJID == "" {
|
|
229
|
+
// A message retraction by the person who sent it
|
|
230
|
+
originJID = types.EmptyJID
|
|
231
|
+
} else {
|
|
232
|
+
// A message moderation
|
|
233
|
+
originJID, err = types.ParseJID(message.OriginJID)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return fmt.Errorf("Could not parse sender JID for message: %s", err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
payload = s.client.BuildRevoke(jid, originJID, message.ID)
|
|
239
|
+
case MessageReaction:
|
|
240
|
+
// Send message as emoji reaction to a given message.
|
|
241
|
+
payload = &waE2E.Message{
|
|
242
|
+
ReactionMessage: &waE2E.ReactionMessage{
|
|
243
|
+
Key: &waCommon.MessageKey{
|
|
244
|
+
RemoteJID: &message.JID,
|
|
245
|
+
FromMe: &message.IsCarbon,
|
|
246
|
+
ID: &message.ID,
|
|
247
|
+
Participant: &message.OriginJID,
|
|
248
|
+
},
|
|
249
|
+
Text: &message.Body,
|
|
250
|
+
SenderTimestampMS: ptrTo(time.Now().UnixMilli()),
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
default:
|
|
254
|
+
payload = s.getMessagePayload(message)
|
|
255
|
+
extra.ID = message.ID
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
s.gateway.logger.Debugf("Sending message to JID '%s': %+v", jid, payload)
|
|
259
|
+
_, err = s.client.SendMessage(context.Background(), jid, payload, extra)
|
|
260
|
+
return err
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const (
|
|
264
|
+
// The maximum size thumbnail image we'll send in outgoing URL preview messages.
|
|
265
|
+
maxPreviewThumbnailSize = 1024 * 500 // 500KiB
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// GetMessagePayload returns a concrete WhatsApp protocol message for the given Message representation.
|
|
269
|
+
// The specific fields set within the protocol message, as well as its type, can depend on specific
|
|
270
|
+
// fields set in the Message type, and may be nested recursively (e.g. when replying to a reply).
|
|
271
|
+
func (s *Session) getMessagePayload(message Message) *waE2E.Message {
|
|
272
|
+
var payload *waE2E.Message
|
|
273
|
+
var ctx = context.Background()
|
|
274
|
+
|
|
275
|
+
// Compose extended message when made as a reply to a different message.
|
|
276
|
+
if message.ReplyID != "" {
|
|
277
|
+
// Fall back to our own JID if no origin JID has been specified, in which case we assume
|
|
278
|
+
// we're replying to our own messages.
|
|
279
|
+
if message.OriginJID == "" {
|
|
280
|
+
message.OriginJID = s.device.JID().ToNonAD().String()
|
|
281
|
+
}
|
|
282
|
+
payload = &waE2E.Message{
|
|
283
|
+
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
|
284
|
+
Text: &message.Body,
|
|
285
|
+
ContextInfo: &waE2E.ContextInfo{
|
|
286
|
+
StanzaID: &message.ReplyID,
|
|
287
|
+
QuotedMessage: &waE2E.Message{Conversation: ptrTo(message.ReplyBody)},
|
|
288
|
+
Participant: &message.OriginJID,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Add URL preview, if any was given in message.
|
|
295
|
+
if message.Preview.URL != "" {
|
|
296
|
+
if payload == nil {
|
|
297
|
+
payload = &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{Text: &message.Body}}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
switch message.Preview.Kind {
|
|
301
|
+
case PreviewPlain:
|
|
302
|
+
payload.ExtendedTextMessage.PreviewType = ptrTo(waE2E.ExtendedTextMessage_NONE)
|
|
303
|
+
case PreviewVideo:
|
|
304
|
+
payload.ExtendedTextMessage.PreviewType = ptrTo(waE2E.ExtendedTextMessage_VIDEO)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
payload.ExtendedTextMessage.MatchedText = &message.Preview.URL
|
|
308
|
+
payload.ExtendedTextMessage.CanonicalURL = &message.Preview.URL
|
|
309
|
+
payload.ExtendedTextMessage.Title = &message.Preview.Title
|
|
310
|
+
payload.ExtendedTextMessage.Description = &message.Preview.Description
|
|
311
|
+
|
|
312
|
+
if len(message.Preview.Thumbnail) > 0 && len(message.Preview.Thumbnail) < maxPreviewThumbnailSize {
|
|
313
|
+
data, err := media.Convert(ctx, message.Preview.Thumbnail, &previewThumbnailSpec)
|
|
314
|
+
if err == nil {
|
|
315
|
+
payload.ExtendedTextMessage.JPEGThumbnail = data
|
|
316
|
+
if info, err := jpeg.DecodeConfig(bytes.NewReader(data)); err == nil {
|
|
317
|
+
payload.ExtendedTextMessage.ThumbnailWidth = ptrTo(uint32(info.Width))
|
|
318
|
+
payload.ExtendedTextMessage.ThumbnailHeight = ptrTo(uint32(info.Height))
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Attach any inline mentions extended metadata.
|
|
325
|
+
if len(message.MentionJIDs) > 0 {
|
|
326
|
+
if payload == nil {
|
|
327
|
+
payload = &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{Text: &message.Body}}
|
|
328
|
+
}
|
|
329
|
+
if payload.ExtendedTextMessage.ContextInfo == nil {
|
|
330
|
+
payload.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{}
|
|
331
|
+
}
|
|
332
|
+
payload.ExtendedTextMessage.ContextInfo.MentionedJID = message.MentionJIDs
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Process any location information in message, if possible.
|
|
336
|
+
if message.Location.Latitude > 0 || message.Location.Longitude > 0 {
|
|
337
|
+
if payload == nil {
|
|
338
|
+
payload = &waE2E.Message{LocationMessage: &waE2E.LocationMessage{}}
|
|
339
|
+
}
|
|
340
|
+
payload.LocationMessage.DegreesLatitude = &message.Location.Latitude
|
|
341
|
+
payload.LocationMessage.DegreesLongitude = &message.Location.Longitude
|
|
342
|
+
payload.LocationMessage.AccuracyInMeters = ptrTo(uint32(message.Location.Accuracy))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if payload == nil {
|
|
346
|
+
payload = &waE2E.Message{Conversation: &message.Body}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return payload
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// GenerateMessageID returns a valid, pseudo-random message ID for use in outgoing messages.
|
|
353
|
+
func (s *Session) GenerateMessageID() string {
|
|
354
|
+
return s.client.GenerateMessageID()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// SendChatState sends the given chat state notification (e.g. composing message) to WhatsApp for the
|
|
358
|
+
// contact specified within.
|
|
359
|
+
func (s *Session) SendChatState(state ChatState) error {
|
|
360
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
361
|
+
return fmt.Errorf("Cannot send chat state for unauthenticated session")
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
jid, err := types.ParseJID(state.JID)
|
|
365
|
+
if err != nil {
|
|
366
|
+
return fmt.Errorf("Could not parse sender JID for chat state: %s", err)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
var presence types.ChatPresence
|
|
370
|
+
switch state.Kind {
|
|
371
|
+
case ChatStateComposing:
|
|
372
|
+
presence = types.ChatPresenceComposing
|
|
373
|
+
case ChatStatePaused:
|
|
374
|
+
presence = types.ChatPresencePaused
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return s.client.SendChatPresence(jid, presence, "")
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// SendReceipt sends a read receipt to WhatsApp for the message IDs specified within.
|
|
381
|
+
func (s *Session) SendReceipt(receipt Receipt) error {
|
|
382
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
383
|
+
return fmt.Errorf("Cannot send receipt for unauthenticated session")
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var jid, senderJID types.JID
|
|
387
|
+
var err error
|
|
388
|
+
|
|
389
|
+
if receipt.GroupJID != "" {
|
|
390
|
+
if senderJID, err = types.ParseJID(receipt.JID); err != nil {
|
|
391
|
+
return fmt.Errorf("Could not parse sender JID for receipt: %s", err)
|
|
392
|
+
} else if jid, err = types.ParseJID(receipt.GroupJID); err != nil {
|
|
393
|
+
return fmt.Errorf("Could not parse group JID for receipt: %s", err)
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
if jid, err = types.ParseJID(receipt.JID); err != nil {
|
|
397
|
+
return fmt.Errorf("Could not parse sender JID for receipt: %s", err)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ids := append([]types.MessageID{}, receipt.MessageIDs...)
|
|
402
|
+
return s.client.MarkRead(ids, time.Unix(receipt.Timestamp, 0), jid, senderJID)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// SendPresence sets the activity state and (optional) status message for the current session and
|
|
406
|
+
// user. An error is returned if setting availability fails for any reason.
|
|
407
|
+
func (s *Session) SendPresence(presence PresenceKind, statusMessage string) error {
|
|
408
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
409
|
+
return fmt.Errorf("Cannot send presence for unauthenticated session")
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
var err error
|
|
413
|
+
s.presenceChan <- presence
|
|
414
|
+
|
|
415
|
+
switch presence {
|
|
416
|
+
case PresenceAvailable:
|
|
417
|
+
err = s.client.SendPresence(types.PresenceAvailable)
|
|
418
|
+
case PresenceUnavailable:
|
|
419
|
+
err = s.client.SendPresence(types.PresenceUnavailable)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if err == nil && statusMessage != "" {
|
|
423
|
+
err = s.client.SetStatusMessage(statusMessage)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return err
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// GetContacts subscribes to the WhatsApp roster currently stored in the Session's internal state.
|
|
430
|
+
// If `refresh` is `true`, FetchRoster will pull application state from the remote service and
|
|
431
|
+
// synchronize any contacts found with the adapter.
|
|
432
|
+
func (s *Session) GetContacts(refresh bool) ([]Contact, error) {
|
|
433
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
434
|
+
return nil, fmt.Errorf("Cannot get contacts for unauthenticated session")
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Synchronize remote application state with local state if requested.
|
|
438
|
+
if refresh {
|
|
439
|
+
err := s.client.FetchAppState(appstate.WAPatchCriticalUnblockLow, false, false)
|
|
440
|
+
if err != nil {
|
|
441
|
+
s.gateway.logger.Warnf("Could not get app state from server: %s", err)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Synchronize local contact state with overarching gateway for all local contacts.
|
|
446
|
+
data, err := s.client.Store.Contacts.GetAllContacts()
|
|
447
|
+
if err != nil {
|
|
448
|
+
return nil, fmt.Errorf("Failed getting local contacts: %s", err)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
var contacts []Contact
|
|
452
|
+
for jid, info := range data {
|
|
453
|
+
if err = s.client.SubscribePresence(jid); err != nil {
|
|
454
|
+
s.gateway.logger.Warnf("Failed to subscribe to presence for %s", jid)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_, c := newContactEvent(jid, info)
|
|
458
|
+
contacts = append(contacts, c.Contact)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return contacts, nil
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// GetGroups returns a list of all group-chats currently joined in WhatsApp, along with additional
|
|
465
|
+
// information on present participants.
|
|
466
|
+
func (s *Session) GetGroups() ([]Group, error) {
|
|
467
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
468
|
+
return nil, fmt.Errorf("Cannot get groups for unauthenticated session")
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
data, err := s.client.GetJoinedGroups()
|
|
472
|
+
if err != nil {
|
|
473
|
+
return nil, fmt.Errorf("Failed getting groups: %s", err)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
var groups []Group
|
|
477
|
+
for _, info := range data {
|
|
478
|
+
groups = append(groups, newGroup(s.client, info))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return groups, nil
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// CreateGroup attempts to create a new WhatsApp group for the given human-readable name and
|
|
485
|
+
// participant JIDs given.
|
|
486
|
+
func (s *Session) CreateGroup(name string, participants []string) (Group, error) {
|
|
487
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
488
|
+
return Group{}, fmt.Errorf("Cannot create group for unauthenticated session")
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
var jids []types.JID
|
|
492
|
+
for _, p := range participants {
|
|
493
|
+
jid, err := types.ParseJID(p)
|
|
494
|
+
if err != nil {
|
|
495
|
+
return Group{}, fmt.Errorf("Could not parse participant JID: %s", err)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
jids = append(jids, jid)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
req := whatsmeow.ReqCreateGroup{Name: name, Participants: jids}
|
|
502
|
+
info, err := s.client.CreateGroup(req)
|
|
503
|
+
if err != nil {
|
|
504
|
+
return Group{}, fmt.Errorf("Could not create group: %s", err)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return newGroup(s.client, info), nil
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// LeaveGroup attempts to remove our own user from the given WhatsApp group, for the JID given.
|
|
511
|
+
func (s *Session) LeaveGroup(resourceID string) error {
|
|
512
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
513
|
+
return fmt.Errorf("Cannot leave group for unauthenticated session")
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
jid, err := types.ParseJID(resourceID)
|
|
517
|
+
if err != nil {
|
|
518
|
+
return fmt.Errorf("Could not parse JID for leaving group: %s", err)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return s.client.LeaveGroup(jid)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// GetAvatar fetches a profile picture for the Contact or Group JID given. If a non-empty `avatarID`
|
|
525
|
+
// is also given, GetAvatar will return an empty [Avatar] instance with no error if the remote state
|
|
526
|
+
// for the given ID has not changed.
|
|
527
|
+
func (s *Session) GetAvatar(resourceID, avatarID string) (Avatar, error) {
|
|
528
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
529
|
+
return Avatar{}, fmt.Errorf("Cannot get avatar for unauthenticated session")
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
jid, err := types.ParseJID(resourceID)
|
|
533
|
+
if err != nil {
|
|
534
|
+
return Avatar{}, fmt.Errorf("Could not parse JID for avatar: %s", err)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
p, err := s.client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ExistingID: avatarID})
|
|
538
|
+
if err != nil &&
|
|
539
|
+
!errors.Is(err, whatsmeow.ErrProfilePictureNotSet) &&
|
|
540
|
+
!errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
|
|
541
|
+
return Avatar{}, fmt.Errorf("Could not get avatar: %s", err)
|
|
542
|
+
} else if p != nil {
|
|
543
|
+
return Avatar{ID: p.ID, URL: p.URL}, nil
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return Avatar{}, nil
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// SetAvatar updates the profile picture for the Contact or Group JID given; it can also update the
|
|
550
|
+
// profile picture for our own user by providing an empty JID. The unique picture ID is returned,
|
|
551
|
+
// typically used as a cache reference or in providing to future calls for [Session.GetAvatar].
|
|
552
|
+
func (s *Session) SetAvatar(resourceID string, avatar []byte) (string, error) {
|
|
553
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
554
|
+
return "", fmt.Errorf("Cannot set avatar for unauthenticated session")
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
var ctx = context.Background()
|
|
558
|
+
var jid types.JID
|
|
559
|
+
var err error
|
|
560
|
+
|
|
561
|
+
// Setting the profile picture for the user expects an empty `resourceID`.
|
|
562
|
+
if resourceID == "" {
|
|
563
|
+
jid = types.EmptyJID
|
|
564
|
+
} else if jid, err = types.ParseJID(resourceID); err != nil {
|
|
565
|
+
return "", fmt.Errorf("Could not parse JID for avatar: %s", err)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if len(avatar) == 0 {
|
|
569
|
+
return s.client.SetGroupPhoto(jid, nil)
|
|
570
|
+
} else {
|
|
571
|
+
// Ensure avatar is in JPEG format, and convert before setting if needed.
|
|
572
|
+
data, err := media.Convert(ctx, avatar, &media.Spec{MIME: media.TypeJPEG})
|
|
573
|
+
if err != nil {
|
|
574
|
+
return "", fmt.Errorf("Failed converting avatar to JPEG: %s", err)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return s.client.SetGroupPhoto(jid, data)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// SetGroupName updates the name of a WhatsApp group for the Group JID given.
|
|
582
|
+
func (s *Session) SetGroupName(resourceID, name string) error {
|
|
583
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
584
|
+
return fmt.Errorf("Cannot set group name for unauthenticated session")
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
jid, err := types.ParseJID(resourceID)
|
|
588
|
+
if err != nil {
|
|
589
|
+
return fmt.Errorf("Could not parse JID for group name change: %s", err)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return s.client.SetGroupName(jid, name)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// SetGroupName updates the topic of a WhatsApp group for the Group JID given.
|
|
596
|
+
func (s *Session) SetGroupTopic(resourceID, topic string) error {
|
|
597
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
598
|
+
return fmt.Errorf("Cannot set group topic for unauthenticated session")
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
jid, err := types.ParseJID(resourceID)
|
|
602
|
+
if err != nil {
|
|
603
|
+
return fmt.Errorf("Could not parse JID for group topic change: %s", err)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return s.client.SetGroupTopic(jid, "", "", topic)
|
|
607
|
+
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// UpdateGroupParticipants processes changes to the given group's participants, including additions,
|
|
611
|
+
// removals, and changes to privileges. Participant JIDs given must be part of the authenticated
|
|
612
|
+
// session's roster at least, and must also be active group participants for other types of changes.
|
|
613
|
+
func (s *Session) UpdateGroupParticipants(resourceID string, participants []GroupParticipant) ([]GroupParticipant, error) {
|
|
614
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
615
|
+
return nil, fmt.Errorf("Cannot update group participants for unauthenticated session")
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
jid, err := types.ParseJID(resourceID)
|
|
619
|
+
if err != nil {
|
|
620
|
+
return nil, fmt.Errorf("Could not parse JID for group participant update: %s", err)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
var changes = make(map[whatsmeow.ParticipantChange][]types.JID)
|
|
624
|
+
for _, p := range participants {
|
|
625
|
+
participantJID, err := types.ParseJID(p.JID)
|
|
626
|
+
if err != nil {
|
|
627
|
+
return nil, fmt.Errorf("Could not parse participant JID for update: %s", err)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if c, err := s.client.Store.Contacts.GetContact(participantJID); err != nil {
|
|
631
|
+
return nil, fmt.Errorf("Could not fetch contact for participant: %s", err)
|
|
632
|
+
} else if !c.Found {
|
|
633
|
+
return nil, fmt.Errorf("Cannot update group participant for contact '%s' not in roster", participantJID)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
c := p.Action.toParticipantChange()
|
|
637
|
+
changes[c] = append(changes[c], participantJID)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
var result []GroupParticipant
|
|
641
|
+
for change, participantJIDs := range changes {
|
|
642
|
+
participants, err := s.client.UpdateGroupParticipants(jid, participantJIDs, change)
|
|
643
|
+
if err != nil {
|
|
644
|
+
return nil, fmt.Errorf("Failed setting group affiliation: %s", err)
|
|
645
|
+
}
|
|
646
|
+
for i := range participants {
|
|
647
|
+
p := newGroupParticipant(participants[i])
|
|
648
|
+
if p.JID == "" {
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
651
|
+
result = append(result, p)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return result, nil
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// FindContact attempts to check for a registered contact on WhatsApp corresponding to the given
|
|
659
|
+
// phone number, returning a concrete instance if found; typically, only the contact JID is set. No
|
|
660
|
+
// error is returned if no contact was found, but any unexpected errors will otherwise be returned
|
|
661
|
+
// directly.
|
|
662
|
+
func (s *Session) FindContact(phone string) (Contact, error) {
|
|
663
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
664
|
+
return Contact{}, fmt.Errorf("Cannot find contact for unauthenticated session")
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
jid := types.NewJID(phone, DefaultUserServer)
|
|
668
|
+
if c, err := s.client.Store.Contacts.GetContact(jid); err == nil && c.Found {
|
|
669
|
+
if _, e := newContactEvent(jid, c); e != nil {
|
|
670
|
+
return e.Contact, nil
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
resp, err := s.client.IsOnWhatsApp([]string{phone})
|
|
675
|
+
if err != nil {
|
|
676
|
+
return Contact{}, fmt.Errorf("Failed looking up contact '%s': %s", phone, err)
|
|
677
|
+
} else if len(resp) != 1 {
|
|
678
|
+
return Contact{}, fmt.Errorf("Failed looking up contact '%s': invalid response", phone)
|
|
679
|
+
} else if !resp[0].IsIn || resp[0].JID.IsEmpty() {
|
|
680
|
+
return Contact{}, nil
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return Contact{JID: resp[0].JID.ToNonAD().String()}, nil
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// RequestMessageHistory sends and asynchronous request for message history related to the given
|
|
687
|
+
// resource (e.g. Contact or Group JID), ending at the oldest message given. Messages returned from
|
|
688
|
+
// history should then be handled as a `HistorySync` event of type `ON_DEMAND`, in the session-wide
|
|
689
|
+
// event handler. An error will be returned if requesting history fails for any reason.
|
|
690
|
+
func (s *Session) RequestMessageHistory(resourceID string, oldestMessage Message) error {
|
|
691
|
+
if s.client == nil || s.client.Store.ID == nil {
|
|
692
|
+
return fmt.Errorf("Cannot request history for unauthenticated session")
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
jid, err := types.ParseJID(resourceID)
|
|
696
|
+
if err != nil {
|
|
697
|
+
return fmt.Errorf("Could not parse JID for history request: %s", err)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
info := &types.MessageInfo{
|
|
701
|
+
ID: oldestMessage.ID,
|
|
702
|
+
MessageSource: types.MessageSource{Chat: jid, IsFromMe: oldestMessage.IsCarbon},
|
|
703
|
+
Timestamp: time.Unix(oldestMessage.Timestamp, 0).UTC(),
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
req := s.client.BuildHistorySyncRequest(info, maxHistorySyncMessages)
|
|
707
|
+
_, err = s.client.SendMessage(context.Background(), s.device.JID().ToNonAD(), req, whatsmeow.SendRequestExtra{Peer: true})
|
|
708
|
+
if err != nil {
|
|
709
|
+
return fmt.Errorf("Failed to request history for %s: %s", resourceID, err)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return nil
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// SetEventHandler assigns the given handler function for propagating internal events into the Python
|
|
716
|
+
// gateway. Note that the event handler function is not entirely safe to use directly, and all calls
|
|
717
|
+
// should instead be sent to the [Gateway] via its internal call channel.
|
|
718
|
+
func (s *Session) SetEventHandler(h HandleEventFunc) {
|
|
719
|
+
s.eventHandler = h
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// PropagateEvent handles the given event kind and payload with the adapter event handler defined in
|
|
723
|
+
// [Session.SetEventHandler].
|
|
724
|
+
func (s *Session) propagateEvent(kind EventKind, payload *EventPayload) {
|
|
725
|
+
if s.eventHandler == nil {
|
|
726
|
+
s.gateway.logger.Errorf("Event handler not set when propagating event %d with payload %v", kind, payload)
|
|
727
|
+
return
|
|
728
|
+
} else if kind == EventUnknown {
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Send empty payload instead of a nil pointer, as Python has trouble handling the latter.
|
|
733
|
+
if payload == nil {
|
|
734
|
+
payload = &EventPayload{}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
s.gateway.callChan <- func() { s.eventHandler(kind, payload) }
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// HandleEvent processes the given incoming WhatsApp event, checking its concrete type and
|
|
741
|
+
// propagating it to the adapter event handler. Unknown or unhandled events are ignored, and any
|
|
742
|
+
// errors that occur during processing are logged.
|
|
743
|
+
func (s *Session) handleEvent(evt interface{}) {
|
|
744
|
+
s.gateway.logger.Debugf("Handling event '%T': %+v", evt, evt)
|
|
745
|
+
|
|
746
|
+
switch evt := evt.(type) {
|
|
747
|
+
case *events.AppStateSyncComplete:
|
|
748
|
+
if len(s.client.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
|
|
749
|
+
s.propagateEvent(EventConnect, &EventPayload{Connect: Connect{JID: s.device.JID().ToNonAD().String()}})
|
|
750
|
+
if err := s.client.SendPresence(types.PresenceAvailable); err != nil {
|
|
751
|
+
s.gateway.logger.Warnf("Failed to send available presence: %s", err)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
case *events.ConnectFailure:
|
|
755
|
+
switch evt.Reason {
|
|
756
|
+
case events.ConnectFailureLoggedOut:
|
|
757
|
+
// These events are handled separately.
|
|
758
|
+
default:
|
|
759
|
+
s.gateway.logger.Errorf("Failed to connect: %s", evt.Message)
|
|
760
|
+
s.propagateEvent(EventConnect, &EventPayload{Connect: Connect{Error: evt.Message}})
|
|
761
|
+
}
|
|
762
|
+
case *events.Connected, *events.PushNameSetting:
|
|
763
|
+
if len(s.client.Store.PushName) == 0 {
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
s.propagateEvent(EventConnect, &EventPayload{Connect: Connect{JID: s.device.JID().ToNonAD().String()}})
|
|
767
|
+
if err := s.client.SendPresence(types.PresenceAvailable); err != nil {
|
|
768
|
+
s.gateway.logger.Warnf("Failed to send available presence: %s", err)
|
|
769
|
+
}
|
|
770
|
+
case *events.HistorySync:
|
|
771
|
+
switch evt.Data.GetSyncType() {
|
|
772
|
+
case waHistorySync.HistorySync_PUSH_NAME:
|
|
773
|
+
for _, n := range evt.Data.GetPushnames() {
|
|
774
|
+
jid, err := types.ParseJID(n.GetID())
|
|
775
|
+
if err != nil {
|
|
776
|
+
continue
|
|
777
|
+
}
|
|
778
|
+
s.propagateEvent(newContactEvent(jid, types.ContactInfo{FullName: n.GetPushname()}))
|
|
779
|
+
if err = s.client.SubscribePresence(jid); err != nil {
|
|
780
|
+
s.gateway.logger.Warnf("Failed to subscribe to presence for %s", jid)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
case waHistorySync.HistorySync_INITIAL_BOOTSTRAP, waHistorySync.HistorySync_RECENT, waHistorySync.HistorySync_ON_DEMAND:
|
|
784
|
+
for _, c := range evt.Data.GetConversations() {
|
|
785
|
+
for _, msg := range c.GetMessages() {
|
|
786
|
+
s.propagateEvent(newEventFromHistory(s.client, msg.GetMessage()))
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
case *events.Message:
|
|
791
|
+
s.propagateEvent(newMessageEvent(s.client, evt))
|
|
792
|
+
case *events.Receipt:
|
|
793
|
+
s.propagateEvent(newReceiptEvent(evt))
|
|
794
|
+
case *events.Presence:
|
|
795
|
+
s.propagateEvent(newPresenceEvent(evt))
|
|
796
|
+
case *events.PushName:
|
|
797
|
+
s.propagateEvent(newContactEvent(evt.JID, types.ContactInfo{FullName: evt.NewPushName}))
|
|
798
|
+
case *events.JoinedGroup:
|
|
799
|
+
s.propagateEvent(EventGroup, &EventPayload{Group: newGroup(s.client, &evt.GroupInfo)})
|
|
800
|
+
case *events.GroupInfo:
|
|
801
|
+
s.propagateEvent(newGroupEvent(evt))
|
|
802
|
+
case *events.ChatPresence:
|
|
803
|
+
s.propagateEvent(newChatStateEvent(evt))
|
|
804
|
+
case *events.CallOffer:
|
|
805
|
+
s.propagateEvent(newCallEvent(CallIncoming, evt.BasicCallMeta))
|
|
806
|
+
case *events.CallTerminate:
|
|
807
|
+
s.propagateEvent(newCallEvent(callStateFromReason(evt.Reason), evt.BasicCallMeta))
|
|
808
|
+
case *events.LoggedOut:
|
|
809
|
+
s.client.Disconnect()
|
|
810
|
+
if err := s.client.Store.Delete(); err != nil {
|
|
811
|
+
s.gateway.logger.Warnf("Unable to delete local device state on logout: %s", err)
|
|
812
|
+
}
|
|
813
|
+
s.client = nil
|
|
814
|
+
s.propagateEvent(EventLoggedOut, nil)
|
|
815
|
+
case *events.PairSuccess:
|
|
816
|
+
if s.client.Store.ID == nil {
|
|
817
|
+
s.gateway.logger.Errorf("Pairing succeeded, but device ID is missing")
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
s.device.ID = s.client.Store.ID.String()
|
|
821
|
+
s.propagateEvent(EventPair, &EventPayload{PairDeviceID: s.device.ID})
|
|
822
|
+
if err := s.gateway.CleanupSession(LinkedDevice{ID: s.device.ID}); err != nil {
|
|
823
|
+
s.gateway.logger.Warnf("Failed to clean up devices after pair: %s", err)
|
|
824
|
+
}
|
|
825
|
+
case *events.KeepAliveTimeout:
|
|
826
|
+
if evt.ErrorCount > keepAliveFailureThreshold {
|
|
827
|
+
s.gateway.logger.Debugf("Forcing reconnection after keep-alive timeouts...")
|
|
828
|
+
go func() {
|
|
829
|
+
var interval = keepAliveMinRetryInterval
|
|
830
|
+
s.client.Disconnect()
|
|
831
|
+
for {
|
|
832
|
+
err := s.client.Connect()
|
|
833
|
+
if err == nil || err == whatsmeow.ErrAlreadyConnected {
|
|
834
|
+
break
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
s.gateway.logger.Errorf("Error reconnecting after keep-alive timeouts, retrying in %s: %s", interval, err)
|
|
838
|
+
time.Sleep(interval)
|
|
839
|
+
|
|
840
|
+
if interval > keepAliveMaxRetryInterval {
|
|
841
|
+
interval = keepAliveMaxRetryInterval
|
|
842
|
+
} else if interval < keepAliveMaxRetryInterval {
|
|
843
|
+
interval *= 2
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}()
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// PtrTo returns a pointer to the given value, and is used for convenience when converting between
|
|
852
|
+
// concrete and pointer values without assigning to a variable.
|
|
853
|
+
func ptrTo[T any](t T) *T {
|
|
854
|
+
return &t
|
|
855
|
+
}
|