slidge-whatsapp 0.2.1__cp311-cp311-manylinux_2_36_x86_64.whl → 0.2.2__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.
@@ -13,5 +13,5 @@ def main():
13
13
  entrypoint("slidge_whatsapp")
14
14
 
15
15
 
16
- __version__ = "0.2.1"
16
+ __version__ = "v0.2.2"
17
17
  __all__ = "Gateway", "session", "command", "contact", "config", "group", "main"
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,29 +553,27 @@ 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 {
@@ -638,18 +646,17 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
638
646
  FileLength: ptrTo(uint64(len(attach.Data))),
639
647
  },
640
648
  }
641
- t, err := media.Convert(ctx, attach.Data, &media.Spec{MIME: media.TypeJPEG, ImageWidth: defaultThumbnailWidth})
649
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
642
650
  if err != nil {
643
651
  client.Log.Warnf("failed generating attachment thumbnail: %s", err)
644
652
  } else {
645
653
  message.ImageMessage.JPEGThumbnail = t
646
654
  }
647
655
  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
- }
656
+ spec, err := attach.GetSpec(ctx)
657
+ if err != nil {
658
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
659
+ spec = &media.Spec{}
653
660
  }
654
661
  message = &waE2E.Message{
655
662
  AudioMessage: &waE2E.AudioMessage{
@@ -675,11 +682,10 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
675
682
  }
676
683
  }
677
684
  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
- }
685
+ spec, err := attach.GetSpec(ctx)
686
+ if err != nil {
687
+ client.Log.Warnf("failed fetching attachment metadata: %s", err)
688
+ spec = &media.Spec{}
683
689
  }
684
690
  message = &waE2E.Message{
685
691
  VideoMessage: &waE2E.VideoMessage{
@@ -695,7 +701,7 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
695
701
  Height: ptrTo(uint32(spec.VideoHeight)),
696
702
  },
697
703
  }
698
- t, err := media.GetThumbnail(ctx, attach.Data, defaultThumbnailWidth, 0)
704
+ t, err := media.Convert(ctx, attach.Data, &defaultThumbnailSpec)
699
705
  if err != nil {
700
706
  client.Log.Warnf("failed generating attachment thumbnail: %s", err)
701
707
  } else {
@@ -715,7 +721,22 @@ func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*waE2E.Mess
715
721
  FileSHA256: upload.FileSHA256,
716
722
  FileLength: ptrTo(uint64(len(attach.Data))),
717
723
  FileName: &attach.Filename,
718
- }}
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
+ }
719
740
  }
720
741
 
721
742
  return message, nil
@@ -992,11 +1013,27 @@ type GroupSubject struct {
992
1013
  type GroupParticipantAction int
993
1014
 
994
1015
  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.
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.
998
1020
  )
999
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
+
1000
1037
  // A GroupParticipant represents a contact who is currently joined in a given group. Participants in
1001
1038
  // WhatsApp can always be derived back to their individual [Contact]; there are no anonymous groups
1002
1039
  // in WhatsApp.
@@ -1006,6 +1043,25 @@ type GroupParticipant struct {
1006
1043
  Action GroupParticipantAction // The specific action to take for this participant; typically to add.
1007
1044
  }
1008
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
+
1009
1065
  // NewGroupEvent returns event data meant for [Session.propagateEvent] for the primive group event
1010
1066
  // given. Group data returned by this function can be partial, and callers should take care to only
1011
1067
  // handle non-empty values.
@@ -1036,14 +1092,14 @@ func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
1036
1092
  for _, p := range evt.Promote {
1037
1093
  group.Participants = append(group.Participants, GroupParticipant{
1038
1094
  JID: p.ToNonAD().String(),
1039
- Action: GroupParticipantActionUpdate,
1095
+ Action: GroupParticipantActionPromote,
1040
1096
  Affiliation: GroupAffiliationAdmin,
1041
1097
  })
1042
1098
  }
1043
1099
  for _, p := range evt.Demote {
1044
1100
  group.Participants = append(group.Participants, GroupParticipant{
1045
1101
  JID: p.ToNonAD().String(),
1046
- Action: GroupParticipantActionUpdate,
1102
+ Action: GroupParticipantActionDemote,
1047
1103
  Affiliation: GroupAffiliationNone,
1048
1104
  })
1049
1105
  }
@@ -1055,20 +1111,12 @@ func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
1055
1111
  // be called when partial data is to be returned.
1056
1112
  func newGroup(client *whatsmeow.Client, info *types.GroupInfo) Group {
1057
1113
  var participants []GroupParticipant
1058
- for _, p := range info.Participants {
1059
- if p.Error > 0 {
1114
+ for i := range info.Participants {
1115
+ p := newGroupParticipant(info.Participants[i])
1116
+ if p.JID == "" {
1060
1117
  continue
1061
1118
  }
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
- })
1119
+ participants = append(participants, p)
1072
1120
  }
1073
1121
  return Group{
1074
1122
  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"