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