import (
"context"
- "crypto/sha256"
"fmt"
"log/slog"
+ "path/filepath"
"slices"
"strings"
"sync"
)
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),
//
// 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
clock func() time.Time
groups *WhatsAppGroups
images *ImageStore
+ voices *VoiceStore
+ voice *VoiceTranscriber
logger *slog.Logger
wake chan struct{}
failures chan error
}
// 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()
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)
}
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())
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 != "" {
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()