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.
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/PKG-INFO +2 -1
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/pyproject.toml +1 -1
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/__init__.py +1 -1
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/config.py +0 -6
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/contact.py +4 -2
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/event.go +319 -65
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/gateway.go +82 -76
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/gateway.py +11 -26
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/group.py +13 -12
- slidge_whatsapp-0.2.1/slidge_whatsapp/media/ffmpeg.go +72 -0
- slidge_whatsapp-0.2.1/slidge_whatsapp/media/media.go +448 -0
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/session.go +81 -44
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/session.py +129 -64
- slidge_whatsapp-0.2.0b0/LICENSE +0 -661
- slidge_whatsapp-0.2.0b0/slidge_whatsapp/attachment.go +0 -386
- slidge_whatsapp-0.2.0b0/slidge_whatsapp/go.mod +0 -28
- slidge_whatsapp-0.2.0b0/slidge_whatsapp/go.sum +0 -55
- slidge_whatsapp-0.2.0b0/slidge_whatsapp/util.py +0 -12
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/README.md +0 -0
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/build.py +0 -0
- {slidge_whatsapp-0.2.0b0 → slidge_whatsapp-0.2.1}/slidge_whatsapp/__main__.py +0 -0
- {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.
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
182
|
+
Data []byte // Data for the attachment.
|
|
164
183
|
|
|
165
184
|
// Internal fields.
|
|
166
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
419
|
+
Data: []byte(c.GetVcard()),
|
|
346
420
|
})
|
|
347
|
-
|
|
421
|
+
info = c.GetContextInfo()
|
|
348
422
|
}
|
|
349
423
|
|
|
350
|
-
return result,
|
|
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[
|
|
613
|
+
mediaType := knownMediaTypes[getBaseMediaType(attach.MIME)]
|
|
375
614
|
if mediaType == "" {
|
|
376
615
|
mediaType = whatsmeow.MediaDocument
|
|
377
616
|
}
|
|
378
617
|
|
|
379
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
407
|
-
|
|
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(
|
|
420
|
-
Seconds: ptrTo(uint32(
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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(
|
|
446
|
-
Seconds: ptrTo(uint32(
|
|
447
|
-
Width: ptrTo(uint32(
|
|
448
|
-
Height: ptrTo(uint32(
|
|
449
|
-
}
|
|
450
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|