slidge-whatsapp 0.2.1__cp311-cp311-manylinux_2_36_x86_64.whl → 0.2.3__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.

@@ -13,5 +13,5 @@ def main():
13
13
  entrypoint("slidge_whatsapp")
14
14
 
15
15
 
16
- __version__ = "0.2.1"
16
+ __version__ = "v0.2.3"
17
17
  __all__ = "Gateway", "session", "command", "contact", "config", "group", "main"
@@ -57,7 +57,7 @@ class Roster(LegacyRoster[str, Contact]):
57
57
  avatar = self.session.whatsapp.GetAvatar(data.JID, contact.avatar or "")
58
58
  if avatar.URL and contact.avatar != avatar.ID:
59
59
  await contact.set_avatar(avatar.URL, avatar.ID)
60
- elif avatar.URL == "":
60
+ elif avatar.URL == "" and avatar.ID == "":
61
61
  await contact.set_avatar(None)
62
62
  except RuntimeError as err:
63
63
  self.session.log.error(
slidge_whatsapp/event.go CHANGED
@@ -2,18 +2,15 @@ package whatsapp
2
2
 
3
3
  import (
4
4
  // Standard library.
5
- "bytes"
6
5
  "context"
7
6
  "fmt"
8
- "image/gif"
9
7
  "mime"
10
8
  "strings"
11
9
 
12
10
  // Internal packages.
13
- "git.sr.ht/~nicoco/slidge-whatsapp/slidge_whatsapp/media"
11
+ "codeberg.org/slidge/slidge-whatsapp/slidge_whatsapp/media"
14
12
 
15
13
  // Third-party libraries.
16
- "github.com/h2non/filetype"
17
14
  "go.mau.fi/whatsmeow"
18
15
  "go.mau.fi/whatsmeow/proto/waE2E"
19
16
  "go.mau.fi/whatsmeow/proto/waWeb"
@@ -185,6 +182,21 @@ type Attachment struct {
185
182
  spec *media.Spec // Metadata specific to audio/video files, used in processing.
186
183
  }
187
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
+
188
200
  // PreviewKind represents different ways of previewingadditional data inline with messages.
189
201
  type PreviewKind int
190
202
 
@@ -230,10 +242,9 @@ func newMessageEvent(client *whatsmeow.Client, evt *events.Message) (EventKind,
230
242
  IsCarbon: evt.Info.IsFromMe,
231
243
  }
232
244
 
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.
236
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.
237
248
  if evt.Info.Chat.User == types.StatusBroadcastJID.User || message.IsCarbon {
238
249
  return EventUnknown, nil
239
250
  }
@@ -427,7 +438,7 @@ func getMessageAttachments(client *whatsmeow.Client, message *waE2E.Message) ([]
427
438
  const (
428
439
  // The MIME type used by voice messages on WhatsApp.
429
440
  voiceMessageMIME = string(media.TypeOgg) + "; codecs=opus"
430
- // the MIME type used by animated images on WhatsApp.
441
+ // The MIME type used by animated images on WhatsApp.
431
442
  animatedImageMIME = "image/gif"
432
443
 
433
444
  // The maximum image attachment size we'll attempt to process in any way, in bytes.
@@ -437,10 +448,6 @@ const (
437
448
 
438
449
  // The maximum number of samples to return in media waveforms.
439
450
  maxWaveformSamples = 64
440
-
441
- // Default thumbnail width in pixels.
442
- defaultThumbnailWidth = 100
443
- previewThumbnailWidth = 250
444
451
  )
445
452
 
446
453
  var (
@@ -476,6 +483,18 @@ var (
476
483
  MIME: media.TypeJPEG,
477
484
  ImageQuality: 85,
478
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
+ }
479
498
  )
480
499
 
481
500
  // ConvertAttachment attempts to process a given attachment from a less-supported type to a
@@ -488,11 +507,11 @@ var (
488
507
  // If the input MIME type is unknown, or conversion is impossible, the given attachment is not
489
508
  // changed.
490
509
  func convertAttachment(attach *Attachment) error {
491
- var detectedMIME string
492
- if t, _ := filetype.Match(attach.Data); t != filetype.Unknown {
493
- detectedMIME = t.MIME.Value
510
+ var detectedMIME media.MIMEType
511
+ if t := media.DetectMIMEType(attach.Data); t != media.TypeUnknown {
512
+ detectedMIME = t
494
513
  if attach.MIME == "" || attach.MIME == "application/octet-stream" {
495
- attach.MIME = detectedMIME
514
+ attach.MIME = string(detectedMIME)
496
515
  }
497
516
  }
498
517
 
@@ -500,41 +519,32 @@ func convertAttachment(attach *Attachment) error {
500
519
  var ctx = context.Background()
501
520
 
502
521
  switch detectedMIME {
503
- case "image/png", "image/webp":
522
+ case media.TypePNG, media.TypeWebP:
504
523
  // Convert common image formats to JPEG for inline preview.
505
524
  if len(attach.Data) > maxConvertImageSize {
506
525
  return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
507
526
  }
508
527
 
509
528
  spec = imageMessageSpec
510
- case "image/gif":
511
- // Convert animated GIFs to MP4, as required by WhatsApp.
529
+ case media.TypeGIF:
530
+ // Convert GIFs to JPEG or MP4, if animated, as required by WhatsApp.
512
531
  if len(attach.Data) > maxConvertImageSize {
513
532
  return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertImageSize)
514
533
  }
515
534
 
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 {
535
+ spec = imageMessageSpec
536
+ if s, err := attach.GetSpec(ctx); err == nil && s.ImageFrameRate > 0 {
522
537
  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)
538
+ spec.ImageFrameRate = s.ImageFrameRate
528
539
  }
529
- case "audio/m4a", "audio/mp4":
540
+ case media.TypeM4A:
530
541
  if len(attach.Data) > maxConvertAudioVideoSize {
531
542
  return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
532
543
  }
533
544
 
534
545
  spec = voiceMessageSpec
535
546
 
536
- if s, err := media.GetSpec(ctx, attach.Data); err == nil {
537
- attach.spec = s
547
+ if s, err := attach.GetSpec(ctx); err == nil {
538
548
  if s.AudioCodec == "alac" {
539
549
  // Don't attempt to process lossless files at all, as it's assumed that the sender
540
550
  // wants to retain these characteristics. Since WhatsApp will try (and likely fail)
@@ -543,33 +553,32 @@ func convertAttachment(attach *Attachment) error {
543
553
  return nil
544
554
  }
545
555
  }
546
- case "audio/ogg":
556
+ case media.TypeOgg:
547
557
  if len(attach.Data) > maxConvertAudioVideoSize {
548
558
  return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
549
559
  }
550
560
 
551
561
  spec = audioMessageSpec
552
- if s, err := media.GetSpec(ctx, attach.Data); err == nil {
553
- attach.spec = s
562
+ if s, err := attach.GetSpec(ctx); err == nil {
554
563
  if s.AudioCodec == "opus" {
555
564
  // Assume that Opus-encoded Ogg files are meant to be voice messages, and re-encode
556
565
  // them as such for WhatsApp.
557
566
  spec = voiceMessageSpec
558
567
  }
559
568
  }
560
- case "video/mp4", "video/webm":
569
+ case media.TypeMP4, media.TypeWebM:
561
570
  if len(attach.Data) > maxConvertAudioVideoSize {
562
571
  return fmt.Errorf("attachment size %d exceeds maximum of %d", len(attach.Data), maxConvertAudioVideoSize)
563
572
  }
564
573
 
565
574
  spec = videoMessageSpec
566
575
 
567
- if s, err := media.GetSpec(ctx, attach.Data); err == nil {
568
- attach.spec = s
576
+ if s, err := attach.GetSpec(ctx); err == nil {
569
577
  // Try to see if there's a video stream for ostensibly video-related MIME types, as
570
578
  // these are some times misdetected as such.
571
579
  if s.VideoWidth == 0 && s.VideoHeight == 0 && s.AudioSampleRate > 0 && s.Duration > 0 {
572
580
  spec = voiceMessageSpec
581
+ spec.SourceMIME = media.TypeM4A
573
582
  }
574
583
  }
575
584
  default:
@@ -578,6 +587,10 @@ func convertAttachment(attach *Attachment) error {
578
587
  }
579
588
 
580
589
  // Convert attachment between file-types, if source MIME matches the known list of convertable types.
590
+ if spec.SourceMIME == "" {
591
+ spec.SourceMIME = detectedMIME
592
+ }
593
+
581
594
  data, err := media.Convert(ctx, attach.Data, &spec)
582
595
  if err != nil {
583
596
  return fmt.Errorf("failed converting attachment: %w", err)
@@ -638,18 +651,17 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
638
651
  FileLength: ptrTo(uint64(len(attach.Data))),
639
652
  },
640
653
  }
641
- t, err := media.Convert(ctx, attach.Data, &media.Spec{MIME: media.TypeJPEG, ImageWidth: defaultThumbnailWidth})
654
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
642
655
  if err != nil {
643
656
  client.Log.Warnf("failed generating attachment thumbnail: %s", err)
644
657
  } else {
645
658
  message.ImageMessage.JPEGThumbnail = t
646
659
  }
647
660
  case whatsmeow.MediaAudio:
648
- spec := attach.spec
649
- if spec == nil {
650
- if spec, err = media.GetSpec(ctx, attach.Data); err != nil {
651
- client.Log.Warnf("failed fetching attachment metadata: %s", err)
652
- }
661
+ spec, err := attach.GetSpec(ctx)
662
+ if err != nil {
663
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
664
+ spec = &media.Spec{}
653
665
  }
654
666
  message = &waE2E.Message{
655
667
  AudioMessage: &waE2E.AudioMessage{
@@ -675,11 +687,10 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
675
687
  }
676
688
  }
677
689
  case whatsmeow.MediaVideo:
678
- spec := attach.spec
679
- if spec == nil {
680
- if spec, err = media.GetSpec(ctx, attach.Data); err != nil {
681
- client.Log.Warnf("failed fetching attachment metadata: %s", err)
682
- }
690
+ spec, err := attach.GetSpec(ctx)
691
+ if err != nil {
692
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
693
+ spec = &media.Spec{}
683
694
  }
684
695
  message = &waE2E.Message{
685
696
  VideoMessage: &waE2E.VideoMessage{
@@ -695,7 +706,7 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
695
706
  Height: ptrTo(uint32(spec.VideoHeight)),
696
707
  },
697
708
  }
698
- t, err := media.GetThumbnail(ctx, attach.Data, defaultThumbnailWidth, 0)
709
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
699
710
  if err != nil {
700
711
  client.Log.Warnf("failed generating attachment thumbnail: %s", err)
701
712
  } else {
@@ -715,7 +726,22 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
715
726
  FileSHA256: upload.FileSHA256,
716
727
  FileLength: ptrTo(uint64(len(attach.Data))),
717
728
  FileName: &attach.Filename,
718
- }}
729
+ },
730
+ }
731
+ switch media.MIMEType(attach.MIME) {
732
+ case media.TypePDF:
733
+ if spec, err := attach.GetSpec(ctx); err != nil {
734
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
735
+ } else {
736
+ message.DocumentMessage.PageCount = ptrTo(uint32(spec.DocumentPage))
737
+ }
738
+ t, err := media.Convert(ctx, attach.Data, &previewThumbnailSpec)
739
+ if err != nil {
740
+ client.Log.Warnf("failed generating attachment thumbnail: %s", err)
741
+ } else {
742
+ message.DocumentMessage.JPEGThumbnail = t
743
+ }
744
+ }
719
745
  }
720
746
 
721
747
  return message, nil
@@ -992,11 +1018,27 @@ type GroupSubject struct {
992
1018
  type GroupParticipantAction int
993
1019
 
994
1020
  const (
995
- GroupParticipantActionAdd GroupParticipantAction = iota // Default action; add participant to list.
996
- GroupParticipantActionUpdate // Update existing participant information.
997
- GroupParticipantActionRemove // Remove participant from list, if existing.
1021
+ GroupParticipantActionAdd GroupParticipantAction = iota // Default action; add participant to list.
1022
+ GroupParticipantActionRemove // Remove participant from list, if existing.
1023
+ GroupParticipantActionPromote // Make group member into administrator.
1024
+ GroupParticipantActionDemote // Make group administrator into member.
998
1025
  )
999
1026
 
1027
+ // ToParticipantChange converts our public [GroupParticipantAction] to the internal [ParticipantChange]
1028
+ // representation.
1029
+ func (a GroupParticipantAction) toParticipantChange() whatsmeow.ParticipantChange {
1030
+ switch a {
1031
+ case GroupParticipantActionRemove:
1032
+ return whatsmeow.ParticipantChangeRemove
1033
+ case GroupParticipantActionPromote:
1034
+ return whatsmeow.ParticipantChangePromote
1035
+ case GroupParticipantActionDemote:
1036
+ return whatsmeow.ParticipantChangeDemote
1037
+ default:
1038
+ return whatsmeow.ParticipantChangeAdd
1039
+ }
1040
+ }
1041
+
1000
1042
  // A GroupParticipant represents a contact who is currently joined in a given group. Participants in
1001
1043
  // WhatsApp can always be derived back to their individual [Contact]; there are no anonymous groups
1002
1044
  // in WhatsApp.
@@ -1006,6 +1048,25 @@ type GroupParticipant struct {
1006
1048
  Action GroupParticipantAction // The specific action to take for this participant; typically to add.
1007
1049
  }
1008
1050
 
1051
+ // NewGroupParticipant returns a [GroupParticipant], filling fields from the internal participant
1052
+ // type. This is a no-op if [types.GroupParticipant.Error] is non-zero, and other fields may only
1053
+ // be set optionally.
1054
+ func newGroupParticipant(p types.GroupParticipant) GroupParticipant {
1055
+ if p.Error > 0 {
1056
+ return GroupParticipant{}
1057
+ }
1058
+ var affiliation = GroupAffiliationNone
1059
+ if p.IsSuperAdmin {
1060
+ affiliation = GroupAffiliationOwner
1061
+ } else if p.IsAdmin {
1062
+ affiliation = GroupAffiliationAdmin
1063
+ }
1064
+ return GroupParticipant{
1065
+ JID: p.JID.ToNonAD().String(),
1066
+ Affiliation: affiliation,
1067
+ }
1068
+ }
1069
+
1009
1070
  // NewGroupEvent returns event data meant for [Session.propagateEvent] for the primive group event
1010
1071
  // given. Group data returned by this function can be partial, and callers should take care to only
1011
1072
  // handle non-empty values.
@@ -1036,14 +1097,14 @@ func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
1036
1097
  for _, p := range evt.Promote {
1037
1098
  group.Participants = append(group.Participants, GroupParticipant{
1038
1099
  JID: p.ToNonAD().String(),
1039
- Action: GroupParticipantActionUpdate,
1100
+ Action: GroupParticipantActionPromote,
1040
1101
  Affiliation: GroupAffiliationAdmin,
1041
1102
  })
1042
1103
  }
1043
1104
  for _, p := range evt.Demote {
1044
1105
  group.Participants = append(group.Participants, GroupParticipant{
1045
1106
  JID: p.ToNonAD().String(),
1046
- Action: GroupParticipantActionUpdate,
1107
+ Action: GroupParticipantActionDemote,
1047
1108
  Affiliation: GroupAffiliationNone,
1048
1109
  })
1049
1110
  }
@@ -1055,20 +1116,12 @@ func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
1055
1116
  // be called when partial data is to be returned.
1056
1117
  func newGroup(client *whatsmeow.Client, info *types.GroupInfo) Group {
1057
1118
  var participants []GroupParticipant
1058
- for _, p := range info.Participants {
1059
- if p.Error > 0 {
1119
+ for i := range info.Participants {
1120
+ p := newGroupParticipant(info.Participants[i])
1121
+ if p.JID == "" {
1060
1122
  continue
1061
1123
  }
1062
- var affiliation = GroupAffiliationNone
1063
- if p.IsSuperAdmin {
1064
- affiliation = GroupAffiliationOwner
1065
- } else if p.IsAdmin {
1066
- affiliation = GroupAffiliationAdmin
1067
- }
1068
- participants = append(participants, GroupParticipant{
1069
- JID: p.JID.ToNonAD().String(),
1070
- Affiliation: affiliation,
1071
- })
1124
+ participants = append(participants, p)
1072
1125
  }
1073
1126
  return Group{
1074
1127
  JID: info.JID.ToNonAD().String(),
@@ -8,7 +8,7 @@ import (
8
8
  "runtime"
9
9
 
10
10
  // Internal packages.
11
- "git.sr.ht/~nicoco/slidge-whatsapp/slidge_whatsapp/media"
11
+ "codeberg.org/slidge/slidge-whatsapp/slidge_whatsapp/media"
12
12
 
13
13
  // Third-party libraries.
14
14
  _ "github.com/mattn/go-sqlite3"