]> git.giacomolli.uk - onechat/blobdiff - whatsapp.go
Convert WhatsApp events with pure functions
[onechat] / whatsapp.go
index 4268a168fa1d4069f79b61b46b60119f6dc8fb32..44b9dfa6557336a241348894e8f681c976ea9dc3 100644 (file)
@@ -330,7 +330,7 @@ func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) err
                                pairing.Go(func() {
                                        defer cancelPair()
 
-                                       w.pair(pairCtx, l, request.phone, request.answer)
+                                       w.pair(pairCtx, ctx, l, request.phone, request.answer)
                                })
                        }
                }
@@ -797,7 +797,13 @@ func (w *WhatsApp) removeSession() error {
 // number, an expired code, or a failure. A pairing that a new one ends gives no
 // answer. A link notes "WhatsApp is linked." in the log of l, with no number.
 // The client of a pairing that does not link is disconnected.
-func (w *WhatsApp) pair(ctx context.Context, l *TurnLoop, phone string, answer func(text string)) {
+//
+// ctx limits the pairing, but the connection uses runCtx, the context of Run.
+// The library keeps the context of the connection for its keep-alive, its event
+// handler, and its reconnections, and after the link the server ends the stream
+// and the library connects again. With ctx, the end of the pairing would stop
+// that reconnection, and the linked device would never connect.
+func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, answer func(text string)) {
        device, err := w.newDevice(ctx)
        if err != nil {
                answer("The pairing failed: " + err.Error())
@@ -814,7 +820,7 @@ func (w *WhatsApp) pair(ctx context.Context, l *TurnLoop, phone string, answer f
 
        qr, err := client.GetQRChannel(ctx)
        if err == nil {
-               err = client.ConnectContext(ctx)
+               err = client.ConnectContext(runCtx)
        }
 
        if err != nil {
@@ -1013,7 +1019,12 @@ func (w *WhatsApp) receive(event *events.Message) bool {
        ctx, cancel := context.WithTimeout(context.Background(), whatsappLookupTime)
        defer cancel()
 
-       chat, message, found := w.message(ctx, event)
+       w.mutex.Lock()
+       device := w.device
+       own := slices.Clone(w.own)
+       w.mutex.Unlock()
+
+       chat, message, found := whatsappMessage(ctx, device, own, event)
        if !found {
                return true
        }
@@ -1036,98 +1047,6 @@ func (w *WhatsApp) receive(event *events.Message) bool {
        return true
 }
 
-// message returns the chat and the batch message of event, or false when the
-// batches do not take it. They 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.
-//
-// 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
-// log.
-func (w *WhatsApp) message(ctx context.Context, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
-       w.mutex.Lock()
-       device := w.device
-       own := slices.Clone(w.own)
-       w.mutex.Unlock()
-
-       info := event.Info
-       chatJID := info.Chat.ToNonAD()
-       switch {
-       case device == nil:
-       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.SourceWebMsg != nil:
-       case chatJID.Server != types.DefaultUserServer && chatJID.Server != types.HiddenUserServer && chatJID.Server != types.GroupServer:
-       case slices.Contains(own, chatJID.User):
-       default:
-               return w.text(ctx, device, own, event)
-       }
-
-       return WhatsAppChat{}, WhatsAppMessage{}, false
-}
-
-// text returns the chat and the batch message of event, a message that the
-// batches take when it has text.
-func (w *WhatsApp) text(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
-       info := event.Info
-       text := event.Message.GetConversation()
-       extended := event.Message.GetExtendedTextMessage()
-       if text == "" {
-               text = extended.GetText()
-       }
-
-       if strings.TrimSpace(text) == "" {
-               return WhatsAppChat{}, WhatsAppMessage{}, false
-       }
-
-       contextInfo := extended.GetContextInfo()
-       message := WhatsAppMessage{
-               ID:   info.ID,
-               Time: info.Timestamp,
-               Mine: info.IsFromMe,
-       }
-
-       if !message.Mine {
-               message.Sender = whatsappContactName(ctx, device, info.Sender, info.PushName)
-       }
-
-       for _, mentioned := range contextInfo.GetMentionedJID() {
-               jid, err := types.ParseJID(mentioned)
-               switch {
-               case err != nil:
-               case slices.Contains(own, jid.User):
-                       message.Mention = true
-                       text = strings.ReplaceAll(text, "@"+jid.User, "@you")
-               default:
-                       text = strings.ReplaceAll(text, "@"+jid.User, "@"+whatsappContactName(ctx, device, jid, ""))
-               }
-       }
-
-       participant, err := types.ParseJID(contextInfo.GetParticipant())
-       message.Reply = err == nil && slices.Contains(own, participant.User)
-
-       for _, id := range own {
-               text = strings.ReplaceAll(text, id, "OWN")
-       }
-
-       message.Text = text
-       chatJID := info.Chat.ToNonAD()
-       chat := WhatsAppChat{
-               ID:    chatJID.String(),
-               Group: chatJID.Server == types.GroupServer,
-       }
-
-       if !chat.Group {
-               push := info.PushName
-               if message.Mine {
-                       push = ""
-               }
-
-               chat.Name = whatsappContactName(ctx, device, chatJID, push)
-       }
-
-       return chat, message, true
-}
-
 // caughtUpBy ends the catch-up of the connection with the number catchups, when
 // no later connection started a new one.
 func (w *WhatsApp) caughtUpBy(catchups int) {
@@ -1195,11 +1114,7 @@ func (w *WhatsApp) redact(text string) string {
        w.mutex.Lock()
        defer w.mutex.Unlock()
 
-       for _, id := range w.own {
-               text = strings.ReplaceAll(text, id, "OWN")
-       }
-
-       return text
+       return whatsappRedact(w.own, text)
 }
 
 // Send sends text to chat, the ID of a private chat or a group, and returns the
@@ -1314,6 +1229,90 @@ func whatsappNewClient(device *store.Device, handle func(event any) bool) WhatsA
        return client
 }
 
+// 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.
+//
+// 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
+// log.
+func whatsappMessage(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
+       info := event.Info
+       chatJID := info.Chat.ToNonAD()
+       switch {
+       case device == nil:
+       case event.IsEdit, info.Edit != "", event.IsEphemeral, event.IsViewOnce, event.SourceWebMsg != nil:
+       case chatJID.Server != types.DefaultUserServer && chatJID.Server != types.HiddenUserServer && chatJID.Server != types.GroupServer:
+       case slices.Contains(own, chatJID.User):
+       default:
+               return whatsappText(ctx, device, own, event)
+       }
+
+       return WhatsAppChat{}, WhatsAppMessage{}, false
+}
+
+// whatsappText returns the chat and the batch message of event, a message that
+// the batches take when it has text.
+func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
+       info := event.Info
+       text := event.Message.GetConversation()
+       extended := event.Message.GetExtendedTextMessage()
+       if text == "" {
+               text = extended.GetText()
+       }
+
+       if strings.TrimSpace(text) == "" {
+               return WhatsAppChat{}, WhatsAppMessage{}, false
+       }
+
+       contextInfo := extended.GetContextInfo()
+       message := WhatsAppMessage{
+               ID:   info.ID,
+               Time: info.Timestamp,
+               Mine: info.IsFromMe,
+       }
+
+       if !message.Mine {
+               message.Sender = whatsappContactName(ctx, device, info.Sender, info.PushName)
+       }
+
+       for _, mentioned := range contextInfo.GetMentionedJID() {
+               jid, err := types.ParseJID(mentioned)
+               switch {
+               case err != nil:
+               case slices.Contains(own, jid.User):
+                       message.Mention = true
+                       text = strings.ReplaceAll(text, "@"+jid.User, "@you")
+               default:
+                       text = strings.ReplaceAll(text, "@"+jid.User, "@"+whatsappContactName(ctx, device, jid, ""))
+               }
+       }
+
+       participant, err := types.ParseJID(contextInfo.GetParticipant())
+       message.Reply = err == nil && slices.Contains(own, participant.User)
+
+       message.Text = whatsappRedact(own, text)
+       chatJID := info.Chat.ToNonAD()
+       chat := WhatsAppChat{
+               ID:    chatJID.String(),
+               Group: chatJID.Server == types.GroupServer,
+       }
+
+       if !chat.Group {
+               push := info.PushName
+               if message.Mine {
+                       push = ""
+               }
+
+               chat.Name = whatsappContactName(ctx, device, chatJID, push)
+       }
+
+       return chat, message, true
+}
+
 // whatsappContactName returns the name of the person jid: the full name, the
 // first name, the business name, or the push name of the contact in the session
 // of device, then push, the push name of a message, then the phone number. A
@@ -1427,6 +1426,15 @@ func whatsappOwnIDs(ids ...string) []string {
        return own
 }
 
+// whatsappRedact replaces each of the own IDs own in text with OWN.
+func whatsappRedact(own []string, text string) string {
+       for _, id := range own {
+               text = strings.ReplaceAll(text, id, "OWN")
+       }
+
+       return text
+}
+
 // whatsappSessionAddress returns the SQLite address of the session database at
 // path, with foreign keys on, as the library needs, and a wait for a busy
 // database.