]> git.giacomolli.uk - onechat/blobdiff - whatsapp_inbox.go
Pass caller contexts through WhatsApp callbacks
[onechat] / whatsapp_inbox.go
index adff79e698f22d28bb1ce388298c90839d263cb7..06f1c558d03b930bd52f597c4bd61e49806ca9e5 100644 (file)
@@ -2,7 +2,9 @@ package main
 
 import (
        "context"
+       "crypto/sha256"
        "fmt"
+       "log/slog"
        "slices"
        "strings"
        "sync"
@@ -23,19 +25,24 @@ const (
 )
 
 // newWhatsAppInbox returns the inbox of the batches in state, with clock for
-// the times of the batch texts and groups for the names of the groups.
-func newWhatsAppInbox(state *WhatsAppState, clock func() time.Time, groups *WhatsAppGroups) *WhatsAppInbox {
+// the times of the batch texts and groups for the names of the groups. images
+// holds shared images, and logger gets image failures.
+func newWhatsAppInbox(state *WhatsAppState, clock func() time.Time, groups *WhatsAppGroups, images *ImageStore, logger *slog.Logger) *WhatsAppInbox {
        return &WhatsAppInbox{
                state:        state,
                clock:        clock,
                groups:       groups,
+               images:       images,
+               logger:       logger,
+               lookupTime:   whatsappLookupTime,
+               imageTime:    whatsappImageTime,
                catchupLimit: whatsappCatchupLimit,
                wake:         make(chan struct{}, 1),
                failures:     make(chan error, 1),
        }
 }
 
-// WhatsAppInbox puts the text messages into batches, one chat at a time, as
+// WhatsAppInbox puts messages into batches, one chat at a time, as
 // WhatsAppState says. deliver logs each closed batch as a work message that
 // starts with [WhatsApp], and a batch that needs the user wakes the turn loop.
 //
@@ -61,6 +68,10 @@ type WhatsAppInbox struct {
        state        *WhatsAppState
        clock        func() time.Time
        groups       *WhatsAppGroups
+       images       *ImageStore
+       logger       *slog.Logger
+       lookupTime   time.Duration
+       imageTime    time.Duration
        catchupLimit time.Duration
        wake         chan struct{}
        failures     chan error
@@ -212,20 +223,27 @@ func (i *WhatsAppInbox) startCatchup() {
        })
 }
 
-// receive puts the text of event into its batch and wakes Run. link gives the
-// device and the own IDs. It returns false when the state file cannot hold the
+// receive stores an image before its batch and wakes Run. link gives the device
+// and the own IDs. It returns false when the state file cannot hold the
 // message, so that the library does not acknowledge it, and Run stops the
-// daemon. A message that is not text, or that the batches do not take, is
-// handled with nothing to do.
-func (i *WhatsAppInbox) receive(link WhatsAppLink, event *events.Message) bool {
-       ctx, cancel := context.WithTimeout(context.Background(), whatsappLookupTime)
+// daemon. An image that cannot be downloaded or stored gets a text marker. A
+// message that the batches do not take is handled with nothing to do.
+func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *events.Message) bool {
+       lookupCtx, cancel := context.WithTimeout(ctx, i.lookupTime)
        defer cancel()
 
-       chat, message, found := whatsappMessage(ctx, link.device, link.own, event)
+       chat, message, found := whatsappMessage(lookupCtx, link.device, link.own, event)
        if !found {
                return true
        }
 
+       if event.Message.GetImageMessage() != nil {
+               ctx, cancel := context.WithTimeout(ctx, i.imageTime)
+               defer cancel()
+
+               message.Text = i.imageText(ctx, link, event, message.Text)
+       }
+
        _, err := i.state.Add(chat, message, time.Now())
        if err != nil {
                select {
@@ -244,6 +262,28 @@ func (i *WhatsAppInbox) receive(link WhatsAppLink, event *events.Message) bool {
        return true
 }
 
+// imageText stores the image of event before returning its marker and caption.
+// Failures keep the caption with a text marker. The daemon log identifies the
+// chat by a short hash, not its phone number.
+func (i *WhatsAppInbox) imageText(ctx context.Context, link WhatsAppLink, event *events.Message, caption string) string {
+       name, err := whatsappImage(ctx, link.client, i.images, event.Message.GetImageMessage())
+       if err != nil {
+               chat := event.Info.Chat.ToNonAD()
+               digest := sha256.Sum256([]byte(chat.String()))
+               redact := append(slices.Clone(link.own), chat.User)
+               i.logger.Error("image receive failed", "chat", fmt.Sprintf("%x", digest[:6]), "err", whatsappRedact(redact, err.Error()))
+
+               text := "(image not downloaded)"
+               if caption != "" {
+                       text += "\n" + caption
+               }
+
+               return text
+       }
+
+       return imageText([]string{name}, caption)
+}
+
 // caughtUpBy ends the catch-up of the connection with the number catchups, when
 // no later connection started a new one.
 func (i *WhatsAppInbox) caughtUpBy(catchups int) {
@@ -303,10 +343,10 @@ func (i *WhatsAppInbox) close() {
 
 // whatsappMessage returns the chat and the batch message of event, or false
 // when the batches do not take it. device is the linked device, and own holds
-// the own IDs. The batches take the text of a private chat or a group, from
-// another person or from the user. They do not take media, reactions, edits,
-// deletions, view-once and disappearing messages, old history, status
-// broadcasts, channels, and the chat of the user with itself.
+// the own IDs. The batches take text, images, and media markers from a private
+// chat or a group, from another person or from the user. They do not take
+// reactions, edits, deletions, view-once and disappearing messages, old
+// history, status broadcasts, channels, and the chat of the user with itself.
 //
 // A mention of the user becomes @you, another mention @NAME when the name is
 // known, and the own number and LID become OWN, so that they never go to the
@@ -316,7 +356,7 @@ func whatsappMessage(ctx context.Context, device *store.Device, own []string, ev
        chatJID := info.Chat.ToNonAD()
        switch {
        case device == nil:
-       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.SourceWebMsg != nil:
+       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.Message.GetImageMessage().GetViewOnce(), event.Message.GetVideoMessage().GetViewOnce(), event.SourceWebMsg != nil:
        case chatJID.Server != types.DefaultUserServer && chatJID.Server != types.HiddenUserServer && chatJID.Server != types.GroupServer:
        case slices.Contains(own, chatJID.User):
        default:
@@ -327,7 +367,7 @@ func whatsappMessage(ctx context.Context, device *store.Device, own []string, ev
 }
 
 // whatsappText returns the chat and the batch message of event, a message that
-// the batches take when it has text.
+// the batches take. An image returns its caption until receive stores it.
 func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
        info := event.Info
        text := event.Message.GetConversation()
@@ -336,11 +376,33 @@ func whatsappText(ctx context.Context, device *store.Device, own []string, event
                text = extended.GetText()
        }
 
-       if strings.TrimSpace(text) == "" {
-               return WhatsAppChat{}, WhatsAppMessage{}, false
+       contextInfo := extended.GetContextInfo()
+       switch {
+       case event.Message.GetImageMessage() != nil:
+               image := event.Message.GetImageMessage()
+               text = image.GetCaption()
+               contextInfo = image.GetContextInfo()
+       case event.Message.GetStickerMessage() != nil:
+               text = "(sticker)"
+               contextInfo = event.Message.GetStickerMessage().GetContextInfo()
+       case event.Message.GetVideoMessage() != nil:
+               video := event.Message.GetVideoMessage()
+               text = "(video)"
+               if video.GetGifPlayback() {
+                       text = "(GIF)"
+               }
+
+               if video.GetCaption() != "" {
+                       text += "\n" + video.GetCaption()
+               }
+
+               contextInfo = video.GetContextInfo()
+       default:
+               if strings.TrimSpace(text) == "" {
+                       return WhatsAppChat{}, WhatsAppMessage{}, false
+               }
        }
 
-       contextInfo := extended.GetContextInfo()
        message := WhatsAppMessage{
                ID:   info.ID,
                Time: info.Timestamp,