]> git.giacomolli.uk - onechat/blob - whatsapp_image.go
Send stored images to WhatsApp chats
[onechat] / whatsapp_image.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "time"
9
10 "go.mau.fi/whatsmeow"
11 "go.mau.fi/whatsmeow/proto/waE2E"
12 )
13
14 // whatsappImageUploadTimeout limits one image upload to WhatsApp.
15 const whatsappImageUploadTimeout = 2 * time.Minute
16
17 // whatsappImage downloads only supported images and stores them before the
18 // event can be acknowledged. Both the declared size and the downloaded bytes
19 // must fit the image store. Decoded bytes determine the stored format.
20 func whatsappImage(ctx context.Context, client WhatsAppClient, store *ImageStore, message *waE2E.ImageMessage) (string, error) {
21 if message.GetFileLength() > imageMaxBytes {
22 return "", errImageSize
23 }
24
25 switch message.GetMimetype() {
26 case "image/jpeg", "image/png", "image/webp":
27 default:
28 return "", errImageFormat
29 }
30
31 if client == nil {
32 return "", errors.New("WhatsApp client is not available")
33 }
34
35 data, err := client.Download(ctx, message)
36 if err != nil {
37 return "", fmt.Errorf("download image: %w", whatsappImageDownloadError(err))
38 }
39
40 name, err := store.Save(data)
41 if err != nil {
42 return "", fmt.Errorf("store image: %w", err)
43 }
44
45 return name, nil
46 }
47
48 // whatsappImageDownloadError keeps only known safe causes. Library errors can
49 // contain signed URLs or direct paths even when request preparation fails, so
50 // arbitrary error text cannot go to the daemon log.
51 func whatsappImageDownloadError(err error) error {
52 switch {
53 case errors.Is(err, context.Canceled):
54 return context.Canceled
55 case errors.Is(err, context.DeadlineExceeded):
56 return context.DeadlineExceeded
57 }
58
59 var response whatsmeow.DownloadHTTPError
60
61 if errors.As(err, &response) && response.Response != nil {
62 return fmt.Errorf("media server returned HTTP %d", response.StatusCode)
63 }
64
65 return errors.New("media download failed")
66 }
67
68 // whatsappImageMessage uploads stored pixels and uses the upload keys only in
69 // the outgoing message. The text is the caption, not a separate message.
70 func whatsappImageMessage(ctx context.Context, client WhatsAppClient, data []byte, text string) (*waE2E.Message, error) {
71 ctx, cancel := context.WithTimeout(ctx, whatsappImageUploadTimeout)
72 defer cancel()
73
74 upload, err := client.Upload(ctx, data, whatsmeow.MediaImage)
75 if err != nil {
76 return nil, fmt.Errorf("upload image: %w", whatsappImageSendError(err))
77 }
78
79 return &waE2E.Message{ImageMessage: &waE2E.ImageMessage{
80 Caption: new(text),
81 Mimetype: new(http.DetectContentType(data)),
82 URL: &upload.URL,
83 DirectPath: &upload.DirectPath,
84 MediaKey: upload.MediaKey,
85 FileEncSHA256: upload.FileEncSHA256,
86 FileSHA256: upload.FileSHA256,
87 FileLength: &upload.FileLength,
88 }}, nil
89 }
90
91 // whatsappImageSendError drops arbitrary upload and send errors, which can hold
92 // media URLs and keys. Cancellation and deadline causes are safe to keep.
93 func whatsappImageSendError(err error) error {
94 switch {
95 case errors.Is(err, context.Canceled):
96 return context.Canceled
97 case errors.Is(err, context.DeadlineExceeded):
98 return context.DeadlineExceeded
99 default:
100 return errors.New("media send failed")
101 }
102 }