plugins & .extend
a plugin is a self-contained piece of behavior that attaches to tg under its own namespace. @puregram/session, @puregram/scenes, @puregram/flow, @puregram/markup — every official satellite is a plugin. the api is tg.extend(plugin), it's chainable, and each call narrows the type of tg so you never need declare module 'puregram' augmentations
import { Telegram } from 'puregram'
import { session } from '@puregram/session'
const tg = Telegram.fromToken(process.env.TOKEN!)
.extend(session())
// session is now installed and typed
tg.onMessage(async (message) => {
message.session.counter = (message.session.counter ?? 0) + 1
await message.send(`you sent ${message.session.counter} messages`)
})how .extend works
.extend(plugin) queues the plugin synchronously and returns a narrowed Telegram<Ext> where Ext picks up the plugin's namespace. installs are awaited on tg.start() (called implicitly by startPolling() / startWebhook()) in dependency-resolved order. async installs are fine — install may return a Promise
the returned value of install(tg) gets keyed under plugin.name and assigned onto tg. every update handler sees the same narrowed type automatically — message.session is typed inside withSession.onMessage(...) without any extra ceremony
plugins compose:
import { session } from '@puregram/session'
import { scenes } from '@puregram/scenes'
import { flow } from '@puregram/flow'
const tg = Telegram.fromToken(TOKEN)
.extend(session())
.extend(scenes())
.extend(flow())
// tg.session, tg.scenes, tg.flow — all typed.extend must be called before start
once polling or a webhook starts, the plugin list is frozen. calling .extend(plugin) after startPolling() throws. queue all plugins before the first start()
writing a plugin
createPlugin is a thin factory that gives you the typed spec:
import { createPlugin } from 'puregram'
const greeter = createPlugin({
name: 'greeter',
install: tg => ({
hello: async (chatId: number) => tg.send(chatId, 'hi!')
})
})
const tg = Telegram.fromToken(process.env.TOKEN!).extend(greeter)
await tg.greeter.hello(100) // typed!the install function receives the Telegram instance and returns anything — an object, a class instance, a primitive. that return value becomes tg.<plugin.name>
dependsOn
declare hard dependencies by name so the installer resolves install order topologically:
const analytics = createPlugin({
name: 'analytics',
dependsOn: ['session'],
install: tg => ({
track: (event: string) => {
// tg.session is guaranteed to be installed here
}
})
})the installer throws:
PluginConflict— two plugins share the samenamePluginMissingDep— a declared dep isn't registeredPluginCycle— the dependency graph has a cycle
for soft deps (adapt-if-present), use tg.has('session') — it doesn't widen the type, just checks at runtime
what plugins can and can't do
| supported | |
|---|---|
extend tg with new methods | yes — returned from install |
| extend update classes with per-update helpers | yes — via hooks (e.g. tg.useHook('onUpdate', ...)) |
declare module 'puregram' augmentation | no — the .extend return type handles narrowing |
| install order control | via dependsOn |
| async setup / teardown | install may be async; use tg.useHook('onShutdown', ...) for teardown |
lifecycle hooks inside a plugin
plugins can tap into the bot's lifecycle via useHook:
const myPlugin = createPlugin({
name: 'my-plugin',
install: (tg) => {
tg.useHook('onInit', async () => {
// called once, after all plugins install and before dispatch starts
})
tg.useHook('onShutdown', async () => {
// called on tg.shutdown()
})
return { /* plugin surface */ }
}
})see also
- custom updates — emit non-telegram events through the same dispatch pipeline
- the Telegram client — construction, lifecycle, and options