"context"
"fmt"
"log/slog"
+ "path/filepath"
"slices"
"strings"
"sync"
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.
clock func() time.Time
groups *WhatsAppGroups
images *ImageStore
+ voices *VoiceStore
+ voice *VoiceTranscriber
logger *slog.Logger
wake chan struct{}
failures chan error
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 {
}
if err != nil {
- return err
+ return fmt.Errorf("close due WhatsApp batches: %w", err)
}
batches := i.state.Closed()
err = i.state.Remove(batch.Number)
if err != nil {
- return err
+ return fmt.Errorf("remove delivered WhatsApp batch: %w", err)
}
if batch.Turn() {
})
}
-// 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)
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())
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()
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:
}
// 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()
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()