powerful and epic overall,
puregram
allows you to
easily interact
with
telegram bot api
via
node.js
ππ
introduction
first, what are telegram bots? telegram has their own bot accounts. bots are special telegram accounts that can be only accessed via code and were designed to handle messages, inline queries and callback queries automatically. users can interact with bots by sending them messages, commands and inline requests.
example
const { Telegram } = require('puregram')
const telegram = Telegram.fromToken(process.env.TOKEN)
telegram.updates.on('message', context => context.reply('hey!'))
telegram.updates.startPolling()
note
you can find more examples here
table of contents
why puregram
?
- written by starkΓ³w β
- powered by j++team β
- very cool package name
- package itself is cool (at least i think so)
- works (i guess)
- i understand only about 30% of my code
- because why not?
getting started
getting token
if you want to develop a bot, firstly you need to create it via @botfather and get token from it via /newbot
command.
token looks like this: 123456:abc-def1234ghikl-zyx57w2v1u123ew11
installation
requirements
node.js version must be greater or equal than LTS (16.15.0
atm)
$ yarn add puregram
$ npm i -S puregram
usage
initializing Telegram
instance
let's start with creating a Telegram
instance:
const { Telegram } = require('puregram')
const bot = new Telegram({
token: '123456:abc-def1234ghikl-zyx57w2v1u123ew11'
})
You can also initialize it via Telegram.fromToken
:
const bot = Telegram.fromToken('123456:abc-def1234ghikl-zyx57w2v1u123ew11')
now, we want to get updates from the bot. how can we do it?
getting updates
there are only two ways of getting updates right now:
- polling via
getUpdates
method... or just using puregram
's built-in polling logic:
telegram.updates.startPolling()
- setting up a Webhook via
setWebhook
method:
const { createServer } = require('http')
telegram.api.setWebhook({
url: 'https://www.example.com/'
})
const server = createServer(telegram.updates.getWebhookMiddleware())
server.listen(8443, () => console.log('started'))
remember that there are only four accepted ports for now: 443
, 80
, 88
and 8443
. they are listed here under the notes section.
note
more webhook examples are available here
handling updates
now with this setup we can catch updates like this:
telegram.updates.on('message', context => context.reply('yoo!'))
supported events are listed here
the mergeMediaEvents
if you've had to handle multiple attachments at once you'd know that in telegram every single attachment is a separate message. that makes it pretty hard for us to handle multiple attachs at once. here it comes - the mergeMediaEvents
option in Telegram
's constructor
const telegram = new Telegram({
token: process.env.TOKEN,
mergeMediaEvents: true
})
what's changed? if you'd set up a handler like this:
telegram.updates.on('message', (context) => {
console.log(context)
})
and then sent an album, you'd see that there will be some mediaGroup
field in the MessageContext
. that mediaGroup
(instance of a MediaGroup
class) contains some getters:
getter | type | description |
---|
id | string | media group's id |
contexts | MessageContext[] | list of received (and processed) contexts which contain an attachment |
attachments | Attachment[] | list of attachments mapped through contexts (described earlier) |
telegram.updates.on('message', (context) => {
if (context.isMediaGroup()) {
return context.reply(`this album contains ${context.mediaGroup.attachments.length} attachments!`)
}
})
manual updates handling
if you want to handle updates by yourself, you can use Updates.handleUpdate
method, which takes one argument and this argument is raw Telegram update:
const update = await getUpdate(...)
let context
try {
context = telegram.updates.handleUpdate(update)
} catch (error) {
console.log('update is not supported', update)
}
what is UpdatesFilter
?
as mentioned in getUpdates
documentation,
Specify an empty list to receive all update types except chat_member
(default).
If not specified, the previous setting will be used.
as you can see, you have to specify chat_member
in order to receive chat_member
updates...
but you also will have to specify every single update type that you're going to handle like this:
{
allowedUpdates: ['chat_member', 'message', 'callback_query', 'channel_post', 'edited_message', 'edited_channel_post', ...]
}
not very convenient, is it? that's why we've createed UpdatesFilter
: a class containing a few static methods
that will allow you to specify all update types or even exclude some!
const { Telegram, UpdatesFilter } = require('puregram')
const telegram = Telegram.fromToken(process.env.TOKEN, {
allowedUpdates: UpdatesFilter.all()
})
const { Telegram, UpdatesFilter } = require('puregram')
const telegram = Telegram.fromToken(process.env.TOKEN)
telegram.updates.startPolling({
allowedUpdates: UpdatesFilter.except('callback_query')
})
telegram.updates.on('callback_query', (context) => {
return cry()
})
calling api methods
there are three ways of calling telegram bot api methods:
- using the
telegram.api.call(method, params?)
(useful when new bot api update is released and the package is not updated yet):
const me = await telegram.api.call('getMe')
- using
telegram.api.method(params?)
:
const me = await telegram.api.getMe()
- using context methods:
telegram.updates.on('message', context => context.send('13Β² = 169! well, i mean "169", not "169!"... fuck.'))
suppressing errors
sometimes you dont want to deal with the errors sent by the api,
sometimes you just dont want to create an empty try/catch
statement for that.
this is where suppress
parameter in the api call params comes in!
you can pass suppress: true
to any api method and in case the error happens
puregram
will not throw an error, but will return json object with ok: false
and error_code
and description
properties.
telegram.api
usage
const result = await telegram.api.sendChatAction({
chat_id: getRandomInt(1, 999_999_999),
action: 'typing',
suppress: true
})
if (Telegram.isErrorResponse(result)) {
}
context
methods
const result = await context.sendChatAction('typing', { suppress: true })
if (Telegram.isErrorResponse(result)) {
return
}
sending media
puregram
allows you to send your local media by using MediaSource
class.
you can put URLs, Buffer
s, streams and paths in it.
const { createReadStream } = require('fs')
const path = './puppy.jpg'
const stream = createReadStream(path)
const buffer = getBuffer(path)
const url = 'https://puppies.com/random-puppy'
const fileId = 'this-is-probably-a-real-file-id-for-sure'
telegram.updates.on('message', (context) => {
await Promise.all([
context.sendPhoto(MediaSource.path(path), { caption: 'puppy via path!' }),
context.sendDocument(MediaSource.stream(stream, { filename: 'puppy.jpg' }), { caption: 'more puppies via stream!' }),
context.sendPhoto(MediaSource.buffer(buffer), { caption: 'one more puppy via buffer!' }),
context.sendPhoto(MediaSource.url(url), { caption: 'some random puppy sent using an url!!!' }),
context.sendVideo(MediaSource.fileId(fileId), { caption: 'a video sent via file ID' })
])
})
this works for every method that can send media.
downloading media
telegram bot api allows you to download any media you want by simply calling getFile({ file_id })
,
extracting file_path
from it and constructing a certain URL that you can then fetch and receive
the result, the final media.
that's a little too much work just for one file, isn't it? because of this, puregram
has a mixin that allows just that.
telegram.updates.on('message', async (context) => {
if (!context.hasAttachmentType('photo')) {
return
}
const buffer = await context.download()
return context.sendDocument(MediaSource.buffer(buffer, { filename: 'photo.png' }))
})
of course, you can download an attachment not only via Buffer
s, but via path
and a stream
. we use MediaSourceTo
(not MediaSource
) for that.
MediaSourceTo.path
, via path
telegram.updates.on('message', async (context) => {
if (!context.hasAttachmentType('photo')) {
return
}
const PATH = resolve(__dirname, 'photo.png')
await context.download(MediaSourceTo.path(PATH))
return context.sendDocument(MediaSource.path(PATH, { filename: 'photo.png' }))
})
MediaSourceTo.stream
, via stream
telegram.updates.on('message', async (context) => {
if (!context.hasAttachmentType('photo')) {
return
}
const stream = new PassThrough()
await context.download(MediaSourceTo.stream(stream))
return context.sendDocument(MediaSource.stream(stream, { filename: 'photo.png' }))
})
MediaSourceTo.buffer
, via buffer
telegram.updates.on('message', async (context) => {
if (!context.hasAttachmentType('photo')) {
return
}
const buffer = await context.download(MediaSourceTo.buffer())
return context.sendDocument(MediaSource.buffer(buffer, { filename: 'photo.png' }))
})
more internal api
under the hood context.download(...)
uses telegram.downloadFile(...)
method. it can be called with either file_id
or an attachment
file_id
& Buffer
const fileId = getFileIdSomehow()
const result = await telegram.downloadFile(fileId, MediaSourceTo.buffer())
Attachment
& path
const attachment = context.attachment
const PATH = resolve(__dirname, 'test.png')
const result = await telegram.downloadFile(attachment, MediaSourceTo.path(PATH))
sending input media
some of the methods (like editMessageMedia
or sendMediaGroup
) require such objects
like TelegramInputMediaPhoto
, TelegramInputMediaVideo
and so on
puregram
provides InputMedia
class which allows you to easily map your MediaSource
value to a piece of input media!
const { InputMedia, MediaSource } = require('puregram')
telegram.api.editMessageMedia({
chat_id: 398859857,
message_id: 12345,
media: InputMedia.document(MediaSource.path('./README.md'), {
caption: 'Epic shit'
})
})
context.sendMediaGroup([
InputMedia.photo(MediaSource.path('./image.png')),
InputMedia.video(MediaSource.url('https://example.com/path/to/video.mp4'), {
caption: 'here goes caption'
})
])
you can even use InputMedia
on context.sendMedia
!
context.sendMedia(
InputMedia.photo(MediaSource.path('./image.png'), {
caption: 'EPIC!!ββββββοΈβοΈ'
})
)
using markdown
if you want to use markdown or html, there are two ways of doing that:
- using built-in
HTML
, Markdown
and MarkdownV2
classes:
const message = HTML.bold('very bold, such html')
- writing tags manually as it is told here:
const message = '*very bold, such markdown*'
anyways, after writing the text you need to add parse_mode
field. there are also two ways actually, there are three ways of of doing that!
- writing actual parse mode code like a boss:
{ parse_mode: 'markdown' }
- passing parse mode class like a cheems:
{ parse_mode: HTML }
- passing a value from
ParseMode
enum like a chad would do:
{ parse_mode: ParseMode.Markdown }
note
yeah also ParseMode
can be imported from puregram
natively:
const { ParseMode } = require('puregram')
final api request will look like this:
const message = `some ${HTML.bold('bold')} and ${HTML.italic('italic')} here`
context.send(message, { parse_mode: HTML })
context.send(`imagine using _classes_ for parse mode, *lol*!`, { parse_mode: 'markdown' })
the truth...
fuck this meme is obsolete now that i added ParseMode
enum
since markdown-v2 requires a lot of chars to be escaped, i've came up with a beautiful idea...
const message = MarkdownV2.build`
damn that's a cool usage of ${MarkdownV2.bold('template strings')}!
${MarkdownV2.italic('foo')} bar ${MarkdownV2.underline('baz')}
starkow v3 when
`
note
more markdown examples are available here
keyboards
puregram
has built-in classes for creating basic, inline, force-reply etc. keyboards. they are pretty much easy to use and are definitely more comfortable than building a json.
InlineKeyboard
, Keyboard
and so on
to create a keyboard, you need to call keyboard
method from the keyboard class you chose. this method accepts an array of button rows.
const { InlineKeyboard, Keyboard } = require('puregram')
const keyboard = InlineKeyboard.keyboard([
[
InlineKeyboard.textButton({
text: 'some text here',
payload: 'such payload'
}),
InlineKeyboard.textButton({
text: 'some more text here',
payload: { json: true }
})
],
[
InlineKeyboard.urlButton({
text: 'some url button',
url: 'https://example.com'
})
]
])
const keyboard = Keyboard.keyboard([
Keyboard.textButton('some one-row keyboard'),
Keyboard.textButton('with some buttons')
]).resize()
note
starting from puregram@2.14.0
, you can even use simple strings instead of Keyboard.textButton
s!
const keyboard = Keyboard.keyboard([
[
'first row, one button'
],
[
'second row, still one button!'
]
]).resize()
keyboard builders
there are also keyboard builders which are designed to be building a keyboard step by step:
const { KeyboardBuilder } = require('puregram')
const keyboard = new KeyboardBuilder()
.textButton('first row, first button')
.row()
.textButton('second row, first button')
.textButton('second row, second button')
.resize()
sending keyboards
to send keyboard, you simply need to pass the generated value in reply_markup
field:
context.send('look, here\'s a keyboard!', { reply_markup: keyboard })
Note
more keyboard examples are available here
bot information
if you are using puregram
's built-in polling logic, after Updates.startPolling()
is called you have access to Telegram.bot
property:
telegram.updates.startPolling().then(
() => console.log(`@${telegram.bot.username} started polling`)
)
what are contexts?
Context
is a class, containing current update
object and it's payload (via update[updateType]
). it is loaded with a ton of useful (maybe?) getters and methods that were made to shorten your code while being same efficient and executing the same code.
telegram.updates.on('message', (context) => {
const id = context.senderId
const id = context.from?.id
})
telegram.updates.on('message', (context) => {
context.send('hey!')
telegram.api.sendMessage({
chat_id: context.chat?.id,
text: 'hey!'
})
})
every context has telegram
property, so you can call api methods almost everywhere if you have a context nearby.
telegram.updates.on('message', async (context) => {
const me = await context.telegram.api.getMe()
})
action controller
sendChatAction
is a method that requires to be called every 5 seconds
before the action is complete. but how do you actually implement that?
even the simplest solutions require some hacky workarounds. that's why
puregram
encapsulates these hacks and you can use them right away,
even with a controller!
telegram.updates.on('message', (context) => {
const controller = context.createActionController('typing')
controller.start()
await sleep(14_000)
controller.stop()
return context.send('yeah so we are unable to deliver your message rn sorry')
})
of course, you are able to change controller.action
and all the options mentioned below while the controller is running
createActionController
options
key | type | required? | default | description |
---|
interval | number | no | 5000 | Interval between sendChatAction calls, in milliseconds |
wait | number | no | 0 | Initial wait before the first cycle of sendChatAction calls, in milliseconds |
timeout | number | no | 30000 | Timeout for sendChatAction calls, in milliseconds |
Context
and its varieties
every update in puregram
is handled by a special context, which is detected via the update key.
every context (except for manually created ones and some that were created after methods like sendMessage
) will have updateId
and update
properties.
property | required | description |
---|
updateId | no | unique update id. used as an offset when getting new updates |
update | no | update object. current context was created via this.update[this.updateType] |
for example, if we have the message
update, we will get MessageContext
on this update, CallbackQueryContext
for callback_query
update and so on.
every context requires one argument:
interface ContextOptions {
telegram: Telegram
updateType: UpdateName
update?: TelegramUpdate
updateId?: number
}
note
some contexts may be combined by a single structure because of how telegram bot api is built. what does this mean?
simplest examples are extra contexts:
their payload lies inside of Message
structure itself, so they are naturally also Message
s, meaning that they are also MessageContext
s.
telegram.updates.on('forum_topic_created', (context) => {
})
you can also create any context manually:
const { MessageContext } = require('puregram')
const update = await getUpdate()
const context = new MessageContext({
telegram,
update,
updateType: 'message',
updateId: update.update_id
})
note
every context is listed here
middlewares
puregram
implements middlewares logic, so you can use them to expand your context
variables or measure other middlewares.
next()
is used to call the next middleware on the chain and wait until it's done
- measuring the time it takes to process the update:
telegram.updates.use(async (context, next) => {
const start = Date.now()
await next()
const end = Date.now()
console.log(`${context.updateId ?? '[unknown]'} processed in ${end - start}ms`)
})
telegram.updates.use(async (context, next) => {
context.user = await getUser(context.senderId)
return next()
})
telegram.updates.on('message', (context) => {
return context.send(`hey, ${context.user.name}!`)
})
hooks
since v2.19.0
, puregram
has hooks - a way to intercept the outgoing (and ingoing soon) requests
and manipulate data in them. this means that you can create such an interceptor that will, for example,
always add parse_mode: 'html'
to your sendMessage
calls, or even abort (cancel) the requests!
telegram.onBeforeRequest((context) => {
if (context.path === 'sendMessage') {
context.params.parse_mode = 'html'
}
return context
})
telegram.updates.on('message', (context) => {
return context.reply('this <b>will be</b> <i>parsed</i> correctly!')
})
deeper into the woods hooks!
there are currently five hooks that you can use. each of them has their own set
of variables called context. you need to return the same structure of an object that was given to you when you caught it
each and every context has keys that the previous interceptor had. for example,
take this BaseContext
that every other context extends off of:
key | type | description |
---|
controller | AbortController | basic AbortController , allows to abort() the request |
init | undici.RequestInit | fetch() 's params object |
every context listed below will have those controller
and init
keys PLUS their own keys
hooks are listed below in order of their execution from top to bottom:
onBeforeRequest
: this hook is processed when the API request has been just caught and is starting to set everything up
key | type | description |
---|
path | string | API method path, sendMessage for example |
params | Record<string, any> | API method params |
onRequestIntercept
: hook that is executed right before the API call happens to be processed
key | type | description |
---|
query | string | URL query that was built by the params |
url | string | full API request URL |
-
API call. no hook for this, sorry!
-
onResponseIntercept
: API call has succeeded (probably), response
and json
are yours to experiment with
key | type | description |
---|
response | undici.Response | HTTP response that came from API |
json | ApiResponseUnion | HTTP response that morphed into JSON π» |
onAfterRequest
: everything that has to be done had been done, literally cleaning time
no additional keys are provided for onAfterRequest
and one more, onError
, which is covering the area between onRequestIntercept
and onAfterRequest
hooks
key | type | description |
---|
error | Error | simply an error that happened |
exporting hooks into packages
... or, to put simply, "how do i export more than one hook and use it easily?"
puregram
provides telegram.useHooks(hooks)
method that allows you to pass multiple hooks of different types
easily and instantly. this gradually helps importing several hooks at once if you're, for example, importing them
from another package:
import { hooks as imagination } from 'imaginary-package'
telegram.useHooks(imagination())
under the imaginary hood, hooks
(a.k.a. imagination
in this case) is a function (does not need to be a function though)
that returns puregram.Hooks
interface - an object that you can import from puregram/hooks
:
import { Hooks } from 'puregram/hooks'
export function hooks(): Hooks {
return () => ({
onBeforeRequest: [(context) => { ... }],
onAfterRequest: [(context) => { ... }]
})
}
that's it!
typescript usage
extending contexts
surely enough, you can extend contexts with extra fields and properties you need by intersectioning base context with new properties.
interface ExtraData {
name: string
id?: number
}
telegram.updates.use(async (context, next) => {
const user = await getUser(context.senderId)
context.name = user.name
context.id = user.id
return next()
})
telegram.updates.on<ExtraData>('message', (context) => {
assert(context.name !== undefined)
})
importing Telegram interfaces
all Telegram interfaces and method types are auto-generated and put in different files: telegram-interfaces.ts
for interfaces and methods.ts
+ api-methods.ts
for api methods. they all exist at the paths puregram/telegram-interfaces
, puregram/methods
and puregram/api-methods
respectively.
also there's a puregram/generated
export which exports everything from lib/generated
folder (all of those listed before).
import { TelegramUpdate, TelegramMessage } from 'puregram/generated'
import { SendDocumentParams } from 'puregram/generated'
import { CopyMessageParams } from 'puregram/methods'
import { InputFile, TelegramUpdate } from 'puregram/telegram-interfaces'
type predicates
puregram
implements type predicates (so-called type guards)
on some context methods (mostly on those that have is
/has
/can
at the start of the field name) in order to
keep connection between types and actual values
telegram.updates.on('message', (context) => {
const originalText = context.text
if (context.hasText()) {
const text = context.text
}
})
also, Context.is
is also a type guard! this means that you can do this and get a proper context typing whenever you want:
if (context.is('callback_query')) {
}
this is pretty useful when you have context: Context
and especially convenient because you don't have to import
the right contexts just to do this boring thing:
if (context instanceof CallbackQueryContext) {
}
note
because of type guards, it was decided to transition all getters starting with is
/has
/can
into methods in all structures.
this means that if you see a field starting with aforementioned parts you can be sure that this is definitely a method
and not a getter or a property!
faq
TypeError: Cannot read property '__scene' of undefined
you are trying to use @puregram/scenes
or @puregram/hear
with @puregram/session
, but you're confusing the middlewares order
you should firstly initialize @puregram/session
's middleware and only then initialize other middlewares, depending on it:
const hearManager = new HearManager()
telegram.updates.use(session())
telegram.updates.on('message', hearManager.middleware)
how do i enable debugging?
if you want to inspect out- and ingoing requests made by puregram
, you will need to enable DEBUG
environment variable so the package understands you are ready for logs.
how to enable DEBUG
namespace | example (unix) | description |
---|
api/getMe | DEBUG=puregram:api/getMe | enables debugging getMe update (you can set whichever method you want to debug) |
updates | DEBUG=puregram:updates | enables debugging ingoing updates |
all | DEBUG=puregram:* | enables debugging all of the listed types above |
cmd
> set "DEBUG=puregram:all" & node index
powershell
> $env:DEBUG = "puregram:all"; node index
linux
$ DEBUG=puregram:all node index
are there any telegram chats or channels?
totally! recently puregram
has created its own forum! it has every topic needed and
will be expanding if it needs to!
if you Β―\_(γ)_/Β― what to do and want to ask a question, @pureforum is definitely the way!
why is your readme lowercased?
because i dont like doing anything that looks official so i do my own styling π
btw did you see these issues?
they confirm im against anything that looks kinda too official π
ecosystem
these packages are created by the puregram
community (and not only) and are expanding packages functionality (i guess).
some official packages
non-official ones
thanks to
- negezor (negezor/vk-io) β for inspiration, package idea (!) and some code and implementation ideas