nuxt-pigeon
Seven services want to be talked to in seven different ways. You write one.
await telegram.send('Deploy failed on main')
No client to construct, no token to pass around, no import to write. Channels are auto
imported on the server, they read their credentials from .env, and that is the whole
setup.
What each channel can do
| send | media | edit | delete | receive | |
|---|---|---|---|---|---|
| Telegram | ✅ | ✅ | ✅ | ✅ | ✅ webhook |
| Discord | ✅ | ✅ | ✅ | ✅ | — |
| Slack | ✅ | ✅ ¹ | ✅ ¹ | ✅ ¹ | ✅ Events API |
| ntfy | ✅ | ✅ | ✅ ² | ✅ ² | — |
| Mastodon | ✅ | ✅ | ✅ | ✅ | ✅ polling |
| Bluesky | ✅ | ✅ | ❌ ³ | ✅ | ✅ polling |
| Any webhook | ✅ | — | — ⁴ | — ⁴ | ✅ route |
¹ needs a bot token. An incoming webhook answers ok and no message id, so there is
nothing to point at afterwards.
² needs an ntfy server of 2.16.0 or newer.
³ Bluesky has no post editing. putRecord answers 200 and the appview ignores the
change, so an edit would look like it worked and do nothing. It is not offered rather
than offered and broken.
⁴ you decide what the receiver is, so send carries method and url and a PATCH or
DELETE is one call.
Where a channel cannot do something, the method is missing from its type, so you find out while typing rather than in production. Calling it anyway gets a sentence naming the reason.
What it takes care of
Limits, counted properly
ntfy counts bytes. Bluesky counts graphemes and bytes. Mastodon charges a flat 23 for any link and asks your instance for its real limit. All checked before anything is sent, so you get a number instead of an API error.
Images, four different ways
Telegram fetches a url itself. Mastodon refuses urls. Slack turns the file into the
message. You pass { url } or { data } and never learn which.
Retries that think
A 500 is retried, a 404 is not. Retry-After wins over our own backoff. A request
that never answered is not repeated, because a duplicate notification is worse
than a missing one.
Errors you can log
For Telegram the token is in the url, for Discord the url is the credential. No error here carries either. The target is named, the address never is.
Receiving, in one line
What arrives on the server shows up in your frontend on its own:
<script setup lang="ts">
const { messages } = usePigeon()
</script>
<template>
<p v-for="message in messages" :key="message.at">{{ message.text }}</p>
</template>
Underneath, that is a webhook for Telegram, an Events API route for Slack and a poller for Bluesky. You never have to know which.