Modern Telegram client framework for TypeScript/JavaScript.
- Strictly typed;
- Described all the Telegram API methods;
- Max flexible;
- Built on middleware;
- Authorization helper;
- Getting updates;
- Encrypts secret keys
- Installation
- Getting started
- Authorization
- Requests
- Getting updates
- Data store
- Encryption
- Middleware
- Logging
- Components
# npm
npm install airgram
# yarn
yarn add airgram
Note: all the following examples created with TypeScript. Please check out JavaScript example.
Basic usage (source code):
import 'reflect-metadata' // Do not forget to import
import { Airgram, AuthDialog } from 'airgram'
import { prompt } from 'airgram/helpers'
// Obtain app id and hash here: https://my.telegram.org/apps
const app = {
id: Number(process.env.APP_ID!),
hash: process.env.APP_HASH!
}
const airgram = new Airgram(app)
// Authorization
const { auth } = airgram
airgram.use(auth)
auth.use(new AuthDialog({
phoneNumber: process.env.PHONE_NUMBER, // your phone number
code: () => prompt(`Please enter the secret code:\n`)
}))
// Updates
const { updates } = airgram
airgram.use(updates)
// Get all updates
updates.use((ctx, next) => {
console.log(ctx.update)
return next()
})
// Get only new message updates
updates.on('updateNewMessage', (ctx, next) => {
console.log('New message:', ctx.update)
return next()
})
updates.startPolling().then(() => {
console.log('Long polling started')
}).catch((error) => {
console.error(error)
})
// Get dialogs list
airgram.client.messages.getDialogs({
flags: 0,
limit: 30,
offset_date: 0,
offset_id: 0,
offset_peer: {_: 'inputPeerEmpty'},
}).then((dialogs) => {
console.info(dialogs)
}).catch((error) => {
console.error(error)
})
Airgram provides component Auth
, which implements basic logic for authorization or registration a new Telegram account. User just needs to specify the phone number, secret code and some other necessary information.
import Airgram, { AuthDialog } from 'airgram'
const airgram = new Airgram({ id: process.env.APP_ID, hash: process.env.APP_HASH })
const { auth } = airgram
auth.use((ctx: { _: string, state: { [key: string]: string } }, next: () => Promise<any>) => {
// Set default values
Object.assign(ctx.state, {
firstName: 'John',
lastName: 'Smith',
phoneNumber: '649965043265'
})
return next()
})
// You can use method `on` to call a callback only for given type of the requests
auth.on('code', async ({ state }, next) => {
state.code = await getSecretCodeFromSomewhere()
return next()
})
auth.login()
In the example above you have to implement getSecretCodeFromSomewhere()
function by yourself. That function returns Promise<string>
, where string
is a secret code received from Telegram.
You can use helper prompt
to communicate with user by the command line:
import { prompt } from 'airgram/helpers'
// ...
auth.on('code', async ({ state }, next) => {
state.code = await prompt('Please input the secret code:')
return next()
})
Component AuthDialog
implements authorization middleware and provides more intuitive interface:
import { Airgram, AuthDialog } from 'airgram'
import { prompt } from 'airgram/helpers'
const airgram = new Airgram({ id: process.env.APP_ID, hash: process.env.APP_HASH })
const { auth } = airgram
// Register authorization middleware instead of calling `airgram.auth.login()` manually
airgram.use(auth)
// Register middleware to receive data from user
auth.use(new AuthDialog({
firstName: 'John',
lastName: 'Smith',
phoneNumber: () => process.env.PHONE_NUMBER || prompt('Please input your phone number:'),
code: async () => prompt('Please input the secret code:'),
samePhoneNumber: ({ phoneNumber }) => prompt(`Do you want to sign in with the "${phoneNumber}" phone number? Y/n`)
.then((answer) => !['N', 'n'].includes(answer.charAt(0))),
continue: ({ phoneNumber }) => prompt(`Last authorization with the "${phoneNumber}" phone number has broken. If you have the secret code and wish to continue, input "Yes". Y/n`)
.then((answer) => !['N', 'n'].includes(answer.charAt(0)))
}))
Config that passed to AuthDialog
constructor has the following properties:
Key | Note |
---|---|
firstName |
Only for registration |
lastName |
Only for registration |
phoneNumber |
Only digits |
code |
The secret code received from Telegram |
continue |
Do not start login with zero if the secret code has already been sent |
samePhoneNumber |
Whether to use previous phone number or input the new one |
Each property value has a type: ((state: Partial<Answers>) => Promise<string>) | string | undefined
.
Default value: undefined
.
All Telegram API methods are described and have suitable methods in Airgram.
Account
Auth
Bots
Channels
Contacts
Help
Langpack
Messages
Payments
Phone
Photos
Stickers
Updates
Upload
Users
For getting updates use the Updates
component as shown below:
import { Airgram, api } from 'airgram'
const airgram = new Airgram(/* config */)
airgram.updates.getDifference().then((difference: api.UpdatesDifferenceUnion) => {
console.log('difference:', difference)
})
Note: method airgram.updates.getDifference()
is the wrapper for the method airgram.client.updates.getDifference()
, which has more complicated interface:
interface GetDifferenceParams {
date: number,
flags: number,
pts: number,
qts: number,
pts_total_limit?: number
}
// `getUpdatesState()` – it's your own function that returns the current updates state
const { pts, date, qts } = getUpdatesState()
const params: GetDifferenceParams = { pts, date, qts, flags: 0 }
airgram.client.updates.getDifference(params).then((difference) => {
console.log('difference:', difference)
})
In most cases you do not need to call getDifference()
directly. Airgram supports long polling connection to deliver updates as soon as they come.
Use component Updates
as middleware to handle incoming updates:
const { updates } = airgram
// Use the `Updates` component as a middleware
airgram.use(updates)
// Start to listen new updates
updates.startPolling()
If you need to break long polling connection use method stop()
:
updates.stop()
Use methods use()
and on()
to add your own handlers for updates. Do not forget to call next()
if you want to run next handlers.
// Get all updates
updates.use((ctx, next) => {
console.log(`Update type: ${ctx._}`)
return next()
})
// Get only new message updates
updates.on('updateNewMessage', (ctx, next) => {
console.log(`New message: ${ctx.update}`)
return next()
})
Argument ctx
is an object with the following structure:
Key | Type | Note |
---|---|---|
_ |
string | Updates type |
client |
Client |
The same as in airgram.client |
state |
{[key: string]: any} | Just a plain object. You should use it to pass some data between different middleware |
update |
{[key: string]: any} | An object with update data |
Some of the incoming updates have nested structure. Updates of this type are containers for other updates.
Container usually consists of a list of the chats and users, which are in the children updates. You should save them to your own store.
updates.use(({ update }, next) => {
if ('chats' in update && update.chats.length) {
// saveChats(update.chats)
}
if ('users' in update && update.users.length) {
// saveUsers(update.users)
}
return next()
})
Airgram keeps 4 types of data:
- updates state
- chat's updates state
- last login information
- Telegram access keys
By default all information written into a memory store. If you do not want to lose data between sessions you have to use persistent store.
In the next example we will show how to use PouchDB as a persistent store. Of course, you may easily create a provider for any other database.
Important: do not forget to encrypt all secret information. We will skip this part of the code to simplify it.
Airgram store has the following simple interface:
interface Store<DocT extends { [key: string]: any }> {
delete (key: string): Promise<void>
get (key: string): Promise<DocT | null>
get (key: string, field: string): Promise<any>
set (key: string, value: Partial<DocT>): Promise<Partial<DocT>>
}
Ok, lets implement it for PouchDB:
import PouchDB from 'pouchdb'
import UpsertPlugin from 'pouchdb-upsert'
PouchDB.plugin(UpsertPlugin)
// Here is using pouchdb-server
const db = new PouchDB(`http://127.0.0.1:5984/airgram`)
export default class PouchDBStore {
public async delete (id: string): Promise<void> {
try {
await db.remove(id)
} catch (e) {
throw e
}
}
public async get (key: string, field?: string): Promise<any> {
try {
const value = await db.get<DocT>(key)
return field ? value[field] : value
} catch (e) {
return null
}
}
public async set (id: string, doc: Partial<DocT>): Promise<Partial<DocT>> {
let nextDoc
return db.upsert(id, (currentDoc: DocT) => {
nextDoc = Object.assign({}, currentDoc, doc)
return nextDoc
}).then(() => nextDoc)
}
}
When the store component is created we can bind it to Airgram:
import { Airgram, TYPES } from 'airgram'
const airgram = new Airgram(/* config */)
airgram.bind(TYPES.AuthStore).to(PouchDBStore)
airgram.bind(TYPES.MtpStateStore).to(PouchDBStore)
Please follow to the example page
to see the source code.
By default, encryption of the secret keys is switched of. Follow this instruction to encrypt dangerous data:
// Set secret keys
airgram.client.crypto.setSecretKeys({
key: process.env.SECRET_KEY,
iv: process.env.SECRET_IV
})
// Set what have to be encrypted
airgram.client.mtpState.encryptedFields = ['authKey', 'serverSalt'] // or true | (field: string) => boolean
Use helper generateSecretKeys()
to generate random key
and iv
:
import { generateSecretKeys } from 'airgram/helpers'
const secret = generateSecretKeys()
console.log(secret)
// Output like:
// {
// iv: '0639aca8feb0d2b20da5c561a25c0c25',
// key: '49ad5e7b838924c316eedc83b2b7906f4b4058b577a26746895249ebae9d6764'
// }
Middleware is a chain of callback functions, which are called before the request is send to Telegram. Middleware allows you modify requests and responses to add some additional handlers.
Inside of the box Airgram provides 2 components you can use as middleware. There are Auth
and Updates
.
This is a scaffolding for middleware function:
airgram.use((ctx, next) => {
// Add some code here
return next()
})
Function takes 2 arguments: ctx
and next
.
Argument ctx
contains an object with the following structure:
Key | Type | Note |
---|---|---|
_ |
string |
Updates type |
client |
Client |
The same as in airgram.client |
state |
{ [key: string]: any } |
Just a plain object. You should use it to pass some data between different middleware |
handled |
boolean |
Whether the request has already been handled or not |
request |
{ [key: string]: any } |
Telegram request |
deferred |
{ resolve: (response: any) => any, reject: (error: Error) => any } |
You may immediately resolve or reject the Telegram request |
ctx.request
Key | Type | Note |
---|---|---|
method |
string |
Request method name |
params |
{[key: string]: any} |
Optional. Request parameters according to the Telegram schema |
options |
{[key: string]: any}} |
Optional. Additional options passed to the request |
The second argument next
is a callback function which runs the next handler.
Function next()
returns a promise Promise<any>
, so we can use the next handlers result inside of our middleware:
airgram.use(async (ctx, next) => {
const start = new Date()
const { request } = ctx
const result = await next()
console.log(`Method: ${request.method}, params: ${JSON.stringify(request.params)}, result: ${JSON.stringify(result)}, ${new Date() - start}ms`)
})
When we set middleware by use()
method, callback will be called for every request. Method on()
allows us to mount callback only to some type of requests.
airgram.on('messages.getDialogs', async (ctx, next) => {
const dialogs = await next()
updateMyDialogList(dialogs)
})
Sometimes middleware may be pretty complicated or you want to reuse the code. In this case the most suitable way is to create new class (or use existing) and pass it to use()
or on()
methods.
Your class must have a middleware()
method which returns a factory:
class MyMiddlewareClass {
handle(ctx) {
// do some work
}
middleware() {
return (ctx, next) => {
this.handle(ctx)
return next()
}
}
}
const myMiddleware = new MyMiddlewareClass()
airgram.use(myMiddleware)
By default, all messages with level info
and above fall into terminal via default console
utility. You can change this behavior.
Lets extend the Logger
component. We will add timestamp label and replace console
utility to debug
library:
import { Airgram, getCalleeName, Logger, TYPES } from 'airgram'
import * as createLogger from 'debug'
import moment from 'moment'
import { injectable } from 'inversify'
const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'
const writeLog = createLogger('airgram')
@injectable()
class DebugLogger extends Logger {
protected formatMessage (message, level) {
const parts = [
`${name}${' '.repeat(7 - level.name.length)} `,
`${moment().format(DATETIME_FORMAT)}: `,
this.namespace.map((v) => `[${v}] `)
]
return `${parts.join('')}${message}`
}
protected log (level, message) {
writeLog(this.formatMessage(message, level))
}
}
airgram.bind(TYPES.Logger).to(DebugLogger).onActivation((context, logger) => {
logger.namespace = [getCalleeName(context)] // log will show current component name
logger.level = 'verbose'
return logger
})
Sure, you may create your own logger class from scratch. You just need to implement the following interface:
export interface Logger {
namespace: string[]
debug (...args: any[]): void
verbose (...args: any[]): void
info (...args: any[]): void
warn (...args: any[]): void
error (...args: any[]): void
}
Under the hood Airgram uses inversion of control (IoC) container. It gives max flexibility. You may change or replace any part of framework.
The most popular cases when you need to inject your own component:
- custom logger (debug, winston and etc)
- persistent store
Please check out IoC container documentation.
import { Airgram, ag, DevLogger, TYPES } from 'airgram'
const airgram = new Airgram(/* config */)
airgram.bind<ag.Logger>(TYPES.Logger).to(DevLogger)
Airgram container is accessible via airgram.container
property.
Since all components are defined during initialization you should use method airgram.container.rebind
to avoid an error. There is a shortcut: airgram.bind
.
All the injectable components are contained in the variable TYPES
.
The most popular of them:
Type | Name |
---|---|
AuthStore |
Keeps the condition of whether the user is logged in or not |
ChatStore |
Store that keeps chat's state |
Client |
Value of the airgram.client property |
MtpStateStore |
Store for telegram secret keys |
Logger |
By default the logger use console |
UpdatesStateStore |
Updates state |
The source code is licensed under GPL v3. License is available here.