Sending images
Some services fetch a url for you. Some refuse urls and want the file itself. One turns the file into the message. You write the same thing for all of them.
Copy this
export default defineEventHandler(async () => {
await telegram.send('The failing step', {
media: [{ url: 'https://example.com/shot.png', alt: 'The failing step' }],
})
return { ok: true }
})
That runs. Telegram hands the url on and fetches it itself. Mastodon and Bluesky cannot do that, so for them the module downloads it and uploads it for you. You do not choose, and you do not have to know which.
Two shapes, and only two
{ url: 'https://…/shot.png', alt: 'A screenshot', filename: 'shot.png' }
{ data: bytes, alt: 'A screenshot', filename: 'shot.png', type: 'image/png' }
data takes a Blob, a Uint8Array or an ArrayBuffer. Everything else is optional.
node:fs into the module, and
that does not exist on Cloudflare Workers. Reading the file is two lines you write, and
then it is bytes like everything else.const data = await readFile('shot.png')
await telegram.send('Look', { media: [{ data }] })
Why both shapes exist everywhere
A url only version would force you to publish a private screenshot on the internet so that we can download it again. That is absurd, and in some places it is a data protection problem.
A bytes only version would drop Discord spoilers, which work exclusively on real attachments.
So both work everywhere, and each channel picks the cheaper route:
| Channel | A url | Bytes |
|---|---|---|
| Telegram | handed over untouched, Telegram fetches it | multipart |
| ntfy | handed over as attach, ntfy fetches it | PUT, the body is the file |
| Discord | we fetch it, an attachment takes no url | multipart |
| Mastodon | we fetch it, /api/v2/media takes a file only | native |
| Bluesky | we fetch it, uploadBlob wants raw bytes | native |
| Slack | we fetch it, three step upload | native |
Limits, and they are not close to each other
| Channel | Per file | How many |
|---|---|---|
| Discord | 10 MB | 10 |
| Bluesky | about 1 MB | 4 |
| Mastodon | instance decides, often 10 MB | 4, instance decides |
| Telegram | Telegram's own, generous | one album |
What will cost you an hour
- Telegram's caption limit is 1024, not 4096. Adding an image lowers the limit of the text you already wrote.
- A Discord spoiler only works on an attachment, through a
SPOILER_filename prefix. On anembed.image.urlit is ignored, no error, just not hidden. - Mastodon answers
202while it is still processing a large upload, and posting before it finishes is refused. The module waits for you. - Slack needs the bot in the channel to upload.
chat:write.publiccovers messages but not files, so/inviteit once. - A Slack upload has no message timestamp. The file becomes the message and Slack
answers without a
ts, so it can be deleted but never edited.
Read more
- Changing and removing - including what happens to attachments on an edit
- Bluesky - link cards, and why one post holds one embed
- Discord - embeds and spoilers
Sending text
Send text to Telegram, Discord, Slack, ntfy, Mastodon and Bluesky with one verb. Formatting differs per service, and every limit is checked before sending.
Changing and removing
Edit and delete messages you already sent on Telegram, Discord, Slack, ntfy and Mastodon. The result of send is the handle, and the types say who cannot.