]> git.giacomolli.uk - onechat/blob - whatsapp_send.go
Keep retries after a rejected pair request
[onechat] / whatsapp_send.go
1 package main
2
3 import (
4         "context"
5         "crypto/sha256"
6         "encoding/hex"
7         "errors"
8         "fmt"
9         "slices"
10         "strings"
11         "time"
12
13         "go.mau.fi/whatsmeow"
14         "go.mau.fi/whatsmeow/proto/waE2E"
15         "go.mau.fi/whatsmeow/types"
16 )
17
18 const (
19         // whatsappSendTime is the longest time of a send.
20         whatsappSendTime = 30 * time.Second
21
22         // whatsappSendBytes is the most bytes of the text of a send.
23         whatsappSendBytes = 65536
24
25         // whatsappSendIDBytes is the most bytes of the ID of a send.
26         whatsappSendIDBytes = 100
27
28         // The states of a send record.
29         whatsappSending = "sending"
30         whatsappSent    = "sent"
31         whatsappFailed  = "failed"
32 )
33
34 // whatsappSend sends text to chat, the ID of a private chat or a group, and
35 // returns the answer of the tool. id names the send: a call with an id that has
36 // a record gives the answer of the record and sends nothing, and the same id
37 // with another chat or text is an error. The record "sending" is durable before
38 // the send, so a stop of the daemon during the send gives an answer that the
39 // result is unknown, and not a second send. A disconnected WhatsApp sends
40 // nothing and makes no record. link gives the device, the client, and the own
41 // IDs, groups the names of the groups, and state the send records. The clock
42 // gives the times of the records and the time zone of the answer.
43 func whatsappSend(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, state *WhatsAppState, clock func() time.Time, id, chat, text string) (string, error) {
44         if !link.linked {
45                 return whatsappNotLinked, nil
46         }
47
48         jid, err := whatsappSendCheck(id, chat, text, link.own)
49         if err != nil {
50                 return "", err
51         }
52
53         digest := sha256.Sum256([]byte(text))
54         record := WhatsAppSend{
55                 ID:     id,
56                 Chat:   chat,
57                 Digest: hex.EncodeToString(digest[:]),
58                 State:  whatsappSending,
59                 Time:   clock(),
60         }
61
62         held, found := state.Send(id)
63         switch {
64         case found && (held.Chat != record.Chat || held.Digest != record.Digest):
65                 return "", fmt.Errorf("the id %s belongs to another message: give a new id for each message", id)
66         case found && held.State == whatsappSending:
67                 return "The send with this id started, but its result is unknown, because the daemon stopped during it. Ask the user to look at the chat in WhatsApp before you send the message again with a new id.", nil
68         case found:
69                 return held.Answer + " This is the answer of the first call with this id: nothing was sent again.", nil
70         case link.client == nil || !link.connected:
71                 return "", errors.New("WhatsApp is disconnected, so nothing was sent: try again later")
72         }
73
74         record.MessageID = link.client.GenerateMessageID()
75         err = state.PutSend(record)
76         if err != nil {
77                 return "", err
78         }
79
80         sendCtx, cancel := context.WithTimeout(ctx, whatsappSendTime)
81         defer cancel()
82
83         response, err := link.client.SendMessage(sendCtx, jid, &waE2E.Message{
84                 Conversation: new(text),
85         }, whatsmeow.SendRequestExtra{
86                 ID: record.MessageID,
87         })
88
89         record.State = whatsappSent
90         if err != nil {
91                 record.State = whatsappFailed
92                 record.Answer = "The send failed: " + whatsappRedact(link.own, err.Error()) + "."
93         } else {
94                 name := groups.name(ctx, link, jid.String())
95                 if jid.Server != types.GroupServer {
96                         name = whatsappContactName(ctx, link.device, jid, "")
97                 }
98
99                 record.Answer = "Sent to " + name + " at " + response.Timestamp.In(clock().Location()).Format("15:04") + "."
100         }
101
102         err = state.PutSend(record)
103         if err != nil {
104                 return "", err
105         }
106
107         return record.Answer, nil
108 }
109
110 // whatsappSendCheck returns the JID of chat, and an error when id, chat, or
111 // text cannot go to a send. The chat is a private chat or a group, and not the
112 // chat of the user with itself.
113 func whatsappSendCheck(id, chat, text string, own []string) (types.JID, error) {
114         jid, err := types.ParseJID(chat)
115         switch {
116         case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes:
117                 return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes)
118         case strings.TrimSpace(text) == "" || len(text) > whatsappSendBytes:
119                 return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes)
120         case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat:
121                 return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat)
122         case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer:
123                 return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat)
124         case slices.Contains(own, jid.User):
125                 return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat")
126         }
127
128         return jid, nil
129 }