Skip to content

Commit c688705

Browse files
committed
feat(telegram): add SendDocument + wire document media type in sendMedia
Adds Bot.SendDocument(chatID, path, caption, opts) for sending documents via the Telegram API, matching the SendPhoto/SendVoice pattern. Wires 'document' media type into handler.sendMedia so MEDIA:document:path responses are delivered correctly. Updates testServer to handle /sendDocument and adds dedicated tests.
1 parent c01463e commit c688705

4 files changed

Lines changed: 149 additions & 3 deletions

File tree

internal/telegram/bot.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,26 @@ func (b *Bot) SendVoice(chatID int64, path string, caption string, opts *SendOpt
276276
return &msg, nil
277277
}
278278

279+
// SendDocument sends a document from a file path to the specified chat.
280+
// opts may contain ReplyToMessageID to reply to a specific message.
281+
func (b *Bot) SendDocument(chatID int64, path string, caption string, opts *SendOpts) (*Message, error) {
282+
params := map[string]any{
283+
"chat_id": chatID,
284+
}
285+
if caption != "" {
286+
params["caption"] = caption
287+
}
288+
if opts != nil && opts.ReplyToMessageID != 0 {
289+
params["reply_to_message_id"] = opts.ReplyToMessageID
290+
}
291+
292+
var msg Message
293+
if err := b.doUpload("sendDocument", "document", path, params, &msg); err != nil {
294+
return nil, err
295+
}
296+
return &msg, nil
297+
}
298+
279299
// GetUpdates retrieves incoming updates using long polling.
280300
func (b *Bot) GetUpdates(offset int, timeout int) ([]Update, error) {
281301
params := map[string]any{

internal/telegram/bot_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,91 @@ func TestSendPhoto_FileNotFound(t *testing.T) {
626626
}
627627
}
628628

629+
// ---------------------------------------------------------------------------
630+
// SendDocument — multipart upload
631+
// ---------------------------------------------------------------------------
632+
633+
func TestSendDocument_Success(t *testing.T) {
634+
tmpDir := t.TempDir()
635+
docPath := filepath.Join(tmpDir, "test_doc.pdf")
636+
if err := os.WriteFile(docPath, []byte("fake-pdf-data"), 0o644); err != nil {
637+
t.Fatalf("write temp file: %v", err)
638+
}
639+
640+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
641+
path := strings.TrimSuffix(r.URL.Path, "/")
642+
if path != "/sendDocument" {
643+
t.Errorf("unexpected path: %s", path)
644+
}
645+
requireMultipartBody(t, r, "document", "test_doc.pdf", map[string]any{
646+
"chat_id": float64(123),
647+
"caption": "here is the doc",
648+
})
649+
okResponse(w, map[string]any{
650+
"message_id": 20,
651+
"text": "",
652+
"chat": map[string]any{"id": 123, "type": "private"},
653+
"date": 2000,
654+
})
655+
}))
656+
defer ts.Close()
657+
658+
bot := NewBot("x")
659+
bot.BaseURL = ts.URL
660+
661+
msg, err := bot.SendDocument(123, docPath, "here is the doc", nil)
662+
if err != nil {
663+
t.Fatalf("SendDocument: %v", err)
664+
}
665+
if msg == nil {
666+
t.Fatal("SendDocument returned nil message")
667+
}
668+
if msg.ID != 20 {
669+
t.Errorf("msg.ID = %d, want 20", msg.ID)
670+
}
671+
}
672+
673+
func TestSendDocument_NoCaption(t *testing.T) {
674+
tmpDir := t.TempDir()
675+
docPath := filepath.Join(tmpDir, "report.csv")
676+
if err := os.WriteFile(docPath, []byte("a,b,c\n1,2,3"), 0o644); err != nil {
677+
t.Fatalf("write temp file: %v", err)
678+
}
679+
680+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
681+
requireMultipartBody(t, r, "document", "report.csv", map[string]any{
682+
"chat_id": float64(456),
683+
})
684+
okResponse(w, map[string]any{
685+
"message_id": 21,
686+
"chat": map[string]any{"id": 456},
687+
})
688+
}))
689+
defer ts.Close()
690+
691+
bot := NewBot("x")
692+
bot.BaseURL = ts.URL
693+
694+
msg, err := bot.SendDocument(456, docPath, "", nil)
695+
if err != nil {
696+
t.Fatalf("SendDocument(no caption): %v", err)
697+
}
698+
if msg.ID != 21 {
699+
t.Errorf("msg.ID = %d, want 21", msg.ID)
700+
}
701+
}
702+
703+
func TestSendDocument_FileNotFound(t *testing.T) {
704+
bot := NewBot("x")
705+
_, err := bot.SendDocument(1, "/nonexistent/path.pdf", "", nil)
706+
if err == nil {
707+
t.Fatal("expected error for missing file, got nil")
708+
}
709+
if !strings.Contains(err.Error(), "open") {
710+
t.Errorf("error = %q, want substring %q", err, "open")
711+
}
712+
}
713+
629714
// ---------------------------------------------------------------------------
630715
// AnswerCallbackQuery
631716
// ---------------------------------------------------------------------------

