From: Anderson A. Giacomolli Date: Sun, 8 Nov 2026 21:40:53 +0000 (-0300) Subject: Move whatsapp_send to its own file X-Git-Url: http://git.giacomolli.uk/onechat/commitdiff_plain/33aa709aa4e59e05ada6940a1c055f5e5646a418?ds=sidebyside Move whatsapp_send to its own file The send of whatsapp_send is now the function whatsappSend in whatsapp_send.go, with its check of the arguments and its constants. It owns no state: it gets a WhatsAppLink for the device, the client, the connection, and the own IDs, the WhatsAppGroups for the name of a group in the answer, and the state file for the send records. WhatsApp.Send stays the entry point of the tool and gives the current link. The tests of the tool move to whatsapp_send_test.go. The behavior does not change. Issue: 6f69ca1 --- diff --git a/whatsapp.go b/whatsapp.go index 5b07300..b883496 100644 --- a/whatsapp.go +++ b/whatsapp.go @@ -2,8 +2,6 @@ package main import ( "context" - "crypto/sha256" - "encoding/hex" "errors" "fmt" "log" @@ -22,7 +20,6 @@ import ( "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" - // The session store of the library opens SQLite with this pure Go // driver. _ "modernc.org/sqlite" @@ -77,20 +74,6 @@ const ( // whatsappNotLinked is the answer of a WhatsApp tool without a link. whatsappNotLinked = "WhatsApp is not linked. Tell the user to run /whatsapp pair PHONE in the terminal or in Telegram." - - // whatsappSendTime is the longest time of a send. - whatsappSendTime = 30 * time.Second - - // whatsappSendBytes is the most bytes of the text of a send. - whatsappSendBytes = 65536 - - // whatsappSendIDBytes is the most bytes of the ID of a send. - whatsappSendIDBytes = 100 - - // The states of a send record. - whatsappSending = "sending" - whatsappSent = "sent" - whatsappFailed = "failed" ) // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The @@ -356,6 +339,12 @@ func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) { return whatsappFind(ctx, w.link(), w.groups, name) } +// Send returns the answer of whatsapp_send for the send id of text to chat, as +// whatsappSend says. +func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, error) { + return whatsappSend(ctx, w.link(), w.groups, w.state, w.clock, id, chat, text) +} + func (w *WhatsApp) sessionPath() string { return filepath.Join(w.dir, whatsappSessionFileName) } @@ -810,88 +799,6 @@ func (w *WhatsApp) redact(text string) string { return whatsappRedact(w.own, text) } -// Send sends text to chat, the ID of a private chat or a group, and returns the -// answer of the tool. id names the send: a call with an id that has a record -// gives the answer of the record and sends nothing, and the same id with -// another chat or text is an error. The record "sending" is durable before the -// send, so a stop of the daemon during the send gives an answer that the result -// is unknown, and not a second send. A disconnected WhatsApp sends nothing and -// makes no record. -func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, error) { - w.mutex.Lock() - linked := w.linked - device := w.device - client := w.client - connected := w.connected - own := slices.Clone(w.own) - w.mutex.Unlock() - - if !linked { - return whatsappNotLinked, nil - } - - jid, err := whatsappSendCheck(id, chat, text, own) - if err != nil { - return "", err - } - - digest := sha256.Sum256([]byte(text)) - record := WhatsAppSend{ - ID: id, - Chat: chat, - Digest: hex.EncodeToString(digest[:]), - State: whatsappSending, - Time: w.clock(), - } - - held, found := w.state.Send(id) - switch { - case found && (held.Chat != record.Chat || held.Digest != record.Digest): - return "", fmt.Errorf("the id %s belongs to another message: give a new id for each message", id) - case found && held.State == whatsappSending: - 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 - case found: - return held.Answer + " This is the answer of the first call with this id: nothing was sent again.", nil - case client == nil || !connected: - return "", errors.New("WhatsApp is disconnected, so nothing was sent: try again later") - } - - record.MessageID = client.GenerateMessageID() - err = w.state.PutSend(record) - if err != nil { - return "", err - } - - sendCtx, cancel := context.WithTimeout(ctx, whatsappSendTime) - defer cancel() - - response, err := client.SendMessage(sendCtx, jid, &waE2E.Message{ - Conversation: new(text), - }, whatsmeow.SendRequestExtra{ - ID: record.MessageID, - }) - - record.State = whatsappSent - if err != nil { - record.State = whatsappFailed - record.Answer = "The send failed: " + w.redact(err.Error()) + "." - } else { - name := w.groups.name(ctx, w.link(), jid.String()) - if jid.Server != types.GroupServer { - name = whatsappContactName(ctx, device, jid, "") - } - - record.Answer = "Sent to " + name + " at " + response.Timestamp.In(w.clock().Location()).Format("15:04") + "." - } - - err = w.state.PutSend(record) - if err != nil { - return "", err - } - - return record.Answer, nil -} - // WhatsAppLink is a copy of the link of the device at one time, for the parts // of WhatsApp that do not own it: the groups, the batches, find, and send. type WhatsAppLink struct { @@ -967,27 +874,6 @@ func whatsappContactName(ctx context.Context, device *store.Device, jid types.JI } } -// whatsappSendCheck returns the JID of chat, and an error when id, chat, or -// text cannot go to a send. The chat is a private chat or a group, and not the -// chat of the user with itself. -func whatsappSendCheck(id, chat, text string, own []string) (types.JID, error) { - jid, err := types.ParseJID(chat) - switch { - case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes: - return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes) - case strings.TrimSpace(text) == "" || len(text) > whatsappSendBytes: - return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes) - case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat: - return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat) - case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer: - return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat) - case slices.Contains(own, jid.User): - return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat") - } - - return jid, nil -} - // whatsappOwnIDs returns the own number and the own LID that are not empty. func whatsappOwnIDs(ids ...string) []string { var own []string diff --git a/whatsapp_send.go b/whatsapp_send.go new file mode 100644 index 0000000..ce3f70b --- /dev/null +++ b/whatsapp_send.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "slices" + "strings" + "time" + + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" +) + +const ( + // whatsappSendTime is the longest time of a send. + whatsappSendTime = 30 * time.Second + + // whatsappSendBytes is the most bytes of the text of a send. + whatsappSendBytes = 65536 + + // whatsappSendIDBytes is the most bytes of the ID of a send. + whatsappSendIDBytes = 100 + + // The states of a send record. + whatsappSending = "sending" + whatsappSent = "sent" + whatsappFailed = "failed" +) + +// whatsappSend sends text to chat, the ID of a private chat or a group, and +// returns the answer of the tool. id names the send: a call with an id that has +// a record gives the answer of the record and sends nothing, and the same id +// with another chat or text is an error. The record "sending" is durable before +// the send, so a stop of the daemon during the send gives an answer that the +// result is unknown, and not a second send. A disconnected WhatsApp sends +// nothing and makes no record. link gives the device, the client, and the own +// IDs, groups the names of the groups, and state the send records. The clock +// gives the times of the records and the time zone of the answer. +func whatsappSend(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, state *WhatsAppState, clock func() time.Time, id, chat, text string) (string, error) { + if !link.linked { + return whatsappNotLinked, nil + } + + jid, err := whatsappSendCheck(id, chat, text, link.own) + if err != nil { + return "", err + } + + digest := sha256.Sum256([]byte(text)) + record := WhatsAppSend{ + ID: id, + Chat: chat, + Digest: hex.EncodeToString(digest[:]), + State: whatsappSending, + Time: clock(), + } + + held, found := state.Send(id) + switch { + case found && (held.Chat != record.Chat || held.Digest != record.Digest): + return "", fmt.Errorf("the id %s belongs to another message: give a new id for each message", id) + case found && held.State == whatsappSending: + 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 + case found: + return held.Answer + " This is the answer of the first call with this id: nothing was sent again.", nil + case link.client == nil || !link.connected: + return "", errors.New("WhatsApp is disconnected, so nothing was sent: try again later") + } + + record.MessageID = link.client.GenerateMessageID() + err = state.PutSend(record) + if err != nil { + return "", err + } + + sendCtx, cancel := context.WithTimeout(ctx, whatsappSendTime) + defer cancel() + + response, err := link.client.SendMessage(sendCtx, jid, &waE2E.Message{ + Conversation: new(text), + }, whatsmeow.SendRequestExtra{ + ID: record.MessageID, + }) + + record.State = whatsappSent + if err != nil { + record.State = whatsappFailed + record.Answer = "The send failed: " + whatsappRedact(link.own, err.Error()) + "." + } else { + name := groups.name(ctx, link, jid.String()) + if jid.Server != types.GroupServer { + name = whatsappContactName(ctx, link.device, jid, "") + } + + record.Answer = "Sent to " + name + " at " + response.Timestamp.In(clock().Location()).Format("15:04") + "." + } + + err = state.PutSend(record) + if err != nil { + return "", err + } + + return record.Answer, nil +} + +// whatsappSendCheck returns the JID of chat, and an error when id, chat, or +// text cannot go to a send. The chat is a private chat or a group, and not the +// chat of the user with itself. +func whatsappSendCheck(id, chat, text string, own []string) (types.JID, error) { + jid, err := types.ParseJID(chat) + switch { + case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes: + return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes) + case strings.TrimSpace(text) == "" || len(text) > whatsappSendBytes: + return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes) + case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat: + return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat) + case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer: + return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat) + case slices.Contains(own, jid.User): + return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat") + } + + return jid, nil +} diff --git a/whatsapp_send_test.go b/whatsapp_send_test.go new file mode 100644 index 0000000..51651b2 --- /dev/null +++ b/whatsapp_send_test.go @@ -0,0 +1,206 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "net/http" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" +) + +func TestWhatsAppSendSendsOnceForEachID(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + whatsappTestContacts(t, whatsapp) + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + + answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") + require.NoError(t, err) + require.Equal(t, "Sent to Ana Souza at 12:05.", answer) + + answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") + require.NoError(t, err) + require.Equal(t, "Sent to Ana Souza at 12:05. This is the answer of the first call with this id: nothing was sent again.", answer) + + _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.") + require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message") + + require.Equal(t, []WhatsAppFakeSend{ + { + to: whatsappTestAna.String(), + text: "The keys are under the mat.", + id: "MSG1", + }, + }, fake.Sent()) + + again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName), whatsappBatchQuiet, whatsappBatchWindow) + require.NoError(t, err) + + record, found := again.Send("keys") + require.True(t, found) + require.Equal(t, whatsappSent, record.State) + require.Equal(t, "MSG1", record.MessageID) +} + +func TestWhatsAppSendToAGroup(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + fake.groups = []*types.GroupInfo{ + { + JID: whatsappTestFamily, + GroupName: types.GroupName{ + Name: "Family", + }, + }, + } + + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + + answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.") + require.NoError(t, err) + require.Equal(t, "Sent to Family at 12:05.", answer) +} + +func TestWhatsAppSendThatFails(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + fake.sendErr = errors.New("server error for " + whatsappTestNumber) + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + + for range 2 { + answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + require.NoError(t, err) + require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer) + } + + require.Len(t, fake.Sent(), 1) +} + +func TestWhatsAppSendCutByAStop(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + + digest := sha256.Sum256([]byte("hi")) + require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{ + ID: "keys", + Chat: whatsappTestAna.String(), + Digest: hex.EncodeToString(digest[:]), + State: whatsappSending, + })) + + answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + require.NoError(t, err) + require.Contains(t, answer, "its result is unknown") + require.Empty(t, fake.Sent()) +} + +func TestWhatsAppSendRefuses(t *testing.T) { + testCases := []struct { + name string + id string + chat string + text string + err string + }{ + { + name: "no id", + id: " ", + chat: whatsappTestAna.String(), + text: "hi", + err: "the id must have 1 to 100 bytes", + }, + { + name: "no text", + id: "a", + chat: whatsappTestAna.String(), + text: "", + err: "the text must have 1 to 65536 bytes", + }, + { + name: "a name for a chat", + id: "a", + chat: "Ana Souza", + text: "hi", + err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`, + }, + { + name: "a status broadcast", + id: "a", + chat: types.StatusBroadcastJID.String(), + text: "hi", + err: `"status@broadcast" is not a private chat or a group`, + }, + { + name: "the chat of the user", + id: "a", + chat: whatsappTestOwn.String(), + text: "hi", + err: "this is the chat of the user with itself: send to another chat", + }, + } + + whatsapp, fake := whatsappStartTestLinked(t) + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text) + require.EqualError(t, err, tc.err) + }) + } + + require.Empty(t, fake.Sent()) +} + +func TestWhatsAppSendWhileDisconnected(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) + whatsappTestConnect(t, fake) + require.True(t, fake.Emit(&events.Disconnected{})) + + _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later") + + require.True(t, fake.Emit(&events.Connected{})) + answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + require.NoError(t, err) + require.Equal(t, "Sent to +15550002222 at 12:05.", answer) +} + +func TestWhatsAppSendWithoutALink(t *testing.T) { + whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock()) + + answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + require.NoError(t, err) + require.Equal(t, whatsappNotLinked, answer) +} + +func TestAgentSendsAWhatsAppMessage(t *testing.T) { + whatsapp, fake := whatsappStartTestLinked(t) + whatsappTestContacts(t, whatsapp) + server := newModelFake(t) + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { + if call.number == 0 { + modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_send", `{"id":"keys","chat":"15550002222@s.whatsapp.net","text":"The keys are under the mat."}`)}, 100, 0, 5)) + return + } + + modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5)) + } + + socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp) + client := terminalStartTestClient(t, socket) + whatsappTestConnect(t, fake) + + client.Type("Tell Ana that the keys are under the mat.") + client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]") + require.Len(t, fake.Sent(), 1) +} diff --git a/whatsapp_test.go b/whatsapp_test.go index 2bdb4cc..1ca0a52 100644 --- a/whatsapp_test.go +++ b/whatsapp_test.go @@ -1,10 +1,7 @@ package main import ( - "crypto/sha256" - "encoding/hex" "errors" - "net/http" "os" "path/filepath" "strings" @@ -464,194 +461,3 @@ func TestSubagentHasNoWhatsAppTools(t *testing.T) { require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`)) require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`)) } - -func TestWhatsAppSendSendsOnceForEachID(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - whatsappTestContacts(t, whatsapp) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") - require.NoError(t, err) - require.Equal(t, "Sent to Ana Souza at 12:05.", answer) - - answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") - require.NoError(t, err) - require.Equal(t, "Sent to Ana Souza at 12:05. This is the answer of the first call with this id: nothing was sent again.", answer) - - _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.") - require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message") - - require.Equal(t, []WhatsAppFakeSend{ - { - to: whatsappTestAna.String(), - text: "The keys are under the mat.", - id: "MSG1", - }, - }, fake.Sent()) - - again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName), whatsappBatchQuiet, whatsappBatchWindow) - require.NoError(t, err) - - record, found := again.Send("keys") - require.True(t, found) - require.Equal(t, whatsappSent, record.State) - require.Equal(t, "MSG1", record.MessageID) -} - -func TestWhatsAppSendToAGroup(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - fake.groups = []*types.GroupInfo{ - { - JID: whatsappTestFamily, - GroupName: types.GroupName{ - Name: "Family", - }, - }, - } - - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.") - require.NoError(t, err) - require.Equal(t, "Sent to Family at 12:05.", answer) -} - -func TestWhatsAppSendThatFails(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - fake.sendErr = errors.New("server error for " + whatsappTestNumber) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - for range 2 { - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.NoError(t, err) - require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer) - } - - require.Len(t, fake.Sent(), 1) -} - -func TestWhatsAppSendCutByAStop(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - digest := sha256.Sum256([]byte("hi")) - require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{ - ID: "keys", - Chat: whatsappTestAna.String(), - Digest: hex.EncodeToString(digest[:]), - State: whatsappSending, - })) - - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.NoError(t, err) - require.Contains(t, answer, "its result is unknown") - require.Empty(t, fake.Sent()) -} - -func TestWhatsAppSendRefuses(t *testing.T) { - testCases := []struct { - name string - id string - chat string - text string - err string - }{ - { - name: "no id", - id: " ", - chat: whatsappTestAna.String(), - text: "hi", - err: "the id must have 1 to 100 bytes", - }, - { - name: "no text", - id: "a", - chat: whatsappTestAna.String(), - text: "", - err: "the text must have 1 to 65536 bytes", - }, - { - name: "a name for a chat", - id: "a", - chat: "Ana Souza", - text: "hi", - err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`, - }, - { - name: "a status broadcast", - id: "a", - chat: types.StatusBroadcastJID.String(), - text: "hi", - err: `"status@broadcast" is not a private chat or a group`, - }, - { - name: "the chat of the user", - id: "a", - chat: whatsappTestOwn.String(), - text: "hi", - err: "this is the chat of the user with itself: send to another chat", - }, - } - - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text) - require.EqualError(t, err, tc.err) - }) - } - - require.Empty(t, fake.Sent()) -} - -func TestWhatsAppSendWhileDisconnected(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - require.True(t, fake.Emit(&events.Disconnected{})) - - _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later") - - require.True(t, fake.Emit(&events.Connected{})) - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.NoError(t, err) - require.Equal(t, "Sent to +15550002222 at 12:05.", answer) -} - -func TestWhatsAppSendWithoutALink(t *testing.T) { - whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock()) - - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.NoError(t, err) - require.Equal(t, whatsappNotLinked, answer) -} - -func TestAgentSendsAWhatsAppMessage(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - whatsappTestContacts(t, whatsapp) - server := newModelFake(t) - server.reply = func(w http.ResponseWriter, call ModelFakeCall) { - if call.number == 0 { - modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_send", `{"id":"keys","chat":"15550002222@s.whatsapp.net","text":"The keys are under the mat."}`)}, 100, 0, 5)) - return - } - - modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5)) - } - - socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp) - client := terminalStartTestClient(t, socket) - whatsappTestConnect(t, fake) - - client.Type("Tell Ana that the keys are under the mat.") - client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]") - require.Len(t, fake.Sent(), 1) -}