media
one uniform way to point at a file — local path, url, in-memory buffer, or an existing Telegram file_id — and the Api layer picks the right wire form.
the four sources
a MediaSource is a small discriminated, branded object. you never build one by hand —
use the media.* helpers:
import { media } from "@yaebal/core";
media.path("./photo.jpg"); // local file → uploaded
media.url("https://example.com/p.png"); // remote url → passed as a string
media.buffer(new Uint8Array([…]), "p.png"); // in-memory bytes → uploaded
media.fileId("AgACAgIAAx…"); // already on Telegram → reused| helper | kind | on the wire |
|---|---|---|
media.path | path | read from disk, uploaded as multipart |
media.buffer | buffer | bytes uploaded as multipart (with optional filename) |
media.url | url | sent as a plain url string |
media.fileId | fileId | sent as the file_id string |
isMediaSource
every helper brands its result with a unique symbol. isMediaSource checks that brand,
so a plain object that merely looks like one is rejected — the Api layer uses this to decide what
to encode.
import { isMediaSource, media } from "@yaebal/core";
isMediaSource(media.fileId("AgAC")); // true — branded with a symbol
isMediaSource({ kind: "fileId", fileId: "x" }); // false — not brandedsending media
ctx.sendPhoto and ctx.sendDocument accept a MediaSource or a
raw file_id/url string directly. extra params (caption, reply markup, …) go in the
second argument.
bot.command("photo", (ctx) =>
ctx.sendPhoto(media.url("https://picsum.photos/400"), {
caption: "a random picture",
}),
);
bot.command("doc", (ctx) =>
ctx.sendDocument(media.path("./report.pdf")),
);
// a raw file_id or url string works too — no wrapper required:
ctx.sendPhoto("AgACAgIAAx…");how upload works
encodeRequest decides the encoding per request. if no path or buffer is present, the body is JSON and any url/fileId media
is inlined to its string:
// no path/buffer present → JSON, with url/fileId inlined to strings
await encodeRequest({ chat_id: 1, photo: media.fileId("AgAC") });
// { body: '{"chat_id":1,"photo":"AgAC"}', contentType: "application/json" }
await encodeRequest({ photo: media.url("https://e/p.png") });
// { body: '{"photo":"https://e/p.png"}', contentType: "application/json" }the moment a path or buffer appears anywhere in the top-level params, the
whole request switches to multipart/form-data. each upload is written to a generated
field and the param points at it with attach://:
// a path or buffer present → the whole request becomes multipart.
// each upload is attached under a generated field and referenced via attach://
await encodeRequest({
chat_id: 7,
photo: media.buffer(new Uint8Array([1, 2, 3]), "pic.png"),
reply_markup: { inline_keyboard: [] },
});
// FormData:
// photo = "attach://_file0"
// _file0 = <Blob "pic.png">
// chat_id = "7" (non-string → stringified)
// reply_markup = '{"inline_keyboard":[]}' (object → JSON)MediaSource (e.g. inside sendMediaGroup's media[]) is
rejected loudly rather than silently serialized to garbage — pass media as a top-level param.