]> git.giacomolli.uk - onechat/blobdiff - whatsapp_inbox.go
Check forced-disconnect test setup failures
[onechat] / whatsapp_inbox.go
index cff43c706607576d5ba9a0cfe447adb627a45de1..8bf8a97eaee2dfa431012a539d9af667d4829952 100644 (file)
@@ -4,6 +4,7 @@ import (
        "context"
        "fmt"
        "log/slog"
        "context"
        "fmt"
        "log/slog"
+       "path/filepath"
        "slices"
        "strings"
        "sync"
        "slices"
        "strings"
        "sync"
@@ -31,21 +32,6 @@ const (
        whatsappCatchupTimeout = time.Minute
 )
 
        whatsappCatchupTimeout = time.Minute
 )
 
-// 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. 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,
-               wake:     make(chan struct{}, 1),
-               failures: make(chan error, 1),
-       }
-}
-
 // 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.
 // 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.
@@ -73,6 +59,8 @@ type WhatsAppInbox struct {
        clock    func() time.Time
        groups   *WhatsAppGroups
        images   *ImageStore
        clock    func() time.Time
        groups   *WhatsAppGroups
        images   *ImageStore
+       voices   *VoiceStore
+       voice    *VoiceTranscriber
        logger   *slog.Logger
        wake     chan struct{}
        failures chan error
        logger   *slog.Logger
        wake     chan struct{}
        failures chan error
@@ -84,6 +72,24 @@ type WhatsAppInbox struct {
        catchupTimer *time.Timer
 }
 
        catchupTimer *time.Timer
 }
 
+// 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. images
+// holds shared images, and logger gets image failures. Audio shares the data
+// directory of state.
+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,
+               voices:   &VoiceStore{dir: filepath.Join(filepath.Dir(filepath.Dir(state.path)), voiceDirName)},
+               voice:    &VoiceTranscriber{},
+               logger:   logger,
+               wake:     make(chan struct{}, 1),
+               failures: make(chan error, 1),
+       }
+}
+
 // due returns a channel that gets a value when the next open batch is due, or
 // nil when no batch is open.
 func (i *WhatsAppInbox) due() <-chan time.Time {
 // due returns a channel that gets a value when the next open batch is due, or
 // nil when no batch is open.
 func (i *WhatsAppInbox) due() <-chan time.Time {
@@ -123,7 +129,7 @@ func (i *WhatsAppInbox) deliver(ctx context.Context, l *TurnLoop, link WhatsAppL
        }
 
        if err != nil {
        }
 
        if err != nil {
-               return err
+               return fmt.Errorf("close due WhatsApp batches: %w", err)
        }
 
        batches := i.state.Closed()
        }
 
        batches := i.state.Closed()
@@ -177,7 +183,7 @@ func (i *WhatsAppInbox) deliver(ctx context.Context, l *TurnLoop, link WhatsAppL
 
                err = i.state.Remove(batch.Number)
                if err != nil {
 
                err = i.state.Remove(batch.Number)
                if err != nil {
-                       return err
+                       return fmt.Errorf("remove delivered WhatsApp batch: %w", err)
                }
 
                if batch.Turn() {
                }
 
                if batch.Turn() {
@@ -224,10 +230,40 @@ func (i *WhatsAppInbox) startCatchup() {
        })
 }
 
        })
 }
 
-// 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. An image that cannot be downloaded or stored gets a text marker. A
+// 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) {
+       i.mutex.Lock()
+       defer i.mutex.Unlock()
+
+       if i.catchups == catchups {
+               i.finishCatchup()
+       }
+}
+
+// finishCatchup ends a running catch-up and wakes Run, which logs all batches.
+// The caller holds the mutex.
+func (i *WhatsAppInbox) finishCatchup() {
+       if !i.catchingUp {
+               return
+       }
+
+       i.catchingUp = false
+       i.caughtUp = true
+       if i.catchupTimer != nil {
+               i.catchupTimer.Stop()
+       }
+
+       select {
+       case i.wake <- struct{}{}:
+       default:
+       }
+}
+
+// receive stores images and transcribed voice notes before their batches 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. Failed media input 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, whatsappContactLookupTimeout)
 // 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, whatsappContactLookupTimeout)
