slidge-whatsapp 0.2.0b0__tar.gz → 0.2.1__tar.gz

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.

Files changed (22) hide show
  1. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/PKG-INFO +2 -1
  2. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/pyproject.toml +1 -1
  3. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/__init__.py +1 -1
  4. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/config.py +0 -6
  5. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/contact.py +4 -2
  6. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/event.go +319 -65
  7. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/gateway.go +82 -76
  8. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/gateway.py +11 -26
  9. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/group.py +13 -12
  10. slidge_whatsapp-0.2.1/slidge_whatsapp/media/ffmpeg.go +72 -0
  11. slidge_whatsapp-0.2.1/slidge_whatsapp/media/media.go +448 -0
  12. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/session.go +81 -44
  13. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/session.py +129 -64
  14. slidge_whatsapp-0.2.0b0/LICENSE +0 -661
  15. slidge_whatsapp-0.2.0b0/slidge_whatsapp/attachment.go +0 -386
  16. slidge_whatsapp-0.2.0b0/slidge_whatsapp/go.mod +0 -28
  17. slidge_whatsapp-0.2.0b0/slidge_whatsapp/go.sum +0 -55
  18. slidge_whatsapp-0.2.0b0/slidge_whatsapp/util.py +0 -12
  19. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/README.md +0 -0
  20. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/build.py +0 -0
  21. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/__main__.py +0 -0
  22. {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge-whatsapp
3
- Version: 0.2.0b0
3
+ Version: 0.2.1
4
4
  Summary: A Whatsapp/XMPP gateway.
5
5
  Author: deuill
6
6
  Author-email: alex@deuill.org
@@ -8,6 +8,7 @@ Requires-Python: >=3.11,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
11
12
  Requires-Dist: linkpreview (>=0.6.5,<0.7.0)
12
13
  Requires-Dist: pybindgen (>=0.22.1,<0.23.0)
13
14
  Requires-Dist: slidge (>=0.2.0beta0,<0.3.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "slidge-whatsapp"
3
- version = "0.2.0beta"
3
+ version = "0.2.1"
4
4
  description = "A Whatsapp/XMPP gateway."
5
5
  authors = ["deuill <alex@deuill.org>", "Nicoco <nicoco@nicoco.fr>"]
6
6
  readme = "README.md"
@@ -13,5 +13,5 @@ def main():
13
13
  entrypoint("slidge_whatsapp")
14
14
 
15
15
 
16
- __version__ = "0.2.0beta"
16
+ __version__ = "0.2.1"
17
17
  __all__ = "Gateway", "session", "command", "contact", "config", "group", "main"
@@ -26,12 +26,6 @@ ALWAYS_SYNC_ROSTER__DOC = (
26
26
  "Whether or not to perform a full sync of the WhatsApp roster on startup."
27
27
  )
28
28
 
29
- SKIP_VERIFY_TLS = False
30
- SKIP_VERIFY_TLS__DOC = (
31
- "Whether or not HTTPS connections made by this plugin should verify TLS"
32
- " certificates."
33
- )
34
-
35
29
  ENABLE_LINK_PREVIEWS = True
36
30
  ENABLE_LINK_PREVIEWS__DOC = (
37
31
  "Whether or not previews for links (URLs) should be generated on outgoing messages"
@@ -47,16 +47,18 @@ class Roster(LegacyRoster[str, Contact]):
47
47
  """
48
48
  Adds a WhatsApp contact to local roster, filling all required and optional information.
49
49
  """
50
+ # Don't attempt to add ourselves to the roster.
50
51
  if data.JID == self.user_legacy_id:
51
- # with the current implementation, we don't allow that
52
52
  return None
53
53
  contact = await self.by_legacy_id(data.JID)
54
54
  contact.name = data.Name
55
55
  contact.is_friend = True
56
56
  try:
57
57
  avatar = self.session.whatsapp.GetAvatar(data.JID, contact.avatar or "")
58
- if avatar.URL:
58
+ if avatar.URL and contact.avatar != avatar.ID:
59
59
  await contact.set_avatar(avatar.URL, avatar.ID)
60
+ elif avatar.URL == "":
61
+ await contact.set_avatar(None)
60
62
  except RuntimeError as err:
61
63
  self.session.log.error(
62
64
  "Failed getting avatar for contact %s: %s", data.JID, err
@@ -2,13 +2,18 @@ package whatsapp
2
2
 
3
3
  import (
4
4
  // Standard library.
5
+ "bytes"
5
6
  "context"
6
7
  "fmt"
8
+ "image/gif"
7
9
  "mime"
8
- "os"
9
10
  "strings"
10
11
 
12
+ // Internal packages.
13
+ "git.sr.ht/~nicoco/slidge-whatsapp/slidge_whatsapp/media"
14
+
11
15
  // Third-party libraries.
16
+ "github.com/h2non/filetype"
12
17
  "go.mau.fi/whatsmeow"
13
18
  "go.mau.fi/whatsmeow/proto/waE2E"
14
19
  "go.mau.fi/whatsmeow/proto/waWeb"
@@ -25,7 +30,7 @@ const (
25
30
  EventUnknown EventKind = iota
26
31
  EventQRCode
27
32
  EventPair
28
- EventConnected
33
+ EventConnect
29
34
  EventLoggedOut
30
35
  EventContact
31
36
  EventPresence
@@ -42,7 +47,7 @@ const (
42
47
  type EventPayload struct {
43
48
  QRCode string
44
49
  PairDeviceID string
45
- ConnectedJID string
50
+ Connect Connect
46
51
  Contact Contact
47
52
  Presence Presence
48
53
  Message Message
@@ -52,6 +57,17 @@ type EventPayload struct {
52
57
  Call Call
53
58
  }
54
59
 
60
+ // HandleEventFunc represents a handler for incoming events sent to the Python adapter, accepting an
61
+ // event type and payload.
62
+ type HandleEventFunc func(EventKind, *EventPayload)
63
+
64
+ // Connect represents event data related to a connection to WhatsApp being established, or failing
65
+ // to do so (based on the [Connect.Error] result).
66
+ type Connect struct {
67
+ JID string // The device JID given for this connection.
68
+ Error string // The connection error, if any.
69
+ }
70
+
55
71
  // A Avatar represents a small image set for a Contact or Group.
56
72
  type Avatar struct {
57
73
  ID string // The unique ID for this avatar, used for persistent caching.
@@ -92,7 +108,8 @@ type PresenceKind int
92
108
 
93
109
  // The presences handled by the overarching session event handler.
94
110
  const (
95
- PresenceAvailable PresenceKind = 1 + iota
111
+ PresenceUnknown PresenceKind = iota
112
+ PresenceAvailable
96
113
  PresenceUnavailable
97
114
  )
98
115
 
@@ -126,7 +143,7 @@ type MessageKind int
126
143
 
127
144
  // The message types handled by the overarching session event handler.
128
145
  const (
129
- MessagePlain MessageKind = 1 + iota
146
+ MessagePlain MessageKind = iota
130
147
  MessageEdit
131
148
  MessageRevoke
132
149
  MessageReaction
@@ -145,10 +162,12 @@ type Message struct {
145
162
  Body string // The plain-text message body. For attachment messages, this can be a caption.
146
163
  Timestamp int64 // The Unix timestamp denoting when this message was created.
147
164
  IsCarbon bool // Whether or not this message concerns the gateway user themselves.
165
+ IsForwarded bool // Whether or not the message was forwarded from another source.
148
166
  ReplyID string // The unique message ID this message is in reply to, if any.
149
167
  ReplyBody string // The full body of the message this message is in reply to, if any.
150
168
  Attachments []Attachment // The list of file (image, video, etc.) attachments contained in this message.
151
169
  Preview Preview // A short description for the URL provided in the message body, if any.
170
+ Location Location // The location metadata for messages, if any.
152
171
  MentionJIDs []string // A list of JIDs mentioned in this message, if any.
153
172
  Receipts []Receipt // The receipt statuses for the message, typically provided alongside historical messages.
154
173
  Reactions []Message // Reactions attached to message, typically provided alongside historical messages.
@@ -160,19 +179,41 @@ type Attachment struct {
160
179
  MIME string // The MIME type for attachment.
161
180
  Filename string // The recommended file name for this attachment. May be an auto-generated name.
162
181
  Caption string // The user-provided caption, provided alongside this attachment.
163
- Path string // Local path to the file is stored on disk.
182
+ Data []byte // Data for the attachment.
164
183
 
165
184
  // Internal fields.
166
- meta attachmentMetadata // Metadata specific to audio/video files, used in processing.
185
+ spec *media.Spec // Metadata specific to audio/video files, used in processing.
167
186
  }
168
187
 
188
+ // PreviewKind represents different ways of previewingadditional data inline with messages.
189
+ type PreviewKind int
190
+
191
+ const (
192
+ PreviewPlain PreviewKind = iota
193
+ PreviewVideo
194
+ )
195
+
169
196
  // A Preview represents a short description for a URL provided in a message body, as usually derived
170
197
  // from the content of the page pointed at.
171
198
  type Preview struct {
172
- URL string // The original (or canonical) URL this preview was generated for.
173
- Title string // The short title for the URL preview.
174
- Description string // The (optional) long-form description for the URL preview.
175
- ImagePath string // The local path for the image associated with the URL.
199
+ Kind PreviewKind // The kind of preview to show, defaults to plain URL preview.
200
+ URL string // The original (or canonical) URL this preview was generated for.
201
+ Title string // The short title for the URL preview.
202
+ Description string // The (optional) long-form description for the URL preview.
203
+ Thumbnail []byte // The (optional) thumbnail image data.
204
+ }
205
+
206
+ // A Location represents additional metadata given to location messages.
207
+ type Location struct {
208
+ Latitude float64
209
+ Longitude float64
210
+ Accuracy int
211
+ IsLive bool
212
+
213
+ // Optional fields given for named locations.
214
+ Name string
215
+ Address string
216
+ URL string
176
217
  }
177
218
 
178
219
  // NewMessageEvent returns event data meant for [Session.propagateEvent] for the primive message
@@ -189,12 +230,14 @@ func newMessageEvent(client *whatsmeow.Client, evt *events.Message) (EventKind,
189
230
  IsCarbon: evt.Info.IsFromMe,
190
231
  }
191
232
 
192
- // Broadcast and status messages are currently not handled at all.
233
+ // Handle Broadcasts and Status Updates; currently, only non-carbon, non-status broadcast
234
+ // messages are handled as plain messages, as support for analogues is lacking in the XMPP
235
+ // world.
193
236
  if evt.Info.Chat.Server == types.BroadcastServer {
194
- return EventUnknown, nil
195
- }
196
-
197
- if evt.Info.IsGroup {
237
+ if evt.Info.Chat.User == types.StatusBroadcastJID.User || message.IsCarbon {
238
+ return EventUnknown, nil
239
+ }
240
+ } else if evt.Info.IsGroup {
198
241
  message.GroupJID = evt.Info.Chat.ToNonAD().String()
199
242
  } else if message.IsCarbon {
200
243
  message.JID = evt.Info.Chat.ToNonAD().String()
@@ -227,6 +270,31 @@ func newMessageEvent(client *whatsmeow.Client, evt *events.Message) (EventKind,
227
270
  return EventMessage, &EventPayload{Message: message}
228
271
  }
229
272
 
273
+ // Handle location (static and live) message.
274
+ if l := evt.Message.GetLocationMessage(); l != nil {
275
+ message.Location = Location{
276
+ Latitude: l.GetDegreesLatitude(),
277
+ Longitude: l.GetDegreesLongitude(),
278
+ Accuracy: int(l.GetAccuracyInMeters()),
279
+ IsLive: l.GetIsLive(),
280
+ Name: l.GetName(),
281
+ Address: l.GetAddress(),
282
+ URL: l.GetURL(),
283
+ }
284
+ return EventMessage, &EventPayload{Message: message}
285
+ }
286
+
287
+ if l := evt.Message.GetLiveLocationMessage(); l != nil {
288
+ message.Body = l.GetCaption()
289
+ message.Location = Location{
290
+ Latitude: l.GetDegreesLatitude(),
291
+ Longitude: l.GetDegreesLongitude(),
292
+ Accuracy: int(l.GetAccuracyInMeters()),
293
+ IsLive: true,
294
+ }
295
+ return EventMessage, &EventPayload{Message: message}
296
+ }
297
+
230
298
  // Handle message attachments, if any.
231
299
  if attach, context, err := getMessageAttachments(client, evt.Message); err != nil {
232
300
  client.Log.Errorf("Failed getting message attachments: %s", err)
@@ -267,6 +335,7 @@ func getMessageWithContext(message Message, info *waE2E.ContextInfo) Message {
267
335
 
268
336
  message.ReplyID = info.GetStanzaID()
269
337
  message.OriginJID = info.GetParticipant()
338
+ message.IsForwarded = info.GetIsForwarded()
270
339
 
271
340
  if q := info.GetQuotedMessage(); q != nil {
272
341
  if qe := q.GetExtendedTextMessage(); qe != nil {
@@ -283,7 +352,8 @@ func getMessageWithContext(message Message, info *waE2E.ContextInfo) Message {
283
352
  // via WhatsApp. Any failures in retrieving any attachment will return an error immediately.
284
353
  func getMessageAttachments(client *whatsmeow.Client, message *waE2E.Message) ([]Attachment, *waE2E.ContextInfo, error) {
285
354
  var result []Attachment
286
- var context *waE2E.ContextInfo
355
+ var info *waE2E.ContextInfo
356
+ var convertSpec *media.Spec
287
357
  var kinds = []whatsmeow.DownloadableMessage{
288
358
  message.GetImageMessage(),
289
359
  message.GetAudioMessage(),
@@ -299,7 +369,11 @@ func getMessageAttachments(client *whatsmeow.Client, message *waE2E.Message) ([]
299
369
  case *waE2E.ImageMessage:
300
370
  a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
301
371
  case *waE2E.AudioMessage:
372
+ // Convert Opus-encoded voice messages to AAC-encoded audio, which has better support.
302
373
  a.MIME = msg.GetMimetype()
374
+ if msg.GetPTT() {
375
+ convertSpec = &media.Spec{MIME: media.TypeM4A}
376
+ }
303
377
  case *waE2E.VideoMessage:
304
378
  a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
305
379
  case *waE2E.DocumentMessage:
@@ -313,41 +387,204 @@ func getMessageAttachments(client *whatsmeow.Client, message *waE2E.Message) ([]
313
387
  continue
314
388
  }
315
389
 
316
- // Set filename from SHA256 checksum and MIME type, if none is already set.
317
- if a.Filename == "" {
318
- a.Filename = fmt.Sprintf("%x%s", msg.GetFileSHA256(), extensionByType(a.MIME))
319
- }
320
-
321
390
  // Attempt to download and decrypt raw attachment data, if any.
322
391
  data, err := client.Download(msg)
323
392
  if err != nil {
324
393
  return nil, nil, err
325
394
  }
326
395
 
327
- tmp, err := createTempFile(data)
328
- if err != nil {
329
- return nil, nil, fmt.Errorf("failed writing to temporary file: %w", err)
396
+ a.Data = data
397
+
398
+ // Convert incoming data if a specification has been given, ignoring any errors that occur.
399
+ if convertSpec != nil {
400
+ data, err = media.Convert(context.Background(), a.Data, convertSpec)
401
+ if err == nil {
402
+ a.Data, a.MIME = data, string(convertSpec.MIME)
403
+ }
404
+ }
405
+
406
+ // Set filename from SHA256 checksum and MIME type, if none is already set.
407
+ if a.Filename == "" {
408
+ a.Filename = fmt.Sprintf("%x%s", msg.GetFileSHA256(), extensionByType(a.MIME))
330
409
  }
331
410
 
332
- a.Path = tmp
333
411
  result = append(result, a)
334
412
  }
335
413
 
336
414
  // Handle any contact vCard as attachment.
337
415
  if c := message.GetContactMessage(); c != nil {
338
- tmp, err := createTempFile([]byte(c.GetVcard()))
339
- if err != nil {
340
- return nil, nil, fmt.Errorf("Failed getting contact message: %w", err)
341
- }
342
416
  result = append(result, Attachment{
343
417
  MIME: "text/vcard",
344
418
  Filename: c.GetDisplayName() + ".vcf",
345
- Path: tmp,
419
+ Data: []byte(c.GetVcard()),
346
420
  })
347
- context = c.GetContextInfo()
421
+ info = c.GetContextInfo()
348
422
  }
349
423
 
350
- return result, context, nil
424
+ return result, info, nil
425
+ }
426
+
427
+ const (
428
+ // The MIME type used by voice messages on WhatsApp.
429
+ voiceMessageMIME = string(media.TypeOgg) + "; codecs=opus"
430
+ // the MIME type used by animated images on WhatsApp.
431
+ animatedImageMIME = "image/gif"
432
+
433
+ // The maximum image attachment size we'll attempt to process in any way, in bytes.
434
+ maxConvertImageSize = 1024 * 1024 * 10 // 10MiB
435
+ // The maximum audio/video attachment size we'll attempt to process in any way, in bytes.
436
+ maxConvertAudioVideoSize = 1024 * 1024 * 20 // 20MiB
437
+
438
+ // The maximum number of samples to return in media waveforms.
439
+ maxWaveformSamples = 64
440
+
441
+ // Default thumbnail width in pixels.
442
+ defaultThumbnailWidth = 100
443
+ previewThumbnailWidth = 250
444
+ )
445
+
446
+ var (
447
+ // Default target specification for voice messages.
448
+ voiceMessageSpec = media.Spec{
449
+ MIME: media.MIMEType(voiceMessageMIME),
450
+ AudioBitRate: 64,
451
+ AudioChannels: 1,
452
+ AudioSampleRate: 48000,
453
+ StripMetadata: true,
454
+ }
455
+
456
+ // Default target specification for generic audio messages.
457
+ audioMessageSpec = media.Spec{
458
+ MIME: media.TypeM4A,
459
+ AudioBitRate: 160,
460
+ AudioSampleRate: 44100,
461
+ }
462
+
463
+ // Default target specification for video messages with inline preview.
464
+ videoMessageSpec = media.Spec{
465
+ MIME: media.TypeMP4,
466
+ AudioBitRate: 160,
467
+ AudioSampleRate: 44100,
468
+ VideoFilter: "pad=ceil(iw/2)*2:ceil(ih/2)*2",
469
+ VideoFrameRate: 25,
470
+ VideoPixelFormat: "yuv420p",
471
+ StripMetadata: true,
472
+ }
473
+
474
+ // Default target specification for image messages with inline preview.
475
+ imageMessageSpec = media.Spec{
476
+ MIME: media.TypeJPEG,
477
+ ImageQuality: 85,
478
+ }
479
+ )
480
+
481
+ // ConvertAttachment attempts to process a given attachment from a less-supported type to a
482
+ // canonically supported one; for example, from `image/png` to `image/jpeg`.
483
+ //
484
+ // Decisions about which MIME types to convert to are based on the concrete MIME type inferred from
485
+ // the file itself, and care is taken to conform to WhatsApp semantics for the given input MIME
486
+ // type.
487
+ //
488
+ // If the input MIME type is unknown, or conversion is impossible, the given attachment is not
489
+ // changed.
490
+ func convertAttachment(attach *Attachment) error {
491
+ var detectedMIME string
492
+ if t, _ := filetype.Match(attach.Data); t != filetype.Unknown {
493
+ detectedMIME = t.MIME.Value
494
+ if attach.MIME == "" || attach.MIME == "application/octet-stream" {
495
+ attach.MIME = detectedMIME
496
+ }
497
+ }
498
+
499
+ var spec media.Spec
500
+ var ctx = context.Background()
501
+
502
+ switch detectedMIME {
503
+ case "image/png", "image/webp":
504
+ // Convert common image formats to JPEG for inline preview.
505
+ if len(attach.Data) > maxConvertImageSize {
506
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
507
+ }
508
+
509
+ spec = imageMessageSpec
510
+ case "image/gif":
511
+ // Convert animated GIFs to MP4, as required by WhatsApp.
512
+ if len(attach.Data) > maxConvertImageSize {
513
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
514
+ }
515
+
516
+ img, err := gif.DecodeAll(bytes.NewReader(attach.Data))
517
+ if err != nil {
518
+ return fmt.Errorf("unable to decode GIF attachment")
519
+ } else if len(img.Image) == 1 {
520
+ spec = imageMessageSpec
521
+ } else {
522
+ spec = videoMessageSpec
523
+ var t float64
524
+ for d := range img.Delay {
525
+ t += float64(d) / 100
526
+ }
527
+ spec.ImageFrameRate = int(float64(len(img.Image)) / t)
528
+ }
529
+ case "audio/m4a", "audio/mp4":
530
+ if len(attach.Data) > maxConvertAudioVideoSize {
531
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
532
+ }
533
+
534
+ spec = voiceMessageSpec
535
+
536
+ if s, err := media.GetSpec(ctx, attach.Data); err == nil {
537
+ attach.spec = s
538
+ if s.AudioCodec == "alac" {
539
+ // Don't attempt to process lossless files at all, as it's assumed that the sender
540
+ // wants to retain these characteristics. Since WhatsApp will try (and likely fail)
541
+ // to process this as an audio message anyways, set a unique MIME type.
542
+ attach.MIME = "application/octet-stream"
543
+ return nil
544
+ }
545
+ }
546
+ case "audio/ogg":
547
+ if len(attach.Data) > maxConvertAudioVideoSize {
548
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
549
+ }
550
+
551
+ spec = audioMessageSpec
552
+ if s, err := media.GetSpec(ctx, attach.Data); err == nil {
553
+ attach.spec = s
554
+ if s.AudioCodec == "opus" {
555
+ // Assume that Opus-encoded Ogg files are meant to be voice messages, and re-encode
556
+ // them as such for WhatsApp.
557
+ spec = voiceMessageSpec
558
+ }
559
+ }
560
+ case "video/mp4", "video/webm":
561
+ if len(attach.Data) > maxConvertAudioVideoSize {
562
+ return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
563
+ }
564
+
565
+ spec = videoMessageSpec
566
+
567
+ if s, err := media.GetSpec(ctx, attach.Data); err == nil {
568
+ attach.spec = s
569
+ // Try to see if there's a video stream for ostensibly video-related MIME types, as
570
+ // these are some times misdetected as such.
571
+ if s.VideoWidth == 0 && s.VideoHeight == 0 && s.AudioSampleRate > 0 && s.Duration > 0 {
572
+ spec = voiceMessageSpec
573
+ }
574
+ }
575
+ default:
576
+ // Detected source MIME not in list we're willing to convert, move on without error.
577
+ return nil
578
+ }
579
+
580
+ // Convert attachment between file-types, if source MIME matches the known list of convertable types.
581
+ data, err := media.Convert(ctx, attach.Data, &spec)
582
+ if err != nil {
583
+ return fmt.Errorf("failed converting attachment: %w", err)
584
+ }
585
+
586
+ attach.Data, attach.MIME = data, string(spec.MIME)
587
+ return nil
351
588
  }
352
589
 
353
590
  // KnownMediaTypes represents MIME type to WhatsApp media types known to be handled by WhatsApp in a
@@ -366,24 +603,23 @@ var knownMediaTypes = map[string]whatsmeow.MediaType{
366
603
  // specific format; in addition, certain MIME types may be automatically converted to a
367
604
  // well-supported type via FFmpeg (if available).
368
605
  func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Message, error) {
606
+ var ctx = context.Background()
369
607
  var originalMIME = attach.MIME
608
+
370
609
  if err := convertAttachment(attach); err != nil {
371
610
  client.Log.Warnf("failed to auto-convert attachment: %s", err)
372
611
  }
373
612
 
374
- mediaType := knownMediaTypes[strings.Split(attach.MIME, ";")[0]]
613
+ mediaType := knownMediaTypes[getBaseMediaType(attach.MIME)]
375
614
  if mediaType == "" {
376
615
  mediaType = whatsmeow.MediaDocument
377
616
  }
378
617
 
379
- data, err := os.ReadFile(attach.Path)
380
- if err != nil {
381
- return nil, err
382
- } else if len(data) == 0 {
618
+ if len(attach.Data) == 0 {
383
619
  return nil, fmt.Errorf("attachment file contains no data")
384
620
  }
385
621
 
386
- upload, err := client.Upload(context.Background(), data, mediaType)
622
+ upload, err := client.Upload(ctx, attach.Data, mediaType)
387
623
  if err != nil {
388
624
  return nil, err
389
625
  }
@@ -399,12 +635,19 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
399
635
  Mimetype: &attach.MIME,
400
636
  FileEncSHA256: upload.FileEncSHA256,
401
637
  FileSHA256: upload.FileSHA256,
402
- FileLength: ptrTo(uint64(len(data))),
638
+ FileLength: ptrTo(uint64(len(attach.Data))),
403
639
  },
404
640
  }
641
+ t, err := media.Convert(ctx, attach.Data, &media.Spec{MIME: media.TypeJPEG, ImageWidth: defaultThumbnailWidth})
642
+ if err != nil {
643
+ client.Log.Warnf("failed generating attachment thumbnail: %s", err)
644
+ } else {
645
+ message.ImageMessage.JPEGThumbnail = t
646
+ }
405
647
  case whatsmeow.MediaAudio:
406
- if attach.meta == (attachmentMetadata{}) {
407
- if err = populateAttachmentMetadata(attach); err != nil {
648
+ spec := attach.spec
649
+ if spec == nil {
650
+ if spec, err = media.GetSpec(ctx, attach.Data); err != nil {
408
651
  client.Log.Warnf("failed fetching attachment metadata: %s", err)
409
652
  }
410
653
  }
@@ -416,21 +659,25 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
416
659
  Mimetype: &attach.MIME,
417
660
  FileEncSHA256: upload.FileEncSHA256,
418
661
  FileSHA256: upload.FileSHA256,
419
- FileLength: ptrTo(uint64(len(data))),
420
- Seconds: ptrTo(uint32(attach.meta.duration.Seconds())),
662
+ FileLength: ptrTo(uint64(len(attach.Data))),
663
+ Seconds: ptrTo(uint32(spec.Duration.Seconds())),
421
664
  },
422
665
  }
423
666
  if attach.MIME == voiceMessageMIME {
424
667
  message.AudioMessage.PTT = ptrTo(true)
425
- if wave, err := getAttachmentWaveform(attach); err != nil {
426
- client.Log.Warnf("failed generating attachment waveform: %s", err)
427
- } else {
428
- message.AudioMessage.Waveform = wave
668
+ if spec != nil {
669
+ w, err := media.GetWaveform(ctx, attach.Data, spec, maxWaveformSamples)
670
+ if err != nil {
671
+ client.Log.Warnf("failed generating attachment waveform: %s", err)
672
+ } else {
673
+ message.AudioMessage.Waveform = w
674
+ }
429
675
  }
430
676
  }
431
677
  case whatsmeow.MediaVideo:
432
- if attach.meta == (attachmentMetadata{}) {
433
- if err = populateAttachmentMetadata(attach); err != nil {
678
+ spec := attach.spec
679
+ if spec == nil {
680
+ if spec, err = media.GetSpec(ctx, attach.Data); err != nil {
434
681
  client.Log.Warnf("failed fetching attachment metadata: %s", err)
435
682
  }
436
683
  }
@@ -442,15 +689,17 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
442
689
  Mimetype: &attach.MIME,
443
690
  FileEncSHA256: upload.FileEncSHA256,
444
691
  FileSHA256: upload.FileSHA256,
445
- FileLength: ptrTo(uint64(len(data))),
446
- Seconds: ptrTo(uint32(attach.meta.duration.Seconds())),
447
- Width: ptrTo(uint32(attach.meta.width)),
448
- Height: ptrTo(uint32(attach.meta.height)),
449
- }}
450
- if thumb, err := getAttachmentThumbnail(attach); err != nil {
692
+ FileLength: ptrTo(uint64(len(attach.Data))),
693
+ Seconds: ptrTo(uint32(spec.Duration.Seconds())),
694
+ Width: ptrTo(uint32(spec.VideoWidth)),
695
+ Height: ptrTo(uint32(spec.VideoHeight)),
696
+ },
697
+ }
698
+ t, err := media.GetThumbnail(ctx, attach.Data, defaultThumbnailWidth, 0)
699
+ if err != nil {
451
700
  client.Log.Warnf("failed generating attachment thumbnail: %s", err)
452
701
  } else {
453
- message.VideoMessage.JPEGThumbnail = thumb
702
+ message.VideoMessage.JPEGThumbnail = t
454
703
  }
455
704
  if originalMIME == animatedImageMIME {
456
705
  message.VideoMessage.GifPlayback = ptrTo(true)
@@ -464,7 +713,7 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
464
713
  Mimetype: &attach.MIME,
465
714
  FileEncSHA256: upload.FileEncSHA256,
466
715
  FileSHA256: upload.FileSHA256,
467
- FileLength: ptrTo(uint64(len(data))),
716
+ FileLength: ptrTo(uint64(len(attach.Data))),
468
717
  FileName: &attach.Filename,
469
718
  }}
470
719
  }
@@ -476,6 +725,7 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
476
725
  var knownExtensions = map[string]string{
477
726
  "image/jpeg": ".jpg",
478
727
  "audio/ogg": ".oga",
728
+ "audio/mp4": ".m4a",
479
729
  "video/mp4": ".mp4",
480
730
  }
481
731
 
@@ -492,6 +742,11 @@ func extensionByType(typ string) string {
492
742
  return ".bin"
493
743
  }
494
744
 
745
+ // GetBaseMediaType returns the media type without any additional parameters.
746
+ func getBaseMediaType(typ string) string {
747
+ return strings.SplitN(typ, ";", 2)[0]
748
+ }
749
+
495
750
  // NewEventFromHistory returns event data meant for [Session.propagateEvent] for the primive history
496
751
  // message given. Currently, only events related to group-chats will be handled, due to uncertain
497
752
  // support for history back-fills on 1:1 chats.
@@ -618,7 +873,8 @@ type ChatStateKind int
618
873
 
619
874
  // The chat states handled by the overarching session event handler.
620
875
  const (
621
- ChatStateComposing ChatStateKind = 1 + iota
876
+ ChatStateUnknown ChatStateKind = iota
877
+ ChatStateComposing
622
878
  ChatStatePaused
623
879
  )
624
880
 
@@ -652,7 +908,8 @@ type ReceiptKind int
652
908
 
653
909
  // The delivery receipts handled by the overarching session event handler.
654
910
  const (
655
- ReceiptDelivered ReceiptKind = 1 + iota
911
+ ReceiptUnknown ReceiptKind = iota
912
+ ReceiptDelivered
656
913
  ReceiptRead
657
914
  )
658
915
 
@@ -682,12 +939,9 @@ func newReceiptEvent(evt *events.Receipt) (EventKind, *EventPayload) {
682
939
  return EventUnknown, nil
683
940
  }
684
941
 
685
- // Receipts for broadcast and status messages are currently not handled at all.
686
942
  if evt.MessageSource.Chat.Server == types.BroadcastServer {
687
- return EventUnknown, nil
688
- }
689
-
690
- if evt.MessageSource.IsGroup {
943
+ receipt.JID = evt.MessageSource.BroadcastListOwner.ToNonAD().String()
944
+ } else if evt.MessageSource.IsGroup {
691
945
  receipt.GroupJID = evt.MessageSource.Chat.ToNonAD().String()
692
946
  } else if receipt.IsCarbon {
693
947
  receipt.JID = evt.MessageSource.Chat.ToNonAD().String()