]> git.giacomolli.uk - onechat/blob - whatsapp_send.go
Reflow comments over the reflow width
[onechat] / whatsapp_send.go
1 package main
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "log/slog"
10 "slices"
11 "strings"
12 "time"
13
14 "go.mau.fi/whatsmeow"
15 "go.mau.fi/whatsmeow/proto/waE2E"
16 "go.mau.fi/whatsmeow/types"
17 )
18
19 const (
20 // whatsappSendTimeout is the longest time of a send.
21 whatsappSendTimeout = 30 * time.Second
22
23 // whatsappSendBytes is the most bytes of the text of a send.
24 whatsappSendBytes = 65536
25
26 // whatsappSendIDBytes is the most bytes of the ID of a send.
27 whatsappSendIDBytes = 100
28
29 // The states of a send record.
30 whatsappSending = "sending"
31 whatsappSent = "sent"
32 whatsappFailed = "failed"
33 )
34
35 // whatsappSend sends text and an optional stored image to chat, the ID of a
36 // private chat or a group, and returns the answer of the tool. id names the
37 // send: a call with an id that has a record gives the answer of the record and
38 // sends nothing, and the same id with another chat, text, or image is an error.
39 // The record "sending" is durable before the send, so a stop of the daemon
40 // during the send gives an answer that the result is unknown, and not a second
41 // send. A disconnected WhatsApp sends nothing and makes no record. link gives
42 // the device, the client, and the own IDs, groups the names of the groups, and
43 // state the send records, and logger gets one line for each send. The clock
44 // gives the times of the records and the time zone of the answer. Image names
45 // are checked before the record is written, and uploads start only after the
46 // record is durable. Text-only records keep their text digest.
47 func whatsappSend(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, state *WhatsAppState, images *ImageStore, logger *slog.Logger, clock func() time.Time, id, chat, text, image string) (string, error) {
48 if !link.linked {
49 return whatsappNotLinked, nil
50 }
51
52 jid, err := whatsappSendCheck(id, chat, text, image, link.own)
53 if err != nil {
54 return "", err
55 }
56
57 digest := sha256.Sum256([]byte(text))
58 record := WhatsAppSend{
59 ID: id,
60 Image: image,
61 Chat: chat,
62 Digest: hex.EncodeToString(digest[:]),
63 State: whatsappSending,
64 Time: clock(),
65 }
66
67 held, found := state.Send(id)
68 switch {
69 case found && (held.Chat != record.Chat || held.Digest != record.Digest || held.Image != record.Image):
70 return "", fmt.Errorf("the id %s belongs to another message: give a new id for each message", id)
71 case found && held.State == whatsappSending:
72 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
73 case found:
74 return held.Answer + " This is the answer of the first call with this id: nothing was sent again.", nil
75 case link.client == nil || !link.connected:
76 return "", errors.New("WhatsApp is disconnected, so nothing was sent: try again later")
77 }
78
79 var data []byte
80
81 if image != "" {
82 data, err = images.Read(image)
83 if err != nil {
84 return "", fmt.Errorf("read image to send: %w", err)
85 }
86 }
87
88 record.MessageID = link.client.GenerateMessageID()
89 err = state.PutSend(record)
90 if err != nil {
91 return "", err
92 }
93
94 message := &waE2E.Message{Conversation: new(text)}
95 if image != "" {
96 message, err = whatsappImageMessage(ctx, link.client, data, text)
97 }
98
99 sendCtx, cancel := context.WithTimeout(ctx, whatsappSendTimeout)
100 defer cancel()
101
102 var response whatsmeow.SendResponse
103
104 if err == nil {
105 response, err = link.client.SendMessage(sendCtx, jid, message, whatsmeow.SendRequestExtra{
106 ID: record.MessageID,
107 })
108 if err != nil && image != "" {
109 err = fmt.Errorf("send image: %w", whatsappImageSendError(err))
110 }
111 }
112
113 record.State = whatsappSent
114 if err != nil {
115 record.State = whatsappFailed
116 record.Answer = "The send failed: " + whatsappRedact(link.own, err.Error()) + "."
117 logger.Warn("message send failed", "chat", whatsappChatHash(jid), "image", image != "", "err", whatsappRedact(append(slices.Clone(link.own), jid.User), err.Error()))
118 } else {
119 logger.Info("message sent", "chat", whatsappChatHash(jid), "image", image != "")
120
121 name := groups.name(ctx, link, jid.String())
122 if jid.Server != types.GroupServer {
123 name = whatsappContactName(ctx, link.device, jid, "")
124 }
125
126 record.Answer = "Sent to " + name + " at " + response.Timestamp.In(clock().Location()).Format("15:04") + "."
127 }
128
129 err = state.PutSend(record)
130 if err != nil {
131 return "", err
132 }
133
134 return record.Answer, nil
135 }
136
137 // whatsappSendCheck returns the JID of chat, and an error when id, chat, or
138 // text cannot go to a send. Text can be empty only with an image. The chat is a
139 // private chat or a group, and not the chat of the user with itself.
140 func whatsappSendCheck(id, chat, text, image string, own []string) (types.JID, error) {
141 jid, err := types.ParseJID(chat)
142 switch {
143 case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes:
144 return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes)
145 case (strings.TrimSpace(text) == "" && image == "") || len(text) > whatsappSendBytes:
146 return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes)
147 case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat:
148 return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat)
149 case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer:
150 return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat)
151 case slices.Contains(own, jid.User):
152 return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat")
153 }
154
155 return jid, nil
156 }