@@ -243,6 +279,8 @@ func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *e
                defer cancel()
 
                message.Text = i.imageText(ctx, link, event, message.Text)
                defer cancel()
 
                message.Text = i.imageText(ctx, link, event, message.Text)
+       } else if event.Message.GetAudioMessage().GetPTT() {
+               message.Text = whatsappRedact(link.own, whatsappVoice(ctx, link.client, i.voices, i.voice, event.Message.GetAudioMessage()))
        }
 
        _, err := i.state.Add(chat, message, time.Now())
        }
 
        _, err := i.state.Add(chat, message, time.Now())
@@ -284,36 +322,6 @@ func (i *WhatsAppInbox) imageText(ctx context.Context, link WhatsAppLink, event
        return imageText([]string{name}, caption)
 }
 
        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) {
-       i.mutex.Lock()
-       defer i.mutex.Unlock()
-
-       if i.catchups == catchups {
-               i.finishCatchup()
-       }
-}
-
-// finishCatchup ends a running catch-up and wakes Run, which logs all batches.
-// The caller holds the mutex.
-func (i *WhatsAppInbox) finishCatchup() {
-       if !i.catchingUp {
-               return
-       }
-
-       i.catchingUp = false
-       i.caughtUp = true
-       if i.catchupTimer != nil {
-               i.catchupTimer.Stop()
-       }
-
-       select {
-       case i.wake <- struct{}{}:
-       default:
-       }
-}
-
 // endCatchup ends a running catch-up at OfflineSyncCompleted.
 func (i *WhatsAppInbox) endCatchup() {
        i.mutex.Lock()
 // endCatchup ends a running catch-up at OfflineSyncCompleted.
 func (i *WhatsAppInbox) endCatchup() {
        i.mutex.Lock()
@@ -356,7 +364,7 @@ func whatsappMessage(ctx context.Context, device *store.Device, own []string, ev
        chatJID := info.Chat.ToNonAD()
        switch {
        case device == nil:
        chatJID := info.Chat.ToNonAD()
        switch {
        case device == nil:
-       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.Message.GetImageMessage().GetViewOnce(), event.Message.GetVideoMessage().GetViewOnce(), event.SourceWebMsg != nil:
+       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.Message.GetImageMessage().GetViewOnce(), event.Message.GetVideoMessage().GetViewOnce(), event.Message.GetAudioMessage().GetViewOnce(), event.SourceWebMsg != nil:
        case chatJID.Server != types.DefaultUserServer && chatJID.Server != types.HiddenUserServer && chatJID.Server != types.GroupServer:
        case slices.Contains(own, chatJID.User):
        default:
        case chatJID.Server != types.DefaultUserServer && chatJID.Server != types.HiddenUserServer && chatJID.Server != types.GroupServer:
        case slices.Contains(own, chatJID.User):
        default:
@@ -367,7 +375,8 @@ func whatsappMessage(ctx context.Context, device *store.Device, own []string, ev
 }
 
 // whatsappText returns the chat and the batch message of event, a message that
 }
 
 // whatsappText returns the chat and the batch message of event, a message that
-// the batches take. An image returns its caption until receive stores it.
+// the batches take. An image returns its caption until receive stores it. A
+// voice note returns a marker until receive transcribes it.
 func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
        info := event.Info
        text := event.Message.GetConversation()
 func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
        info := event.Info
        text := event.Message.GetConversation()
@@ -378,6 +387,9 @@ func whatsappText(ctx context.Context, device *store.Device, own []string, event
 
        contextInfo := extended.GetContextInfo()
        switch {
 
        contextInfo := extended.GetContextInfo()
        switch {
+       case event.Message.GetAudioMessage().GetPTT():
+               text = "(voice not downloaded)"
+               contextInfo = event.Message.GetAudioMessage().GetContextInfo()
        case event.Message.GetImageMessage() != nil:
                image := event.Message.GetImageMessage()
                text = image.GetCaption()
        case event.Message.GetImageMessage() != nil:
                image := event.Message.GetImageMessage()
                text = image.GetCaption()