sce
@puregram/scenes
npm i @puregram/scenes
sce

@puregram/scenes

powerful and modern telegram bot api sdk for node.js and typescript 😁

by starków

2.0.1-rc.1 (see all)License:MITTypeScript:Built-In
npm i @puregram/scenes
Readme

powerful and epic overall, puregram allows you to easily interact with telegram bot api via node.js 😎👍

examples  •  typescript usage  •  telegram channel  •  faq

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.on('callback_query', context => context.answerCallbackQuery())

telegram.updates.startPolling()

you can find more examples here


table of contents


why puregram?

  • written by starków
  • 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:

  1. polling via getUpdates method... or just using puregram's built-in polling logic:
telegram.updates.startPolling()
  1. setting up a Webhook via setWebhook method:
const { createServer } = require('http')

// you need to send this request only once
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.

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!'))

list of supported updates will be somewhere here when it's done

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:

gettertypedescription
idstringmedia group's id
contextsMessageContext[]list of received (and processed) contexts which contain an attachment
attachmentsAttachment[]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:

/** let's pretend i'm polling updates manually... */

const update = await getUpdate(...)

let context

try {
  context = await telegram.updates.handleUpdate(update)
} catch (error) {
  console.log('update is not supported', update)
}

// voila! now you have the right context
// (or you don't if the event is not supported 😢)

calling api methods

there are three ways of calling telegram bot api methods:

  1. 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')
  1. using telegram.api.method(params?):
const me = await telegram.api.getMe()
  1. using context methods:
telegram.updates.on('message', context => context.send('13² = 169! i mean "169", not "169!'))

sending media

puregram allows you to send your local media by using MediaSource class. you can put URLs, Buffers, streams and paths in it.

/** let's imagine we have an image called puppy.jpg in this directory... */

const { createReadStream } = require('fs')

const path = './puppy.jpg'
const stream = createReadStream(path)
const buffer = getBuffer(path)
const url = 'https://puppies.com/random-puppy'

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!!!' })
  ])
})

this works for every method that can send media.


using markdown

if you want to use markdown or html, there are two ways of doing that:

  1. using built-in HTML, Markdown and MarkdownV2 classes:
const message = HTML.bold('very bold, such html')
  1. 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 of doing that ¯\_(ツ)_/¯:

  1. writing actual parse mode code like a boss:
{ parse_mode: 'markdown' }
  1. passing parse mode class like a cheems:
{ parse_mode: HTML }

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...

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([
  [ // first row
    InlineKeyboard.textButton({ // first row, first button
      text: 'some text here',
      payload: 'such payload'
    }),

    InlineKeyboard.textButton({ // first row, second button
      text: 'some more text here',
      payload: { json: true }
    })
  ],

  [ // second row
    InlineKeyboard.urlButton({ // second row, first button
      text: 'some url button',
      url: 'https://example.com'
    })
  ]
])
// one-row keyboard with two buttons, no brackets for rows needed
const keyboard = Keyboard.keyboard([
  Keyboard.textButton('some one-row keyboard'),
  Keyboard.textButton('with some buttons')
])

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() // keyboard will be much smaller

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 })

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
  // is the same as
  const id = context.from?.id
})
telegram.updates.on('message', (context) => {
  context.send('hey!')
  // equals to
  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()
})

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.

propertyrequireddescription
updateIdnounique update id. used as an offset when getting new updates
updatenoupdate 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 {
  // main Telegram instance
  telegram: Telegram

  // update type, e.g. 'message', 'callback_query'
  updateType: UpdateName
  
  // whole update object
  // optional, allows user to do the `context.update` to get the whole update object
  update?: TelegramUpdate

  // update id, located at TelegramUpdate
  // optional, allows user to get this update's id
  updateId?: number
}

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
})

every context is listed here


middlewares

puregram uses middlewares, 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 proceed the update:

telegram.updates.use(async (context, next) => {
  const start = Date.now()

  await next() // next() is async, so we need to await it

  const end = Date.now()

  console.log(`${context.updateId ?? '[unknown]'} proceeded in ${end - start}ms`)
})

extending the context:

telegram.updates.use(async (context, next) => {
  context.user = await getUser(context.senderId)

  return next()
})

telegram.updates.on('message', (context) => {
  // here we can access property we made in the middleware
  return context.send(`hey, ${context.user.name}!`)
})

typescript usage

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'

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()
})

/**
 * there are 2 ways of updating context's type:
 * 1. external type override:
 * `(context: MessageContext & ExtraData) => ...`
 * 2. using generics:
 * `telegram.updates.on<ExtraData>(...)`
 * 
 * below I will be using the second way.
 */

telegram.updates.on<ExtraData>('message', (context) => {
  assert(context.name !== undefined)
})

faq

TypeError: Cannot read property '__scene' of undefined

you are trying to use @puregram/scenes or @puregram/hear with @puregram/session, but you're the confusing middlewares order.

you should firstly initialize @puregram/session's middleware and only then initialize other middlewares, depending on it:

const sessionManager = new SessionManager()
const hearManager = new HearManager()

// 1. session middleware first
telegram.updates.on('message', sessionManager.middleware)

// 2. hear middleware second
telegram.updates.on('message', hearManager.middleware)

ExperimentalWarning: buffer.Blob is an experimental feature.

yes, this is because ^2.5.0 versions are using undici's fetch which is experimental atm. you can look it up here for future changes

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

namespaceexample (unix)description
apiDEBUG=puregram:apienables debugging api out- and ingoing requests
api/getMeDEBUG=puregram:api/getMeenables debugging getMe update (you can set whichever method you want to debug)
updatesDEBUG=puregram:updatesenables debugging ingoing updates
allDEBUG=puregram:allenables 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?

yeah, there are.

whathow to get here
channel 📢click
russian chat 🇷🇺press
english chat 🇬🇧: .。. o(≧▽≦)o .。.:
puregram chats list 🍔d=====( ̄▽ ̄*)b
offtop chat 👀ᕦ( ⊡ 益 ⊡ )ᕤ

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 😉


community

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

Downloads/wk

31

GitHub Stars

57

LAST COMMIT

1mo ago

MAINTAINERS

1

CONTRIBUTORS

6

OPEN ISSUES

0

OPEN PRs

0
VersionTagPublished
2.0.1-rc.1
latest
1yr ago
No alternatives found
No tutorials found
Add a tutorial