slidge-whatsapp 0.2.2__cp313-cp313-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.

Potentially problematic release.


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

@@ -0,0 +1,1175 @@
1
+ package whatsapp
2
+
3
+ import (
4
+ // Standard library.
5
+ "context"
6
+ "fmt"
7
+ "mime"
8
+ "strings"
9
+
10
+ // Internal packages.
11
+ "codeberg.org/slidge/slidge-whatsapp/slidge_whatsapp/media"
12
+
13
+ // Third-party libraries.
14
+ "go.mau.fi/whatsmeow"
15
+ "go.mau.fi/whatsmeow/proto/waE2E"
16
+ "go.mau.fi/whatsmeow/proto/waWeb"
17
+ "go.mau.fi/whatsmeow/types"
18
+ "go.mau.fi/whatsmeow/types/events"
19
+ )
20
+
21
+ // EventKind represents all event types recognized by the Python session adapter, as emitted by the
22
+ // Go session adapter.
23
+ type EventKind int
24
+
25
+ // The event types handled by the overarching session adapter handler.
26
+ const (
27
+ EventUnknown EventKind = iota
28
+ EventQRCode
29
+ EventPair
30
+ EventConnect
31
+ EventLoggedOut
32
+ EventContact
33
+ EventPresence
34
+ EventMessage
35
+ EventChatState
36
+ EventReceipt
37
+ EventGroup
38
+ EventCall
39
+ )
40
+
41
+ // EventPayload represents the collected payloads for all event types handled by the overarching
42
+ // session adapter handler. Only specific fields will be populated in events emitted by internal
43
+ // handlers, see documentation for specific types for more information.
44
+ type EventPayload struct {
45
+ QRCode string
46
+ PairDeviceID string
47
+ Connect Connect
48
+ Contact Contact
49
+ Presence Presence
50
+ Message Message
51
+ ChatState ChatState
52
+ Receipt Receipt
53
+ Group Group
54
+ Call Call
55
+ }
56
+
57
+ // HandleEventFunc represents a handler for incoming events sent to the Python adapter, accepting an
58
+ // event type and payload.
59
+ type HandleEventFunc func(EventKind, *EventPayload)
60
+
61
+ // Connect represents event data related to a connection to WhatsApp being established, or failing
62
+ // to do so (based on the [Connect.Error] result).
63
+ type Connect struct {
64
+ JID string // The device JID given for this connection.
65
+ Error string // The connection error, if any.
66
+ }
67
+
68
+ // A Avatar represents a small image set for a Contact or Group.
69
+ type Avatar struct {
70
+ ID string // The unique ID for this avatar, used for persistent caching.
71
+ URL string // The HTTP URL over which this avatar might be retrieved. Can change for the same ID.
72
+ }
73
+
74
+ // A Contact represents any entity that be communicated with directly in WhatsApp. This typically
75
+ // represents people, but may represent a business or bot as well, but not a group-chat.
76
+ type Contact struct {
77
+ JID string // The WhatsApp JID for this contact.
78
+ Name string // The user-set, human-readable name for this contact.
79
+ }
80
+
81
+ // NewContactEvent returns event data meant for [Session.propagateEvent] for the contact information
82
+ // given. Unknown or invalid contact information will return an [EventUnknown] event with nil data.
83
+ func newContactEvent(jid types.JID, info types.ContactInfo) (EventKind, *EventPayload) {
84
+ var contact = Contact{
85
+ JID: jid.ToNonAD().String(),
86
+ }
87
+
88
+ for _, n := range []string{info.FullName, info.FirstName, info.BusinessName, info.PushName} {
89
+ if n != "" {
90
+ contact.Name = n
91
+ break
92
+ }
93
+ }
94
+
95
+ // Don't attempt to synchronize contacts with no user-readable name.
96
+ if contact.Name == "" {
97
+ return EventUnknown, nil
98
+ }
99
+
100
+ return EventContact, &EventPayload{Contact: contact}
101
+ }
102
+
103
+ // PresenceKind represents the different kinds of activity states possible in WhatsApp.
104
+ type PresenceKind int
105
+
106
+ // The presences handled by the overarching session event handler.
107
+ const (
108
+ PresenceUnknown PresenceKind = iota
109
+ PresenceAvailable
110
+ PresenceUnavailable
111
+ )
112
+
113
+ // Precence represents a contact's general state of activity, and is periodically updated as
114
+ // contacts start or stop paying attention to their client of choice.
115
+ type Presence struct {
116
+ JID string
117
+ Kind PresenceKind
118
+ LastSeen int64
119
+ }
120
+
121
+ // NewPresenceEvent returns event data meant for [Session.propagateEvent] for the primitive presence
122
+ // event given.
123
+ func newPresenceEvent(evt *events.Presence) (EventKind, *EventPayload) {
124
+ var presence = Presence{
125
+ JID: evt.From.ToNonAD().String(),
126
+ Kind: PresenceAvailable,
127
+ LastSeen: evt.LastSeen.Unix(),
128
+ }
129
+
130
+ if evt.Unavailable {
131
+ presence.Kind = PresenceUnavailable
132
+ }
133
+
134
+ return EventPresence, &EventPayload{Presence: presence}
135
+ }
136
+
137
+ // MessageKind represents all concrete message types (plain-text messages, edit messages, reactions)
138
+ // recognized by the Python session adapter.
139
+ type MessageKind int
140
+
141
+ // The message types handled by the overarching session event handler.
142
+ const (
143
+ MessagePlain MessageKind = iota
144
+ MessageEdit
145
+ MessageRevoke
146
+ MessageReaction
147
+ MessageAttachment
148
+ )
149
+
150
+ // A Message represents one of many kinds of bidirectional communication payloads, for example, a
151
+ // text message, a file (image, video) attachment, an emoji reaction, etc. Messages of different
152
+ // kinds are denoted as such, and re-use fields where the semantics overlap.
153
+ type Message struct {
154
+ Kind MessageKind // The concrete message kind being sent or received.
155
+ ID string // The unique message ID, used for referring to a specific Message instance.
156
+ JID string // The JID this message concerns, semantics can change based on IsCarbon.
157
+ GroupJID string // The JID of the group-chat this message was sent in, if any.
158
+ OriginJID string // For reactions and replies in groups, the JID of the original user.
159
+ Body string // The plain-text message body. For attachment messages, this can be a caption.
160
+ Timestamp int64 // The Unix timestamp denoting when this message was created.
161
+ IsCarbon bool // Whether or not this message concerns the gateway user themselves.
162
+ IsForwarded bool // Whether or not the message was forwarded from another source.
163
+ ReplyID string // The unique message ID this message is in reply to, if any.
164
+ ReplyBody string // The full body of the message this message is in reply to, if any.
165
+ Attachments []Attachment // The list of file (image, video, etc.) attachments contained in this message.
166
+ Preview Preview // A short description for the URL provided in the message body, if any.
167
+ Location Location // The location metadata for messages, if any.
168
+ MentionJIDs []string // A list of JIDs mentioned in this message, if any.
169
+ Receipts []Receipt // The receipt statuses for the message, typically provided alongside historical messages.
170
+ Reactions []Message // Reactions attached to message, typically provided alongside historical messages.
171
+ }
172
+
173
+ // A Attachment represents additional binary data (e.g. images, videos, documents) provided alongside
174
+ // a message, for display or storage on the recepient client.
175
+ type Attachment struct {
176
+ MIME string // The MIME type for attachment.
177
+ Filename string // The recommended file name for this attachment. May be an auto-generated name.
178
+ Caption string // The user-provided caption, provided alongside this attachment.
179
+ Data []byte // Data for the attachment.
180
+
181
+ // Internal fields.
182
+ spec *media.Spec // Metadata specific to audio/video files, used in processing.
183
+ }
184
+
185
+ // GetSpec returns metadata for this attachment, as derived from the underlying attachment data.
186
+ func (a *Attachment) GetSpec(ctx context.Context) (*media.Spec, error) {
187
+ if a.spec != nil {
188
+ return a.spec, nil
189
+ }
190
+
191
+ spec, err := media.GetSpec(ctx, a.Data)
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+
196
+ a.spec = spec
197
+ return a.spec, nil
198
+ }
199
+
200
+ // PreviewKind represents different ways of previewingadditional data inline with messages.
201
+ type PreviewKind int
202
+
203
+ const (
204
+ PreviewPlain PreviewKind = iota
205
+ PreviewVideo
206
+ )
207
+
208
+ // A Preview represents a short description for a URL provided in a message body, as usually derived
209
+ // from the content of the page pointed at.
210
+ type Preview struct {
211
+ Kind PreviewKind // The kind of preview to show, defaults to plain URL preview.
212
+ URL string // The original (or canonical) URL this preview was generated for.
213
+ Title string // The short title for the URL preview.
214
+ Description string // The (optional) long-form description for the URL preview.
215
+ Thumbnail []byte // The (optional) thumbnail image data.
216
+ }
217
+
218
+ // A Location represents additional metadata given to location messages.
219
+ type Location struct {
220
+ Latitude float64
221
+ Longitude float64
222
+ Accuracy int
223
+ IsLive bool
224
+
225
+ // Optional fields given for named locations.
226
+ Name string
227
+ Address string
228
+ URL string
229
+ }
230
+
231
+ // NewMessageEvent returns event data meant for [Session.propagateEvent] for the primive message
232
+ // event given. Unknown or invalid messages will return an [EventUnknown] event with nil data.
233
+ func newMessageEvent(client *whatsmeow.Client, evt *events.Message) (EventKind, *EventPayload) {
234
+ // Set basic data for message, to be potentially amended depending on the concrete version of
235
+ // the underlying message.
236
+ var message = Message{
237
+ Kind: MessagePlain,
238
+ ID: evt.Info.ID,
239
+ JID: evt.Info.Sender.ToNonAD().String(),
240
+ Body: evt.Message.GetConversation(),
241
+ Timestamp: evt.Info.Timestamp.Unix(),
242
+ IsCarbon: evt.Info.IsFromMe,
243
+ }
244
+
245
+ if evt.Info.Chat.Server == types.BroadcastServer {
246
+ // Handle non-carbon, non-status broadcast messages as plain messages; support for other
247
+ // types is lacking in the XMPP world.
248
+ if evt.Info.Chat.User == types.StatusBroadcastJID.User || message.IsCarbon {
249
+ return EventUnknown, nil
250
+ }
251
+ } else if evt.Info.IsGroup {
252
+ message.GroupJID = evt.Info.Chat.ToNonAD().String()
253
+ } else if message.IsCarbon {
254
+ message.JID = evt.Info.Chat.ToNonAD().String()
255
+ }
256
+
257
+ // Handle handle protocol messages (such as message deletion or editing).
258
+ if p := evt.Message.GetProtocolMessage(); p != nil {
259
+ switch p.GetType() {
260
+ case waE2E.ProtocolMessage_MESSAGE_EDIT:
261
+ if m := p.GetEditedMessage(); m != nil {
262
+ message.Kind = MessageEdit
263
+ message.ID = p.Key.GetID()
264
+ message.Body = m.GetConversation()
265
+ } else {
266
+ return EventUnknown, nil
267
+ }
268
+ case waE2E.ProtocolMessage_REVOKE:
269
+ message.Kind = MessageRevoke
270
+ message.ID = p.Key.GetID()
271
+ message.OriginJID = p.Key.GetParticipant()
272
+ return EventMessage, &EventPayload{Message: message}
273
+ }
274
+ }
275
+
276
+ // Handle emoji reaction to existing message.
277
+ if r := evt.Message.GetReactionMessage(); r != nil {
278
+ message.Kind = MessageReaction
279
+ message.ID = r.Key.GetID()
280
+ message.Body = r.GetText()
281
+ return EventMessage, &EventPayload{Message: message}
282
+ }
283
+
284
+ // Handle location (static and live) message.
285
+ if l := evt.Message.GetLocationMessage(); l != nil {
286
+ message.Location = Location{
287
+ Latitude: l.GetDegreesLatitude(),
288
+ Longitude: l.GetDegreesLongitude(),
289
+ Accuracy: int(l.GetAccuracyInMeters()),
290
+ IsLive: l.GetIsLive(),
291
+ Name: l.GetName(),
292
+ Address: l.GetAddress(),
293
+ URL: l.GetURL(),
294
+ }
295
+ return EventMessage, &EventPayload{Message: message}
296
+ }
297
+
298
+ if l := evt.Message.GetLiveLocationMessage(); l != nil {
299
+ message.Body = l.GetCaption()
300
+ message.Location = Location{
301
+ Latitude: l.GetDegreesLatitude(),
302
+ Longitude: l.GetDegreesLongitude(),
303
+ Accuracy: int(l.GetAccuracyInMeters()),
304
+ IsLive: true,
305
+ }
306
+ return EventMessage, &EventPayload{Message: message}
307
+ }
308
+
309
+ // Handle message attachments, if any.
310
+ if attach, context, err := getMessageAttachments(client, evt.Message); err != nil {
311
+ client.Log.Errorf("Failed getting message attachments: %s", err)
312
+ return EventUnknown, nil
313
+ } else if len(attach) > 0 {
314
+ message.Attachments = append(message.Attachments, attach...)
315
+ message.Kind = MessageAttachment
316
+ if context != nil {
317
+ message = getMessageWithContext(message, context)
318
+ }
319
+ }
320
+
321
+ // Get extended information from message, if available. Extended messages typically represent
322
+ // messages with additional context, such as replies, forwards, etc.
323
+ if e := evt.Message.GetExtendedTextMessage(); e != nil {
324
+ if message.Body == "" {
325
+ message.Body = e.GetText()
326
+ }
327
+
328
+ message = getMessageWithContext(message, e.GetContextInfo())
329
+ }
330
+
331
+ // Ignore obviously invalid messages.
332
+ if message.Kind == MessagePlain && message.Body == "" {
333
+ return EventUnknown, nil
334
+ }
335
+
336
+ return EventMessage, &EventPayload{Message: message}
337
+ }
338
+
339
+ // GetMessageWithContext processes the given [Message] and applies any context metadata might be
340
+ // useful; examples of context include messages being quoted. If no context is found, the original
341
+ // message is returned unchanged.
342
+ func getMessageWithContext(message Message, info *waE2E.ContextInfo) Message {
343
+ if info == nil {
344
+ return message
345
+ }
346
+
347
+ message.ReplyID = info.GetStanzaID()
348
+ message.OriginJID = info.GetParticipant()
349
+ message.IsForwarded = info.GetIsForwarded()
350
+
351
+ if q := info.GetQuotedMessage(); q != nil {
352
+ if qe := q.GetExtendedTextMessage(); qe != nil {
353
+ message.ReplyBody = qe.GetText()
354
+ } else {
355
+ message.ReplyBody = q.GetConversation()
356
+ }
357
+ }
358
+
359
+ return message
360
+ }
361
+
362
+ // GetMessageAttachments fetches and decrypts attachments (images, audio, video, or documents) sent
363
+ // via WhatsApp. Any failures in retrieving any attachment will return an error immediately.
364
+ func getMessageAttachments(client *whatsmeow.Client, message *waE2E.Message) ([]Attachment, *waE2E.ContextInfo, error) {
365
+ var result []Attachment
366
+ var info *waE2E.ContextInfo
367
+ var convertSpec *media.Spec
368
+ var kinds = []whatsmeow.DownloadableMessage{
369
+ message.GetImageMessage(),
370
+ message.GetAudioMessage(),
371
+ message.GetVideoMessage(),
372
+ message.GetDocumentMessage(),
373
+ message.GetStickerMessage(),
374
+ }
375
+
376
+ for _, msg := range kinds {
377
+ // Handle data for specific attachment type.
378
+ var a Attachment
379
+ switch msg := msg.(type) {
380
+ case *waE2E.ImageMessage:
381
+ a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
382
+ case *waE2E.AudioMessage:
383
+ // Convert Opus-encoded voice messages to AAC-encoded audio, which has better support.
384
+ a.MIME = msg.GetMimetype()
385
+ if msg.GetPTT() {
386
+ convertSpec = &media.Spec{MIME: media.TypeM4A}
387
+ }
388
+ case *waE2E.VideoMessage:
389
+ a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
390
+ case *waE2E.DocumentMessage:
391
+ a.MIME, a.Caption, a.Filename = msg.GetMimetype(), msg.GetCaption(), msg.GetFileName()
392
+ case *waE2E.StickerMessage:
393
+ a.MIME = msg.GetMimetype()
394
+ }
395
+
396
+ // Ignore attachments with empty or unknown MIME types.
397
+ if a.MIME == "" {
398
+ continue
399
+ }
400
+
401
+ // Attempt to download and decrypt raw attachment data, if any.
402
+ data, err := client.Download(msg)
403
+ if err != nil {
404
+ return nil, nil, err
405
+ }
406
+
407
+ a.Data = data
408
+
409
+ // Convert incoming data if a specification has been given, ignoring any errors that occur.
410
+ if convertSpec != nil {
411
+ data, err = media.Convert(context.Background(), a.Data, convertSpec)
412
+ if err == nil {
413
+ a.Data, a.MIME = data, string(convertSpec.MIME)
414
+ }
415
+ }
416
+
417
+ // Set filename from SHA256 checksum and MIME type, if none is already set.
418
+ if a.Filename == "" {
419
+ a.Filename = fmt.Sprintf("%x%s", msg.GetFileSHA256(), extensionByType(a.MIME))
420
+ }
421
+
422
+ result = append(result, a)
423
+ }
424
+
425
+ // Handle any contact vCard as attachment.
426
+ if c := message.GetContactMessage(); c != nil {
427
+ result = append(result, Attachment{
428
+ MIME: "text/vcard",
429
+ Filename: c.GetDisplayName() + ".vcf",
430
+ Data: []byte(c.GetVcard()),
431
+ })
432
+ info = c.GetContextInfo()
433
+ }
434
+
435
+ return result, info, nil
436
+ }
437
+
438
+ const (
439
+ // The MIME type used by voice messages on WhatsApp.
440
+ voiceMessageMIME = string(media.TypeOgg) + "; codecs=opus"
441
+ // The MIME type used by animated images on WhatsApp.
442
+ animatedImageMIME = "image/gif"
443
+
444
+ // The maximum image attachment size we'll attempt to process in any way, in bytes.
445
+ maxConvertImageSize = 1024 * 1024 * 10 // 10MiB
446
+ // The maximum audio/video attachment size we'll attempt to process in any way, in bytes.
447
+ maxConvertAudioVideoSize = 1024 * 1024 * 20 // 20MiB
448
+
449
+ // The maximum number of samples to return in media waveforms.
450
+ maxWaveformSamples = 64
451
+ )
452
+
453
+ var (
454
+ // Default target specification for voice messages.
455
+ voiceMessageSpec = media.Spec{
456
+ MIME: media.MIMEType(voiceMessageMIME),
457
+ AudioBitRate: 64,
458
+ AudioChannels: 1,
459
+ AudioSampleRate: 48000,
460
+ StripMetadata: true,
461
+ }
462
+
463
+ // Default target specification for generic audio messages.
464
+ audioMessageSpec = media.Spec{
465
+ MIME: media.TypeM4A,
466
+ AudioBitRate: 160,
467
+ AudioSampleRate: 44100,
468
+ }
469
+
470
+ // Default target specification for video messages with inline preview.
471
+ videoMessageSpec = media.Spec{
472
+ MIME: media.TypeMP4,
473
+ AudioBitRate: 160,
474
+ AudioSampleRate: 44100,
475
+ VideoFilter: "pad=ceil(iw/2)*2:ceil(ih/2)*2",
476
+ VideoFrameRate: 25,
477
+ VideoPixelFormat: "yuv420p",
478
+ StripMetadata: true,
479
+ }
480
+
481
+ // Default target specification for image messages with inline preview.
482
+ imageMessageSpec = media.Spec{
483
+ MIME: media.TypeJPEG,
484
+ ImageQuality: 85,
485
+ }
486
+
487
+ // Default target specifications for default and preview-size thumbnails.
488
+ defaultThumbnailSpec = media.Spec{
489
+ MIME: media.TypeJPEG,
490
+ ImageWidth: 100,
491
+ StripMetadata: true,
492
+ }
493
+ previewThumbnailSpec = media.Spec{
494
+ MIME: media.TypeJPEG,
495
+ ImageWidth: 250,
496
+ StripMetadata: true,
497
+ }
498
+ )
499
+
500
+ // ConvertAttachment attempts to process a given attachment from a less-supported type to a
501
+ // canonically supported one; for example, from `image/png` to `image/jpeg`.
502
+ //
503
+ // Decisions about which MIME types to convert to are based on the concrete MIME type inferred from
504
+ // the file itself, and care is taken to conform to WhatsApp semantics for the given input MIME
505
+ // type.
506
+ //
507
+ // If the input MIME type is unknown, or conversion is impossible, the given attachment is not
508
+ // changed.
509
+ func convertAttachment(attach *Attachment) error {
510
+ var detectedMIME media.MIMEType
511
+ if t := media.DetectMIMEType(attach.Data); t != media.TypeUnknown {
512
+ detectedMIME = t
513
+ if attach.MIME == "" || attach.MIME == "application/octet-stream" {
514
+ attach.MIME = string(detectedMIME)
515
+ }
516
+ }
517
+
518
+ var spec media.Spec
519
+ var ctx = context.Background()
520
+
521
+ switch detectedMIME {
522
+ case media.TypePNG, media.TypeWebP:
523
+ // Convert common image formats to JPEG for inline preview.
524
+ if len(attach.Data) > maxConvertImageSize {
525
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
526
+ }
527
+
528
+ spec = imageMessageSpec
529
+ case media.TypeGIF:
530
+ // Convert GIFs to JPEG or MP4, if animated, as required by WhatsApp.
531
+ if len(attach.Data) > maxConvertImageSize {
532
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
533
+ }
534
+
535
+ spec = imageMessageSpec
536
+ if s, err := attach.GetSpec(ctx); err == nil && s.ImageFrameRate > 0 {
537
+ spec = videoMessageSpec
538
+ spec.ImageFrameRate = s.ImageFrameRate
539
+ }
540
+ case media.TypeM4A:
541
+ if len(attach.Data) > maxConvertAudioVideoSize {
542
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
543
+ }
544
+
545
+ spec = voiceMessageSpec
546
+
547
+ if s, err := attach.GetSpec(ctx); err == nil {
548
+ if s.AudioCodec == "alac" {
549
+ // Don't attempt to process lossless files at all, as it's assumed that the sender
550
+ // wants to retain these characteristics. Since WhatsApp will try (and likely fail)
551
+ // to process this as an audio message anyways, set a unique MIME type.
552
+ attach.MIME = "application/octet-stream"
553
+ return nil
554
+ }
555
+ }
556
+ case media.TypeOgg:
557
+ if len(attach.Data) > maxConvertAudioVideoSize {
558
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
559
+ }
560
+
561
+ spec = audioMessageSpec
562
+ if s, err := attach.GetSpec(ctx); err == nil {
563
+ if s.AudioCodec == "opus" {
564
+ // Assume that Opus-encoded Ogg files are meant to be voice messages, and re-encode
565
+ // them as such for WhatsApp.
566
+ spec = voiceMessageSpec
567
+ }
568
+ }
569
+ case media.TypeMP4, media.TypeWebM:
570
+ if len(attach.Data) > maxConvertAudioVideoSize {
571
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
572
+ }
573
+
574
+ spec = videoMessageSpec
575
+
576
+ if s, err := attach.GetSpec(ctx); err == nil {
577
+ // Try to see if there's a video stream for ostensibly video-related MIME types, as
578
+ // these are some times misdetected as such.
579
+ if s.VideoWidth == 0 && s.VideoHeight == 0 && s.AudioSampleRate > 0 && s.Duration > 0 {
580
+ spec = voiceMessageSpec
581
+ }
582
+ }
583
+ default:
584
+ // Detected source MIME not in list we're willing to convert, move on without error.
585
+ return nil
586
+ }
587
+
588
+ // Convert attachment between file-types, if source MIME matches the known list of convertable types.
589
+ data, err := media.Convert(ctx, attach.Data, &spec)
590
+ if err != nil {
591
+ return fmt.Errorf("failed converting attachment: %w", err)
592
+ }
593
+
594
+ attach.Data, attach.MIME = data, string(spec.MIME)
595
+ return nil
596
+ }
597
+
598
+ // KnownMediaTypes represents MIME type to WhatsApp media types known to be handled by WhatsApp in a
599
+ // special way (that is, not as generic file uploads).
600
+ var knownMediaTypes = map[string]whatsmeow.MediaType{
601
+ "image/jpeg": whatsmeow.MediaImage,
602
+ "audio/mpeg": whatsmeow.MediaAudio,
603
+ "audio/mp4": whatsmeow.MediaAudio,
604
+ "audio/aac": whatsmeow.MediaAudio,
605
+ "audio/ogg": whatsmeow.MediaAudio,
606
+ "video/mp4": whatsmeow.MediaVideo,
607
+ }
608
+
609
+ // UploadAttachment attempts to push the given attachment data to WhatsApp according to the MIME
610
+ // type specified within. Attachments are handled as generic file uploads unless they're of a
611
+ // specific format; in addition, certain MIME types may be automatically converted to a
612
+ // well-supported type via FFmpeg (if available).
613
+ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Message, error) {
614
+ var ctx = context.Background()
615
+ var originalMIME = attach.MIME
616
+
617
+ if err := convertAttachment(attach); err != nil {
618
+ client.Log.Warnf("failed to auto-convert attachment: %s", err)
619
+ }
620
+
621
+ mediaType := knownMediaTypes[getBaseMediaType(attach.MIME)]
622
+ if mediaType == "" {
623
+ mediaType = whatsmeow.MediaDocument
624
+ }
625
+
626
+ if len(attach.Data) == 0 {
627
+ return nil, fmt.Errorf("attachment file contains no data")
628
+ }
629
+
630
+ upload, err := client.Upload(ctx, attach.Data, mediaType)
631
+ if err != nil {
632
+ return nil, err
633
+ }
634
+
635
+ var message *waE2E.Message
636
+ switch mediaType {
637
+ case whatsmeow.MediaImage:
638
+ message = &waE2E.Message{
639
+ ImageMessage: &waE2E.ImageMessage{
640
+ URL: &upload.URL,
641
+ DirectPath: &upload.DirectPath,
642
+ MediaKey: upload.MediaKey,
643
+ Mimetype: &attach.MIME,
644
+ FileEncSHA256: upload.FileEncSHA256,
645
+ FileSHA256: upload.FileSHA256,
646
+ FileLength: ptrTo(uint64(len(attach.Data))),
647
+ },
648
+ }
649
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
650
+ if err != nil {
651
+ client.Log.Warnf("failed generating attachment thumbnail: %s", err)
652
+ } else {
653
+ message.ImageMessage.JPEGThumbnail = t
654
+ }
655
+ case whatsmeow.MediaAudio:
656
+ spec, err := attach.GetSpec(ctx)
657
+ if err != nil {
658
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
659
+ spec = &media.Spec{}
660
+ }
661
+ message = &waE2E.Message{
662
+ AudioMessage: &waE2E.AudioMessage{
663
+ URL: &upload.URL,
664
+ DirectPath: &upload.DirectPath,
665
+ MediaKey: upload.MediaKey,
666
+ Mimetype: &attach.MIME,
667
+ FileEncSHA256: upload.FileEncSHA256,
668
+ FileSHA256: upload.FileSHA256,
669
+ FileLength: ptrTo(uint64(len(attach.Data))),
670
+ Seconds: ptrTo(uint32(spec.Duration.Seconds())),
671
+ },
672
+ }
673
+ if attach.MIME == voiceMessageMIME {
674
+ message.AudioMessage.PTT = ptrTo(true)
675
+ if spec != nil {
676
+ w, err := media.GetWaveform(ctx, attach.Data, spec, maxWaveformSamples)
677
+ if err != nil {
678
+ client.Log.Warnf("failed generating attachment waveform: %s", err)
679
+ } else {
680
+ message.AudioMessage.Waveform = w
681
+ }
682
+ }
683
+ }
684
+ case whatsmeow.MediaVideo:
685
+ spec, err := attach.GetSpec(ctx)
686
+ if err != nil {
687
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
688
+ spec = &media.Spec{}
689
+ }
690
+ message = &waE2E.Message{
691
+ VideoMessage: &waE2E.VideoMessage{
692
+ URL: &upload.URL,
693
+ DirectPath: &upload.DirectPath,
694
+ MediaKey: upload.MediaKey,
695
+ Mimetype: &attach.MIME,
696
+ FileEncSHA256: upload.FileEncSHA256,
697
+ FileSHA256: upload.FileSHA256,
698
+ FileLength: ptrTo(uint64(len(attach.Data))),
699
+ Seconds: ptrTo(uint32(spec.Duration.Seconds())),
700
+ Width: ptrTo(uint32(spec.VideoWidth)),
701
+ Height: ptrTo(uint32(spec.VideoHeight)),
702
+ },
703
+ }
704
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
705
+ if err != nil {
706
+ client.Log.Warnf("failed generating attachment thumbnail: %s", err)
707
+ } else {
708
+ message.VideoMessage.JPEGThumbnail = t
709
+ }
710
+ if originalMIME == animatedImageMIME {
711
+ message.VideoMessage.GifPlayback = ptrTo(true)
712
+ }
713
+ case whatsmeow.MediaDocument:
714
+ message = &waE2E.Message{
715
+ DocumentMessage: &waE2E.DocumentMessage{
716
+ URL: &upload.URL,
717
+ DirectPath: &upload.DirectPath,
718
+ MediaKey: upload.MediaKey,
719
+ Mimetype: &attach.MIME,
720
+ FileEncSHA256: upload.FileEncSHA256,
721
+ FileSHA256: upload.FileSHA256,
722
+ FileLength: ptrTo(uint64(len(attach.Data))),
723
+ FileName: &attach.Filename,
724
+ },
725
+ }
726
+ switch media.MIMEType(attach.MIME) {
727
+ case media.TypePDF:
728
+ if spec, err := attach.GetSpec(ctx); err != nil {
729
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
730
+ } else {
731
+ message.DocumentMessage.PageCount = ptrTo(uint32(spec.DocumentPage))
732
+ }
733
+ t, err := media.Convert(ctx, attach.Data, &previewThumbnailSpec)
734
+ if err != nil {
735
+ client.Log.Warnf("failed generating attachment thumbnail: %s", err)
736
+ } else {
737
+ message.DocumentMessage.JPEGThumbnail = t
738
+ }
739
+ }
740
+ }
741
+
742
+ return message, nil
743
+ }
744
+
745
+ // KnownExtensions represents MIME type to file-extension mappings for basic, known media types.
746
+ var knownExtensions = map[string]string{
747
+ "image/jpeg": ".jpg",
748
+ "audio/ogg": ".oga",
749
+ "audio/mp4": ".m4a",
750
+ "video/mp4": ".mp4",
751
+ }
752
+
753
+ // ExtensionByType returns the file extension for the given MIME type, or a generic extension if the
754
+ // MIME type is unknown.
755
+ func extensionByType(typ string) string {
756
+ // Handle common, known MIME types first.
757
+ if ext := knownExtensions[typ]; ext != "" {
758
+ return ext
759
+ }
760
+ if ext, _ := mime.ExtensionsByType(typ); len(ext) > 0 {
761
+ return ext[0]
762
+ }
763
+ return ".bin"
764
+ }
765
+
766
+ // GetBaseMediaType returns the media type without any additional parameters.
767
+ func getBaseMediaType(typ string) string {
768
+ return strings.SplitN(typ, ";", 2)[0]
769
+ }
770
+
771
+ // NewEventFromHistory returns event data meant for [Session.propagateEvent] for the primive history
772
+ // message given. Currently, only events related to group-chats will be handled, due to uncertain
773
+ // support for history back-fills on 1:1 chats.
774
+ //
775
+ // Otherwise, the implementation largely follows that of [newMessageEvent], however the base types
776
+ // used by these two functions differ in many small ways which prevent unifying the approach.
777
+ //
778
+ // Typically, this will return [EventMessage] events with appropriate [Message] payloads; unknown or
779
+ // invalid messages will return an [EventUnknown] event with nil data.
780
+ func newEventFromHistory(client *whatsmeow.Client, info *waWeb.WebMessageInfo) (EventKind, *EventPayload) {
781
+ // Handle message as group message is remote JID is a group JID in the absence of any other,
782
+ // specific signal, or don't handle at all if no group JID is found.
783
+ var jid = info.GetKey().GetRemoteJID()
784
+ if j, _ := types.ParseJID(jid); j.Server != types.GroupServer {
785
+ return EventUnknown, nil
786
+ }
787
+
788
+ // Set basic data for message, to be potentially amended depending on the concrete version of
789
+ // the underlying message.
790
+ var message = Message{
791
+ Kind: MessagePlain,
792
+ ID: info.GetKey().GetID(),
793
+ GroupJID: info.GetKey().GetRemoteJID(),
794
+ Body: info.GetMessage().GetConversation(),
795
+ Timestamp: int64(info.GetMessageTimestamp()),
796
+ IsCarbon: info.GetKey().GetFromMe(),
797
+ }
798
+
799
+ if info.Participant != nil {
800
+ message.JID = info.GetParticipant()
801
+ } else if info.GetKey().GetFromMe() {
802
+ message.JID = client.Store.ID.ToNonAD().String()
803
+ } else {
804
+ // It's likely we cannot handle this message correctly if we don't know the concrete
805
+ // sender, so just ignore it completely.
806
+ return EventUnknown, nil
807
+ }
808
+
809
+ // Handle handle protocol messages (such as message deletion or editing), while ignoring known
810
+ // unhandled types.
811
+ switch info.GetMessageStubType() {
812
+ case waWeb.WebMessageInfo_CIPHERTEXT:
813
+ return EventUnknown, nil
814
+ case waWeb.WebMessageInfo_CALL_MISSED_VOICE, waWeb.WebMessageInfo_CALL_MISSED_VIDEO:
815
+ return EventCall, &EventPayload{Call: Call{
816
+ State: CallMissed,
817
+ JID: info.GetKey().GetRemoteJID(),
818
+ Timestamp: int64(info.GetMessageTimestamp()),
819
+ }}
820
+ case waWeb.WebMessageInfo_REVOKE:
821
+ if p := info.GetMessageStubParameters(); len(p) > 0 {
822
+ message.Kind = MessageRevoke
823
+ message.ID = p[0]
824
+ return EventMessage, &EventPayload{Message: message}
825
+ } else {
826
+ return EventUnknown, nil
827
+ }
828
+ }
829
+
830
+ // Handle emoji reaction to existing message.
831
+ for _, r := range info.GetReactions() {
832
+ if r.GetText() != "" {
833
+ message.Reactions = append(message.Reactions, Message{
834
+ Kind: MessageReaction,
835
+ ID: r.GetKey().GetID(),
836
+ JID: r.GetKey().GetRemoteJID(),
837
+ Body: r.GetText(),
838
+ Timestamp: r.GetSenderTimestampMS() / 1000,
839
+ IsCarbon: r.GetKey().GetFromMe(),
840
+ })
841
+ }
842
+ }
843
+
844
+ // Handle message attachments, if any.
845
+ if attach, context, err := getMessageAttachments(client, info.GetMessage()); err != nil {
846
+ client.Log.Errorf("Failed getting message attachments: %s", err)
847
+ return EventUnknown, nil
848
+ } else if len(attach) > 0 {
849
+ message.Attachments = append(message.Attachments, attach...)
850
+ message.Kind = MessageAttachment
851
+ if context != nil {
852
+ message = getMessageWithContext(message, context)
853
+ }
854
+ }
855
+
856
+ // Handle pre-set receipt status, if any.
857
+ for _, r := range info.GetUserReceipt() {
858
+ // Ignore self-receipts for the moment, as these cannot be handled correctly by the adapter.
859
+ if client.Store.ID.ToNonAD().String() == r.GetUserJID() {
860
+ continue
861
+ }
862
+ var receipt = Receipt{MessageIDs: []string{message.ID}, JID: r.GetUserJID(), GroupJID: message.GroupJID}
863
+ switch info.GetStatus() {
864
+ case waWeb.WebMessageInfo_DELIVERY_ACK:
865
+ receipt.Kind = ReceiptDelivered
866
+ receipt.Timestamp = r.GetReceiptTimestamp()
867
+ case waWeb.WebMessageInfo_READ:
868
+ receipt.Kind = ReceiptRead
869
+ receipt.Timestamp = r.GetReadTimestamp()
870
+ }
871
+ message.Receipts = append(message.Receipts, receipt)
872
+ }
873
+
874
+ // Get extended information from message, if available. Extended messages typically represent
875
+ // messages with additional context, such as replies, forwards, etc.
876
+ if e := info.GetMessage().GetExtendedTextMessage(); e != nil {
877
+ if message.Body == "" {
878
+ message.Body = e.GetText()
879
+ }
880
+
881
+ message = getMessageWithContext(message, e.GetContextInfo())
882
+ }
883
+
884
+ // Ignore obviously invalid messages.
885
+ if message.Kind == MessagePlain && message.Body == "" {
886
+ return EventUnknown, nil
887
+ }
888
+
889
+ return EventMessage, &EventPayload{Message: message}
890
+ }
891
+
892
+ // ChatStateKind represents the different kinds of chat-states possible in WhatsApp.
893
+ type ChatStateKind int
894
+
895
+ // The chat states handled by the overarching session event handler.
896
+ const (
897
+ ChatStateUnknown ChatStateKind = iota
898
+ ChatStateComposing
899
+ ChatStatePaused
900
+ )
901
+
902
+ // A ChatState represents the activity of a contact within a certain discussion, for instance,
903
+ // whether the contact is currently composing a message. This is separate to the concept of a
904
+ // Presence, which is the contact's general state across all discussions.
905
+ type ChatState struct {
906
+ Kind ChatStateKind
907
+ JID string
908
+ GroupJID string
909
+ }
910
+
911
+ // NewChatStateEvent returns event data meant for [Session.propagateEvent] for the primitive
912
+ // chat-state event given.
913
+ func newChatStateEvent(evt *events.ChatPresence) (EventKind, *EventPayload) {
914
+ var state = ChatState{JID: evt.MessageSource.Sender.ToNonAD().String()}
915
+ if evt.MessageSource.IsGroup {
916
+ state.GroupJID = evt.MessageSource.Chat.ToNonAD().String()
917
+ }
918
+ switch evt.State {
919
+ case types.ChatPresenceComposing:
920
+ state.Kind = ChatStateComposing
921
+ case types.ChatPresencePaused:
922
+ state.Kind = ChatStatePaused
923
+ }
924
+ return EventChatState, &EventPayload{ChatState: state}
925
+ }
926
+
927
+ // ReceiptKind represents the different types of delivery receipts possible in WhatsApp.
928
+ type ReceiptKind int
929
+
930
+ // The delivery receipts handled by the overarching session event handler.
931
+ const (
932
+ ReceiptUnknown ReceiptKind = iota
933
+ ReceiptDelivered
934
+ ReceiptRead
935
+ )
936
+
937
+ // A Receipt represents a notice of delivery or presentation for [Message] instances sent or
938
+ // received. Receipts can be delivered for many messages at once, but are generally all delivered
939
+ // under one specific state at a time.
940
+ type Receipt struct {
941
+ Kind ReceiptKind // The distinct kind of receipt presented.
942
+ MessageIDs []string // The list of message IDs to mark for receipt.
943
+ JID string
944
+ GroupJID string
945
+ Timestamp int64
946
+ IsCarbon bool
947
+ }
948
+
949
+ // NewReceiptEvent returns event data meant for [Session.propagateEvent] for the primive receipt
950
+ // event given. Unknown or invalid receipts will return an [EventUnknown] event with nil data.
951
+ func newReceiptEvent(evt *events.Receipt) (EventKind, *EventPayload) {
952
+ var receipt = Receipt{
953
+ MessageIDs: append([]string{}, evt.MessageIDs...),
954
+ JID: evt.MessageSource.Sender.ToNonAD().String(),
955
+ Timestamp: evt.Timestamp.Unix(),
956
+ IsCarbon: evt.MessageSource.IsFromMe,
957
+ }
958
+
959
+ if len(receipt.MessageIDs) == 0 {
960
+ return EventUnknown, nil
961
+ }
962
+
963
+ if evt.MessageSource.Chat.Server == types.BroadcastServer {
964
+ receipt.JID = evt.MessageSource.BroadcastListOwner.ToNonAD().String()
965
+ } else if evt.MessageSource.IsGroup {
966
+ receipt.GroupJID = evt.MessageSource.Chat.ToNonAD().String()
967
+ } else if receipt.IsCarbon {
968
+ receipt.JID = evt.MessageSource.Chat.ToNonAD().String()
969
+ }
970
+
971
+ switch evt.Type {
972
+ case types.ReceiptTypeDelivered:
973
+ receipt.Kind = ReceiptDelivered
974
+ case types.ReceiptTypeRead:
975
+ receipt.Kind = ReceiptRead
976
+ }
977
+
978
+ return EventReceipt, &EventPayload{Receipt: receipt}
979
+ }
980
+
981
+ // GroupAffiliation represents the set of privilidges given to a specific participant in a group.
982
+ type GroupAffiliation int
983
+
984
+ const (
985
+ GroupAffiliationNone GroupAffiliation = iota // None, or normal member group affiliation.
986
+ GroupAffiliationAdmin // Can perform some management operations.
987
+ GroupAffiliationOwner // Can manage group fully, including destroying the group.
988
+ )
989
+
990
+ // A Group represents a named, many-to-many chat space which may be joined or left at will. All
991
+ // fields apart from the group JID are considered to be optional, and may not be set in cases where
992
+ // group information is being updated against previous assumed state. Groups in WhatsApp are
993
+ // generally invited to out-of-band with respect to overarching adaptor; see the documentation for
994
+ // [Session.GetGroups] for more information.
995
+ type Group struct {
996
+ JID string // The WhatsApp JID for this group.
997
+ Name string // The user-defined, human-readable name for this group.
998
+ Subject GroupSubject // The longer-form, user-defined description for this group.
999
+ Nickname string // Our own nickname in this group-chat.
1000
+ Participants []GroupParticipant // The list of participant contacts for this group, including ourselves.
1001
+ }
1002
+
1003
+ // A GroupSubject represents the user-defined group description and attached metadata thereof, for a
1004
+ // given [Group].
1005
+ type GroupSubject struct {
1006
+ Subject string // The user-defined group description.
1007
+ SetAt int64 // The exact time this group description was set at, as a timestamp.
1008
+ SetByJID string // The JID of the user that set the subject.
1009
+ }
1010
+
1011
+ // GroupParticipantAction represents the distinct set of actions that can be taken when encountering
1012
+ // a group participant, typically to add or remove.
1013
+ type GroupParticipantAction int
1014
+
1015
+ const (
1016
+ GroupParticipantActionAdd GroupParticipantAction = iota // Default action; add participant to list.
1017
+ GroupParticipantActionRemove // Remove participant from list, if existing.
1018
+ GroupParticipantActionPromote // Make group member into administrator.
1019
+ GroupParticipantActionDemote // Make group administrator into member.
1020
+ )
1021
+
1022
+ // ToParticipantChange converts our public [GroupParticipantAction] to the internal [ParticipantChange]
1023
+ // representation.
1024
+ func (a GroupParticipantAction) toParticipantChange() whatsmeow.ParticipantChange {
1025
+ switch a {
1026
+ case GroupParticipantActionRemove:
1027
+ return whatsmeow.ParticipantChangeRemove
1028
+ case GroupParticipantActionPromote:
1029
+ return whatsmeow.ParticipantChangePromote
1030
+ case GroupParticipantActionDemote:
1031
+ return whatsmeow.ParticipantChangeDemote
1032
+ default:
1033
+ return whatsmeow.ParticipantChangeAdd
1034
+ }
1035
+ }
1036
+
1037
+ // A GroupParticipant represents a contact who is currently joined in a given group. Participants in
1038
+ // WhatsApp can always be derived back to their individual [Contact]; there are no anonymous groups
1039
+ // in WhatsApp.
1040
+ type GroupParticipant struct {
1041
+ JID string // The WhatsApp JID for this participant.
1042
+ Affiliation GroupAffiliation // The set of priviledges given to this specific participant.
1043
+ Action GroupParticipantAction // The specific action to take for this participant; typically to add.
1044
+ }
1045
+
1046
+ // NewGroupParticipant returns a [GroupParticipant], filling fields from the internal participant
1047
+ // type. This is a no-op if [types.GroupParticipant.Error] is non-zero, and other fields may only
1048
+ // be set optionally.
1049
+ func newGroupParticipant(p types.GroupParticipant) GroupParticipant {
1050
+ if p.Error > 0 {
1051
+ return GroupParticipant{}
1052
+ }
1053
+ var affiliation = GroupAffiliationNone
1054
+ if p.IsSuperAdmin {
1055
+ affiliation = GroupAffiliationOwner
1056
+ } else if p.IsAdmin {
1057
+ affiliation = GroupAffiliationAdmin
1058
+ }
1059
+ return GroupParticipant{
1060
+ JID: p.JID.ToNonAD().String(),
1061
+ Affiliation: affiliation,
1062
+ }
1063
+ }
1064
+
1065
+ // NewGroupEvent returns event data meant for [Session.propagateEvent] for the primive group event
1066
+ // given. Group data returned by this function can be partial, and callers should take care to only
1067
+ // handle non-empty values.
1068
+ func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
1069
+ var group = Group{JID: evt.JID.ToNonAD().String()}
1070
+ if evt.Name != nil {
1071
+ group.Name = evt.Name.Name
1072
+ }
1073
+ if evt.Topic != nil {
1074
+ group.Subject = GroupSubject{
1075
+ Subject: evt.Topic.Topic,
1076
+ SetAt: evt.Topic.TopicSetAt.Unix(),
1077
+ SetByJID: evt.Topic.TopicSetBy.ToNonAD().String(),
1078
+ }
1079
+ }
1080
+ for _, p := range evt.Join {
1081
+ group.Participants = append(group.Participants, GroupParticipant{
1082
+ JID: p.ToNonAD().String(),
1083
+ Action: GroupParticipantActionAdd,
1084
+ })
1085
+ }
1086
+ for _, p := range evt.Leave {
1087
+ group.Participants = append(group.Participants, GroupParticipant{
1088
+ JID: p.ToNonAD().String(),
1089
+ Action: GroupParticipantActionRemove,
1090
+ })
1091
+ }
1092
+ for _, p := range evt.Promote {
1093
+ group.Participants = append(group.Participants, GroupParticipant{
1094
+ JID: p.ToNonAD().String(),
1095
+ Action: GroupParticipantActionPromote,
1096
+ Affiliation: GroupAffiliationAdmin,
1097
+ })
1098
+ }
1099
+ for _, p := range evt.Demote {
1100
+ group.Participants = append(group.Participants, GroupParticipant{
1101
+ JID: p.ToNonAD().String(),
1102
+ Action: GroupParticipantActionDemote,
1103
+ Affiliation: GroupAffiliationNone,
1104
+ })
1105
+ }
1106
+ return EventGroup, &EventPayload{Group: group}
1107
+ }
1108
+
1109
+ // NewGroup returns a concrete [Group] for the primitive data given. This function will generally
1110
+ // populate fields with as much data as is available from the remote, and is therefore should not
1111
+ // be called when partial data is to be returned.
1112
+ func newGroup(client *whatsmeow.Client, info *types.GroupInfo) Group {
1113
+ var participants []GroupParticipant
1114
+ for i := range info.Participants {
1115
+ p := newGroupParticipant(info.Participants[i])
1116
+ if p.JID == "" {
1117
+ continue
1118
+ }
1119
+ participants = append(participants, p)
1120
+ }
1121
+ return Group{
1122
+ JID: info.JID.ToNonAD().String(),
1123
+ Name: info.GroupName.Name,
1124
+ Subject: GroupSubject{
1125
+ Subject: info.Topic,
1126
+ SetAt: info.TopicSetAt.Unix(),
1127
+ SetByJID: info.TopicSetBy.ToNonAD().String(),
1128
+ },
1129
+ Nickname: client.Store.PushName,
1130
+ Participants: participants,
1131
+ }
1132
+ }
1133
+
1134
+ // CallState represents the state of the call to synchronize with.
1135
+ type CallState int
1136
+
1137
+ // The call states handled by the overarching session event handler.
1138
+ const (
1139
+ CallUnknown CallState = iota
1140
+ CallIncoming
1141
+ CallMissed
1142
+ )
1143
+
1144
+ // CallStateFromReason converts the given (internal) reason string to a public [CallState]. Calls
1145
+ // given invalid or unknown reasons will return the [CallUnknown] state.
1146
+ func callStateFromReason(reason string) CallState {
1147
+ switch reason {
1148
+ case "", "timeout":
1149
+ return CallMissed
1150
+ default:
1151
+ return CallUnknown
1152
+ }
1153
+ }
1154
+
1155
+ // A Call represents an incoming or outgoing voice/video call made over WhatsApp. Full support for
1156
+ // calls is currently not implemented, and this structure contains the bare minimum data required
1157
+ // for notifying on missed calls.
1158
+ type Call struct {
1159
+ State CallState
1160
+ JID string
1161
+ Timestamp int64
1162
+ }
1163
+
1164
+ // NewCallEvent returns event data meant for [Session.propagateEvent] for the call metadata given.
1165
+ func newCallEvent(state CallState, meta types.BasicCallMeta) (EventKind, *EventPayload) {
1166
+ if state == CallUnknown || meta.From.IsEmpty() {
1167
+ return EventUnknown, nil
1168
+ }
1169
+
1170
+ return EventCall, &EventPayload{Call: Call{
1171
+ State: state,
1172
+ JID: meta.From.ToNonAD().String(),
1173
+ Timestamp: meta.Timestamp.Unix(),
1174
+ }}
1175
+ }