slidge-whatsapp 0.2.0a0__cp311-cp311-manylinux_2_36_x86_64.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.

Potentially problematic release.


This version of slidge-whatsapp might be problematic. Click here for more details.

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