internal/telegram/handler.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,12 @@ func (h *Handler) sendMedia(chatID int64, text string, replyToMessageID int) {
438438
opts = &SendOpts{ReplyToMessageID: replyToMessageID}
439439
}
440440
_, err = h.Bot.SendVoice(chatID, filePath, "", opts)
441+
case "document":
442+
var opts *SendOpts
443+
if replyToMessageID != 0 {
444+
opts = &SendOpts{ReplyToMessageID: replyToMessageID}
445+
}
446+
_, err = h.Bot.SendDocument(chatID, filePath, "", opts)
441447
default:
442448
h.log.Error("unknown media type", "chat_id", chatID, "media_type", mediaType)
443449
if h.OnError != nil {

internal/telegram/handler_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ func testServer(t *testing.T, recorder *requestRecorder) *httptest.Server {
6363
"ok": true,
6464
"result": map[string]any{"message_id": 3},
6565
}
66+
case strings.HasSuffix(r.URL.Path, "/sendDocument"):
67+
resp = map[string]any{
68+
"ok": true,
69+
"result": map[string]any{"message_id": 4},
70+
}
6671
default:
6772
resp = map[string]any{"ok": true}
6873
}
@@ -1084,19 +1089,49 @@ func TestSendResponse_MediaUnknownType(t *testing.T) {
10841089
errCalled = true
10851090
}
10861091

1087-
h.SendResponse(123, "MEDIA:document:"+tmpPath, 0)
1092+
h.SendResponse(123, "MEDIA:video:"+tmpPath, 0)
10881093

10891094
reqs := rec.all()
10901095
for _, req := range reqs {
1091-
if strings.HasSuffix(req.Path, "/sendPhoto") || strings.HasSuffix(req.Path, "/sendVoice") {
1092-
t.Errorf("unexpected send request for unknown media type")
1096+
if strings.HasSuffix(req.Path, "/sendPhoto") || strings.HasSuffix(req.Path, "/sendVoice") || strings.HasSuffix(req.Path, "/sendDocument") {
1097+
t.Errorf("unexpected send request for unknown media type: %s", req.Path)
10931098
}
10941099
}
10951100
if !errCalled {
10961101
t.Error("OnError was not called for unknown media type")
10971102
}
10981103
}
10991104

1105+
func TestSendResponse_MediaDocument(t *testing.T) {
1106+
tmpFile, err := os.CreateTemp("", "test-doc-*.pdf")
1107+
if err != nil {
1108+
t.Fatalf("failed to create temp file: %v", err)
1109+
}
1110+
tmpPath := tmpFile.Name()
1111+
tmpFile.Close()
1112+
defer os.Remove(tmpPath)
1113+
1114+
rec := new(requestRecorder)
1115+
ts := testServer(t, rec)
1116+
defer ts.Close()
1117+
bot := testBot(t, ts)
1118+
h := NewHandler(bot)
1119+
1120+
h.SendResponse(456, "MEDIA:document:"+tmpPath, 0)
1121+
1122+
reqs := rec.all()
1123+
found := false
1124+
for _, req := range reqs {
1125+
if strings.HasSuffix(req.Path, "/sendDocument") {
1126+
found = true
1127+
break
1128+
}
1129+
}
1130+
if !found {
1131+
t.Error("no sendDocument request was made for MEDIA:document")
1132+
}
1133+
}
1134+
11001135
func TestSendResponse_MediaMalformed(t *testing.T) {
11011136
rec := new(requestRecorder)
11021137
ts := testServer(t, rec)

0 commit comments

Comments
 (0)