]> git.giacomolli.uk - onechat/blobdiff - whatsapp_inbox.go
Order daemon and storage declarations
[onechat] / whatsapp_inbox.go
index 392593fda5109d1f4ba9f921b9baa6eb1d169562..c9ca81b666148d4a43795a1b9a3d38fdb8cde0dd 100644 (file)
@@ -2,9 +2,9 @@ package main
 
 import (
        "context"
-       "crypto/sha256"
        "fmt"
        "log/slog"
+       "path/filepath"
        "slices"
        "strings"
        "sync"
@@ -16,30 +16,34 @@ import (
 )
 
 const (
-       // whatsappLookupTime is the longest time of a read of names in the
-       // session.
-       whatsappLookupTime = 10 * time.Second
+       // whatsappContactLookupTimeout is the longest time of a read of names
+       // in the session.
+       whatsappContactLookupTimeout = 10 * time.Second
 
-       // whatsappImageTime is the longest time of an image download.
-       whatsappImageTime = 2 * time.Minute
+       // whatsappImageDownloadTimeout is the longest time of an image
+       // download.
+       whatsappImageDownloadTimeout = 2 * time.Minute
 
        // whatsappSource is the source of the work events of the batches.
        whatsappSource = "whatsapp"
 
-       // whatsappCatchupLimit is the longest wait for the end of the catch-up
-       // after a connection.
-       whatsappCatchupLimit = time.Minute
+       // whatsappCatchupTimeout is the longest wait for the end of the
+       // catch-up after a connection.
+       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.
+// 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),
@@ -52,7 +56,7 @@ func newWhatsAppInbox(state *WhatsAppState, clock func() time.Time, groups *What
 //
 // After each connection, the servers send the messages that came while the
 // daemon was away, and then OfflineSyncCompleted. Until then, or until
-// whatsappCatchupLimit passes, the closed batches wait in the state file. At
+// whatsappCatchupTimeout passes, the closed batches wait in the state file. At
 // the end of the catch-up, deliver closes all batches and logs them. It wakes
 // the root once for its batches, so their catch-up gives one turn, not one for
 // each chat. The batches of the state file at the start wait for the first
@@ -73,6 +77,8 @@ type WhatsAppInbox struct {
        clock    func() time.Time
        groups   *WhatsAppGroups
        images   *ImageStore
+       voices   *VoiceStore
+       voice    *VoiceTranscriber
        logger   *slog.Logger
        wake     chan struct{}
        failures chan error
@@ -207,7 +213,7 @@ func (i *WhatsAppInbox) waitForCatchup() {
 }
 
 // startCatchup starts the catch-up of a new connection, which ends at
-// endCatchup or after whatsappCatchupLimit.
+// endCatchup or after whatsappCatchupTimeout.
 func (i *WhatsAppInbox) startCatchup() {
        i.mutex.Lock()
        defer i.mutex.Unlock()
@@ -219,18 +225,18 @@ func (i *WhatsAppInbox) startCatchup() {
                i.catchupTimer.Stop()
        }
 
-       i.catchupTimer = time.AfterFunc(whatsappCatchupLimit, func() {
+       i.catchupTimer = time.AfterFunc(whatsappCatchupTimeout, func() {
                i.caughtUpBy(catchups)
        })
 }
 
-// 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
+// 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, whatsappLookupTime)
+       lookupCtx, cancel := context.WithTimeout(ctx, whatsappContactLookupTimeout)
        defer cancel()
 
        chat, message, found := whatsappMessage(lookupCtx, link.device, link.own, event)
@@ -239,10 +245,12 @@ func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *e
        }
 
        if event.Message.GetImageMessage() != nil {
-               ctx, cancel := context.WithTimeout(ctx, whatsappImageTime)
+               ctx, cancel := context.WithTimeout(ctx, whatsappImageDownloadTimeout)
                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())
@@ -270,9 +278,8 @@ func (i *WhatsAppInbox) imageText(ctx context.Context, link WhatsAppLink, event
        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()))
+               i.logger.Error("image receive failed", "chat", whatsappChatHash(chat), "err", whatsappRedact(redact, err.Error()))
 
                text := "(image not downloaded)"
                if caption != "" {
@@ -357,7 +364,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.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:
@@ -368,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
-// 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()
@@ -379,6 +387,9 @@ func whatsappText(ctx context.Context, device *store.Device, own []string, event
 
        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()