├── .gitignore
├── src
├── structs
│ ├── index.js
│ └── TextStruct.js
├── errors
│ ├── ClientFetchError.js
│ ├── ClientNotCreatedError.js
│ ├── InvalidBotTokenError.js
│ ├── ClientCreateError.js
│ ├── InvalidCallbackError.js
│ └── index.js
├── main.module.js
├── main.node.js
├── ClientNode
│ ├── utils.js
│ └── index.js
├── ClientWasm
│ └── index.js
├── ClientBase.js
└── TG.js
├── rollup.config.js
├── examples
├── getChats.js
├── handleUpdates.js
├── sendMessage.js
├── botCommand.js
└── multiClients.js
├── package.json
├── test
└── test.js
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | package-lock.json
4 | /node.js
5 | /wasm.js
6 |
--------------------------------------------------------------------------------
/src/structs/index.js:
--------------------------------------------------------------------------------
1 | import TextStruct from './TextStruct'
2 |
3 | export {
4 | TextStruct,
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/ClientFetchError.js:
--------------------------------------------------------------------------------
1 | export default class ClientFetchError extends Error {
2 | constructor(update) {
3 | super()
4 | this.message = JSON.stringify(update)
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/errors/ClientNotCreatedError.js:
--------------------------------------------------------------------------------
1 | export default class ClientNotCreatedError extends Error {
2 | constructor(error) {
3 | super(error)
4 | this.message = 'Client is not created'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/errors/InvalidBotTokenError.js:
--------------------------------------------------------------------------------
1 | export default class InvalidBotTokenError extends Error {
2 | constructor() {
3 | super()
4 | this.message = 'Provided Bot Token is not valid'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.module.js:
--------------------------------------------------------------------------------
1 | import Client from './ClientWasm/index.js'
2 | import * as Errors from './errors/index.js'
3 | import * as Structs from './structs/index.js'
4 |
5 | export { Client, Errors, Structs }
6 |
--------------------------------------------------------------------------------
/src/main.node.js:
--------------------------------------------------------------------------------
1 | import Client from './ClientNode/index.js'
2 | import * as Errors from './errors/index.js'
3 | import * as Structs from './structs/index.js'
4 |
5 | export { Client, Errors, Structs }
6 |
--------------------------------------------------------------------------------
/src/errors/ClientCreateError.js:
--------------------------------------------------------------------------------
1 | export default class ClientCreateError extends Error {
2 | constructor(error) {
3 | super(error)
4 | this.message = `Unable to create client: ${error.message}`
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/errors/InvalidCallbackError.js:
--------------------------------------------------------------------------------
1 | export default class InvalidCallbackError extends Error {
2 | constructor(eventName) {
3 | super()
4 | this.message = `"${eventName}" is not a valid callback.`
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/errors/index.js:
--------------------------------------------------------------------------------
1 | import InvalidCallbackError from './InvalidCallbackError'
2 | import InvalidBotTokenError from './InvalidBotTokenError'
3 | import ClientCreateError from './ClientCreateError'
4 | import ClientNotCreatedError from './ClientNotCreatedError'
5 | import ClientFetchError from './ClientFetchError'
6 |
7 | export {
8 | InvalidCallbackError,
9 | InvalidBotTokenError,
10 | ClientCreateError,
11 | ClientNotCreatedError,
12 | ClientFetchError,
13 | }
14 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import json from 'rollup-plugin-json';
2 | import pkg from './package.json';
3 |
4 | export default [
5 | // ES module (for Node) build.
6 | {
7 | input: 'src/main.node.js',
8 | external: [],
9 | output: [
10 | { file: pkg.main, format: 'cjs' }
11 | ],
12 | plugins: [
13 | json()
14 | ]
15 | },
16 | // ES module (for browser bundlers) build.
17 | {
18 | input: 'src/main.module.js',
19 | external: [],
20 | output: [
21 | { file: pkg.module, format: 'es' }
22 | ],
23 | plugins: [
24 | json()
25 | ]
26 | },
27 | ];
28 |
--------------------------------------------------------------------------------
/src/structs/TextStruct.js:
--------------------------------------------------------------------------------
1 | export default class TextStruct {
2 | constructor(text, parseMode) {
3 | this.text = text
4 | if (['textParseModeHTML', 'textParseModeMarkdown'].indexOf(parseMode) >= 0) {
5 | this.parseMode = parseMode
6 | }
7 | }
8 |
9 | // Internal method
10 | async _format(_client) {
11 | let args
12 | if (this.parseMode) {
13 | args = await _client._execute({
14 | '@type': 'parseTextEntities',
15 | 'text': this.text,
16 | 'parse_mode': { '@type': this.parseMode },
17 | })
18 | } else {
19 | args = {
20 | '@type': 'formattedText',
21 | 'text': this.text,
22 | }
23 | }
24 | return args
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ClientNode/utils.js:
--------------------------------------------------------------------------------
1 | import ref from 'ref-napi'
2 | import inquirer from 'inquirer'
3 |
4 | export function buildQuery(query) {
5 | const buffer = Buffer.from(JSON.stringify(query) + '\0', 'utf-8')
6 | buffer.type = ref.types.CString
7 | return buffer
8 | }
9 |
10 | export async function getInput({ string, extras: { hint } = {} }) {
11 | let input = ''
12 | while (!input.length) {
13 | const type = (
14 | string.startsWith('tglib.input.AuthorizationCode') ||
15 | string.startsWith('tglib.input.AuthorizationPassword')
16 | ) ? 'password' : 'input'
17 | const message = `${string}${ hint ? ` (${hint})` : '' }`
18 | const result = await inquirer.prompt([
19 | { type, name: 'input', message },
20 | ])
21 | input = result.input
22 | }
23 | return input
24 | }
25 |
--------------------------------------------------------------------------------
/examples/getChats.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('tglib')
2 |
3 | void async function() {
4 | const client = new Client({
5 | apiId: 'YOUR_API_ID',
6 | apiHash: 'YOUR_API_HASH',
7 | })
8 |
9 | // Save tglib default handler which prompt input at console
10 | const defaultHandler = client.callbacks['td:getInput']
11 |
12 | // Register own callback for returning auth details
13 | client.registerCallback('td:getInput', async (args) => {
14 | if (args.string === 'tglib.input.AuthorizationType') {
15 | return 'user'
16 | } else if (args.string === 'tglib.input.AuthorizationValue') {
17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER'
18 | }
19 | return await defaultHandler(args)
20 | })
21 |
22 | await client.ready
23 |
24 | const result = await client.fetch({
25 | '@type': 'getChats',
26 | 'offset_order': '9223372036854775807',
27 | 'offset_chat_id': 0,
28 | 'limit': 100,
29 | })
30 |
31 | // latest 100 chat id will be returned
32 | console.log(result)
33 | }()
34 |
--------------------------------------------------------------------------------
/examples/handleUpdates.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('tglib')
2 |
3 | void async function() {
4 | const client = new Client({
5 | apiId: 'YOUR_API_ID',
6 | apiHash: 'YOUR_API_HASH',
7 | })
8 |
9 | // Save tglib default handler which prompt input at console
10 | const defaultHandler = client.callbacks['td:getInput']
11 |
12 | // Register own callback for returning auth details
13 | client.registerCallback('td:getInput', async (args) => {
14 | if (args.string === 'tglib.input.AuthorizationType') {
15 | return 'user'
16 | } else if (args.string === 'tglib.input.AuthorizationValue') {
17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER'
18 | }
19 | return await defaultHandler(args)
20 | })
21 |
22 | client.registerCallback('td:update', (update) => {
23 | console.log('Got update:', JSON.stringify(update, null, 2))
24 | })
25 |
26 | client.registerCallback('td:error', (update) => {
27 | console.error('Got error:', JSON.stringify(update, null, 2))
28 | })
29 |
30 | await client.ready
31 | }()
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tglib",
3 | "version": "3.0.5",
4 | "author": "nodegin",
5 | "license": "MIT",
6 | "description": "TDLib (Telegram Database library) bindings for Node.js",
7 | "homepage": "https://github.com/nodegin/tglib#readme",
8 | "bugs": {
9 | "url": "https://github.com/nodegin/tglib/issues"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/nodegin/tglib.git"
14 | },
15 | "keywords": [
16 | "tdlib",
17 | "telegram"
18 | ],
19 | "main": "node.js",
20 | "module": "wasm.js",
21 | "scripts": {
22 | "build": "rollup -c",
23 | "dev": "rollup -c -w",
24 | "test": "node test/test.js",
25 | "pretest": "npm run build",
26 | "prepublish": "npm run build"
27 | },
28 | "dependencies": {
29 | "crc": "^3.8.0",
30 | "event-emitter": "^0.3.5",
31 | "ffi-napi": "^2.4.5",
32 | "fs-extra": "^7.0.1",
33 | "inquirer": "^6.2.2",
34 | "ref-napi": "^1.4.1"
35 | },
36 | "devDependencies": {
37 | "rollup": "^1.0.0",
38 | "rollup-plugin-json": "^4.0.0"
39 | },
40 | "files": [
41 | "node.js",
42 | "wasm.js"
43 | ],
44 | "engines": {
45 | "node": ">= 9.0.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/examples/sendMessage.js:
--------------------------------------------------------------------------------
1 | const { Client, Structs } = require('tglib')
2 |
3 | void async function() {
4 | const client = new Client({
5 | apiId: 'YOUR_API_ID',
6 | apiHash: 'YOUR_API_HASH',
7 | })
8 |
9 | // Save tglib default handler which prompt input at console
10 | const defaultHandler = client.callbacks['td:getInput']
11 |
12 | // Register own callback for returning auth details
13 | client.registerCallback('td:getInput', async (args) => {
14 | if (args.string === 'tglib.input.AuthorizationType') {
15 | return 'user'
16 | } else if (args.string === 'tglib.input.AuthorizationValue') {
17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER'
18 | }
19 | return await defaultHandler(args)
20 | })
21 |
22 | await client.ready
23 |
24 | await client._send({
25 | '@type': 'sendMessage',
26 | 'chat_id': -123456789,
27 | 'input_message_content': {
28 | '@type': 'inputMessageText',
29 | 'text': {
30 | '@type': 'formattedText',
31 | 'text': 'Hi',
32 | },
33 | },
34 | })
35 |
36 | // or use tglib API
37 | await client.tg.sendTextMessage({
38 | '$text': new Structs.TextStruct('Hi bold', 'textParseModeHTML'),
39 | 'chat_id': -123456789,
40 | })
41 | }()
42 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const howLongTillLunch = require('..');
3 |
4 | function MockDate () {
5 | this.date = 0;
6 | this.hours = 0;
7 | this.minutes = 0;
8 | this.seconds = 0;
9 | this.milliseconds = 0;
10 | };
11 |
12 | Object.assign(MockDate.prototype, {
13 | getDate () { return this.date; },
14 | setDate (date) { this.date = date; },
15 | setHours (h) { this.hours = h; },
16 | setMinutes (m) { this.minutes = m; },
17 | setSeconds (s) { this.seconds = s; },
18 | setMilliseconds (ms) { this.milliseconds = ms; },
19 | valueOf () {
20 | return (
21 | this.milliseconds +
22 | this.seconds * 1e3 +
23 | this.minutes * 1e3 * 60 +
24 | this.hours * 1e3 * 60 * 60 +
25 | this.date * 1e3 * 60 * 60 * 24
26 | );
27 | }
28 | });
29 |
30 | const now = new MockDate();
31 | MockDate.now = () => now.valueOf();
32 |
33 | global.Date = MockDate;
34 |
35 | function test(hours, minutes, seconds, expected) {
36 | now.setHours(hours);
37 | now.setMinutes(minutes);
38 | now.setSeconds(seconds);
39 |
40 | assert.equal(howLongTillLunch(...lunchtime), expected);
41 | console.log(`\u001B[32m✓\u001B[39m ${expected}`);
42 | }
43 |
44 | let lunchtime = [ 12, 30 ];
45 | test(11, 30, 0, '1 hour');
46 | test(10, 30, 0, '2 hours');
47 | test(12, 25, 0, '5 minutes');
48 | test(12, 29, 15, '45 seconds');
49 | test(13, 30, 0, '23 hours');
50 |
51 | // some of us like an early lunch
52 | lunchtime = [ 11, 0 ];
53 | test(10, 30, 0, '30 minutes');
--------------------------------------------------------------------------------
/examples/botCommand.js:
--------------------------------------------------------------------------------
1 | const { Client, Structs } = require('tglib')
2 |
3 | void async function() {
4 | const client = new Client({
5 | apiId: 'YOUR_API_ID',
6 | apiHash: 'YOUR_API_HASH',
7 | })
8 |
9 | // Save tglib default handler which prompt input at console
10 | const defaultHandler = client.callbacks['td:getInput']
11 |
12 | // Register own callback for returning auth details
13 | client.registerCallback('td:getInput', async (args) => {
14 | if (args.string === 'tglib.input.AuthorizationType') {
15 | return 'bot'
16 | } else if (args.string === 'tglib.input.AuthorizationValue') {
17 | return 'YOUR_BOT_TOKEN'
18 | }
19 | return await defaultHandler(args)
20 | })
21 |
22 | await client.ready
23 |
24 | const { id: myId } = await client.fetch({ '@type': 'getMe' })
25 |
26 | client.registerCallback('td:update', async (update) => {
27 | if (update['@type'] === 'updateNewMessage') {
28 | // check if message is sent from self
29 | const sender = update['message']['sender_user_id']
30 | if (sender !== myId) {
31 | const { text: { text } } = update['message']['content']
32 | let replyText
33 | if (text.startsWith('/')) {
34 | replyText = `Are you requested ${text}?`
35 | } else {
36 | replyText = `Sorry I do not understand ${text}.`
37 | }
38 | await client.tg.sendTextMessage({
39 | '$text': new Structs.TextStruct(replyText, 'textParseModeHTML'),
40 | 'chat_id': 123456789,
41 | 'disable_notification': true,
42 | 'clear_draft': false,
43 | })
44 | }
45 | }
46 | })
47 | }()
48 |
--------------------------------------------------------------------------------
/examples/multiClients.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { Client, Structs } = require('tglib')
3 |
4 | async function initClients() {
5 | const clients = {}
6 | const credentials = {
7 | alice: { type: 'user', value: 'YOUR_INTERNATIONAL_PHONE_NUMBER' },
8 | }
9 |
10 | for (const key in credentials) {
11 | try {
12 | const { type, value } = credentials[key]
13 |
14 | // Create new client for each credential
15 | // Storing data in corresponding directories
16 | const client = new Client({
17 | apiId: 'YOUR_API_ID',
18 | apiHash: 'YOUR_API_HASH',
19 | appDir: path.resolve(process.cwd(), `__tglib-${type}-${value}__`),
20 | })
21 |
22 | // Save tglib default handler which prompt input at console
23 | const defaultHandler = client.callbacks['td:getInput']
24 |
25 | // Register own callback for returning auth details
26 | client.registerCallback('td:getInput', async (args) => {
27 | if (args.string === 'tglib.input.AuthorizationType') {
28 | return type
29 | } else if (args.string === 'tglib.input.AuthorizationValue') {
30 | return value
31 | }
32 | return await defaultHandler(args)
33 | })
34 |
35 | // Wait for client ready
36 | await client.ready
37 | clients[key] = client
38 | } catch (e) {
39 | console.log(`Cannot create ${key}: `, e.message)
40 | }
41 | }
42 |
43 | return clients
44 | }
45 |
46 | void async function() {
47 | const clients = await initClients()
48 |
49 | await clients.alice.sendTextMessage({
50 | '$text': new Structs.TextStruct('hello'),
51 | 'chat_id': 123456789,
52 | 'disable_notification': true,
53 | 'clear_draft': false,
54 | })
55 | }()
56 |
--------------------------------------------------------------------------------
/src/ClientWasm/index.js:
--------------------------------------------------------------------------------
1 | import ClientBase from '../ClientBase'
2 |
3 | import {
4 | ClientCreateError,
5 | ClientNotCreatedError,
6 | } from '../errors/index.js'
7 |
8 | class ClientWASM extends ClientBase {
9 | constructor(options = {}) {
10 | super('wasm', options)
11 | }
12 |
13 | async init() {
14 | try {
15 | const {
16 | verbosityLevel,
17 | wasmModule,
18 | } = this.options
19 |
20 | // See also:
21 | // https://github.com/tdlib/td/blob/master/td/telegram/td_emscripten.cpp
22 | // https://github.com/tdlib/td/blob/master/example/web/tdweb/src/worker.js#L495
23 | this.tdlib = {
24 | td_create: wasmModule.cwrap('td_create', 'number', []),
25 | td_send: wasmModule.cwrap('td_send', null, ['number', 'string']),
26 | td_receive: wasmModule.cwrap('td_receive', 'string', ['number']),
27 | td_execute: wasmModule.cwrap('td_execute', 'string', ['number', 'string']),
28 | td_destroy: wasmModule.cwrap('td_destroy', null, ['number']),
29 | }
30 | this.tdlib.td_execute(
31 | 0,
32 | JSON.stringify({
33 | '@type': 'setLogVerbosityLevel',
34 | new_verbosity_level: verbosityLevel,
35 | })
36 | )
37 |
38 | this.client = await this._create()
39 | } catch (error) {
40 | this.rejector(new ClientCreateError(error))
41 | return
42 | }
43 | this.loop()
44 | }
45 |
46 | async _create() {
47 | const client = await this.tdlib.td_create()
48 | return client
49 | }
50 |
51 | async _send(query) {
52 | if (!this.client) {
53 | throw new ClientNotCreatedError()
54 | }
55 | const response = await this.tdlib.td_send(this.client, JSON.stringify(query))
56 | if (!response) {
57 | return null
58 | }
59 | return JSON.parse(response)
60 | }
61 |
62 | async _receive() {
63 | if (!this.client) {
64 | throw new ClientNotCreatedError()
65 | }
66 | const response = await this.tdlib.td_receive(this.client)
67 | if (!response) {
68 | return null
69 | }
70 | return JSON.parse(response)
71 | }
72 |
73 | async _execute(query) {
74 | if (!this.client) {
75 | throw new ClientNotCreatedError()
76 | }
77 | const response = await this.tdlib.td_execute(this.client, JSON.stringify(query))
78 | if (!response) {
79 | return null
80 | }
81 | return JSON.parse(response)
82 | }
83 |
84 | async _destroy() {
85 | if (this.client) {
86 | await this.tdlib.td_destroy(this.client)
87 | this.client = null
88 | }
89 | }
90 | }
91 |
92 | export default ClientWASM
93 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | #### tglib v3.0.3
4 |
5 | - Added a new high level API `client.tg.download` for downloading files
6 |
7 | -----
8 |
9 | #### tglib v3.0.2
10 |
11 | - Fix WASM verbosity setting for TDLib 1.4.0 (provides official WASM suppor & example)
12 |
13 | -----
14 |
15 | #### tglib v3.0.1
16 |
17 | - Added a new high level API `client.tg.call`
18 |
19 | -----
20 |
21 | #### tglib v3.0.0
22 |
23 | WARNING: This release came with multiple breaking changes, please see README.md / examples for updated usages.
24 |
25 | - Breaking changes: Removed `dataDir` option in flavor of `appDir`
26 | - Breaking changes: Removed `auth` option
27 | - Added support for WebAssembly
28 |
29 | -----
30 |
31 | #### tglib v2.1.0
32 |
33 | - Added a new option `dataDir` for specifying `__tglib__` data directory.
34 | - Added a new option `binaryPath` for specifying TDLib binary.
35 | - Added a new high level API `client.tg.getChat`.
36 | - Fixed issue #36, update loop will now stop when client is destroyed.
37 |
38 | -----
39 |
40 | #### tglib v2.0.0
41 |
42 | - Breaking changes: Rewrote `client.tg.sendTextMessage`.
43 |
44 | *The old way to send text message:*
45 | ```js
46 | await client.tg.sendTextMessage('123456789', 'Hello *World*', {
47 | 'parse_mode': 'markdown',
48 | 'disable_notification': true,
49 | 'clear_draft': false,
50 | })
51 | ```
52 | *The new way to send text message:*
53 | ```js
54 | const { TextStruct } = require('tglib/structs')
55 |
56 | await client.tg.sendTextMessage({
57 | '$text': new TextStruct('`Hello` world!', 'textParseModeMarkdown'),
58 | 'chat_id': 123456789,
59 | 'disable_notification': true,
60 | 'clear_draft': false,
61 | })
62 | ```
63 | - Added `tglib/structs` for common structures.
64 | - Added `TextStruct` for formatted text struct, used in `inputMessageText.text`, `inputMessagePhoto.caption`, etc.
65 | ```js
66 | const text1 = new TextStruct('Normal text')
67 | const text2 = new TextStruct('`Markdown` text', 'textParseModeMarkdown')
68 | const text3 = new TextStruct('HTML text', 'textParseModeHTML')
69 | ```
70 | - Added a new high level API `client.tg.sendPhotoMessage`.
71 | ```js
72 | const { TextStruct } = require('tglib/structs')
73 |
74 | await client.tg.sendPhotoMessage({
75 | '$caption': new TextStruct('Such doge much wow'),
76 | 'chat_id': 123456789,
77 | 'path': '/tmp/doge.jpg',
78 | 'ttl': 5,
79 | })
80 | ```
81 | - Breaking changes: `client.on` now renamed to `client.registerCallback`.
82 |
83 | *The old way to receive updates and errors:*
84 | ```js
85 | client.on('_update', (update) => console.log(update))
86 | client.on('_error', (error) => console.error(error))
87 | ```
88 |
89 | *The new way to receive updates and errors:*
90 | ```js
91 | client.registerCallback('td:update', (update) => console.log(update))
92 | client.registerCallback('td:error', (error) => console.error(error))
93 | ```
94 | - Added a new callback `td:getInput` to split out the `getInput` function from core, you can now register your own callback function to handle the login process.
95 | ```js
96 | client.registerCallback('td:getInput', async (args) => {
97 | const result = await getInputFromUser(args)
98 | return result
99 | })
100 | ```
101 | -----
102 |
103 | #### tglib v1.4
104 |
105 | - Added a new high level API `client.tg.updateUsername`.
106 | - Added a new high level API `client.tg.getAllChats`.
107 | - Added a new high level API `client.tg.openSecretChat`.
108 | - Added a new high level API `client.tg.deleteChat`.
109 | - Fixes new user authorization. (#24)
110 | - Improve documentations.
111 |
112 | -----
113 |
114 | #### tglib v1.3
115 |
116 | - Now support multiple session login by storing files in separate directories. (#16, #19)
117 | - Ability to login as Bots.
118 | ```diff
119 | const client = new Client({
120 | apiId: 'YOUR_API_ID',
121 | apiHash: 'YOUR_API_HASH',
122 | - phoneNumber: 'YOUR_PHONE_NUMBER',
123 | + auth: {
124 | + type: 'user',
125 | + value: 'YOUR_PHONE_NUMBER',
126 | + },
127 | })
128 | ```
129 | - Error events for `client.fetch` are now handled. `ClientFetchError` will be thrown if `client.fetch` fails.
130 | - `client.tg.sendMessageTypeText` renamed to `client.tg.sendTextMessage`.
131 | - `client.connect()` changed to `client.ready`.
132 |
--------------------------------------------------------------------------------
/src/ClientNode/index.js:
--------------------------------------------------------------------------------
1 | import ClientBase from '../ClientBase'
2 |
3 | import ffi from 'ffi-napi'
4 | import fs from 'fs-extra'
5 | import path from 'path'
6 | import { buildQuery, getInput } from './utils'
7 | import {
8 | ClientCreateError,
9 | ClientNotCreatedError,
10 | } from '../errors/index.js'
11 |
12 | class ClientNode extends ClientBase {
13 | constructor(options = {}) {
14 | super('node', {
15 | appDir: path.resolve(process.cwd(), '__tglib__'),
16 | binaryPath: path.resolve(process.cwd(), 'libtdjson'),
17 | ...options,
18 | })
19 | // register inquirer getInput function as default
20 | this.registerCallback('td:getInput', getInput)
21 | }
22 |
23 | async init() {
24 | try {
25 | const {
26 | appDir,
27 | binaryPath,
28 | verbosityLevel,
29 | } = this.options
30 |
31 | await fs.ensureDir(appDir)
32 |
33 | // update options
34 | this.options.filesDir = path.resolve(appDir, 'files')
35 | this.options.databaseDir = path.resolve(appDir, 'database')
36 |
37 | this.tdlib = ffi.Library(binaryPath, {
38 | 'td_json_client_create' : ['pointer', []],
39 | 'td_json_client_send' : ['void' , ['pointer', 'string']],
40 | 'td_json_client_receive' : ['string' , ['pointer', 'double']],
41 | 'td_json_client_execute' : ['string' , ['pointer', 'string']],
42 | 'td_json_client_destroy' : ['void' , ['pointer']],
43 | 'td_set_log_file_path' : ['int' , ['string']],
44 | 'td_set_log_verbosity_level' : ['void' , ['int']],
45 | 'td_set_log_fatal_error_callback': ['void' , ['pointer']],
46 | })
47 | this.tdlib.td_set_log_file_path(path.resolve(appDir, 'logs.txt'))
48 | this.tdlib.td_set_log_verbosity_level(verbosityLevel)
49 | this.tdlib.td_set_log_fatal_error_callback(ffi.Callback('void', ['string'], (message) => {
50 | console.error('TDLib Fatal Error:', message)
51 | }))
52 |
53 | this.client = await this._create()
54 | } catch (error) {
55 | this.rejector(new ClientCreateError(error))
56 | return
57 | }
58 | this.loop()
59 | }
60 |
61 | _create() {
62 | return new Promise((resolve, reject) => {
63 | this.tdlib.td_json_client_create.async((err, client) => {
64 | if (err) {
65 | return reject(err)
66 | }
67 | resolve(client)
68 | })
69 | })
70 | }
71 |
72 | _send(query) {
73 | return new Promise((resolve, reject) => {
74 | if (!this.client) {
75 | return reject(new ClientNotCreatedError())
76 | }
77 | this.tdlib.td_json_client_send.async(this.client, buildQuery(query), (err, response) => {
78 | if (err) {
79 | return reject(err)
80 | }
81 | if (!response) {
82 | return resolve(null)
83 | }
84 | resolve(JSON.parse(response))
85 | })
86 | })
87 | }
88 |
89 | _receive(timeout = 10) {
90 | return new Promise((resolve, reject) => {
91 | if (!this.client) {
92 | return reject(new ClientNotCreatedError())
93 | }
94 | this.tdlib.td_json_client_receive.async(this.client, timeout, (err, response) => {
95 | if (err) {
96 | return reject(err)
97 | }
98 | if (!response) {
99 | return resolve(null)
100 | }
101 | resolve(JSON.parse(response))
102 | })
103 | })
104 | }
105 |
106 | _execute(query) {
107 | return new Promise((resolve, reject) => {
108 | if (!this.client) {
109 | return reject(new ClientNotCreatedError())
110 | }
111 | try {
112 | const response = this.tdlib.td_json_client_execute(this.client, buildQuery(query))
113 | if (!response) {
114 | return resolve(null)
115 | }
116 | resolve(JSON.parse(response))
117 | } catch (err) {
118 | reject(err)
119 | }
120 | })
121 | }
122 |
123 | _destroy() {
124 | return new Promise((resolve) => {
125 | if (this.client) {
126 | this.tdlib.td_json_client_destroy(this.client)
127 | this.client = null
128 | }
129 | resolve()
130 | })
131 | }
132 | }
133 |
134 | export default ClientNode
135 |
--------------------------------------------------------------------------------
/src/ClientBase.js:
--------------------------------------------------------------------------------
1 | import { crc32 } from 'crc'
2 | import TG from './TG'
3 | import { version as appVersion } from '../package.json'
4 | import {
5 | InvalidCallbackError,
6 | InvalidBotTokenError,
7 | ClientFetchError,
8 | } from './errors/index.js'
9 |
10 | class Client {
11 | constructor(mode, options = {}) {
12 | const defaultOptions = {
13 | apiId: null,
14 | apiHash: null,
15 | verbosityLevel: 2,
16 | tdlibParameters: {
17 | 'enable_storage_optimizer': true,
18 | 'use_message_database': true,
19 | 'use_secret_chats': true,
20 | 'system_language_code': 'en',
21 | 'application_version': '1.0',
22 | 'device_model': 'tglib',
23 | 'system_version': appVersion,
24 | },
25 | }
26 | this.options = {
27 | ...defaultOptions,
28 | ...options,
29 | }
30 | this.ready = new Promise((resolve, reject) => {
31 | // Add some delay to allow telegram get ready. (Issue #20)
32 | this.resolver = () => setTimeout(resolve, 500)
33 | this.rejector = reject
34 | })
35 | this.client = null
36 | this.tg = new TG(this)
37 | this.hijackers = {}
38 | this.downloading = {}
39 | this.fetching = {}
40 | this.callbacks = {
41 | 'td:update': () => {},
42 | 'td:error': () => {},
43 | 'td:getInput': () => { throw new Error('td:getInput callback is not set.') },
44 | }
45 | this.init()
46 | }
47 |
48 | registerCallback(key, callback) {
49 | const validNames = Object.keys(this.callbacks)
50 | if (validNames.indexOf(key) < 0) {
51 | throw new InvalidCallbackError(key)
52 | }
53 | this.callbacks[key] = callback
54 | }
55 |
56 | async loop() {
57 | if (!this.client) {
58 | // when client has been destroyed, stop the update loop
59 | return
60 | }
61 | const update = await this._receive()
62 | if (update) {
63 | if (this.hijackers[update['@type']]) {
64 | // for tglib update hijacking
65 | this.hijackers[update['@type']](update)
66 | } else {
67 | // handle update normally
68 | switch (update['@type']) {
69 | case 'updateAuthorizationState': {
70 | await this.handleAuth(update)
71 | break
72 | }
73 | case 'error': {
74 | await this.handleError(update)
75 | break
76 | }
77 | default:
78 | await this.handleUpdate(update)
79 | break
80 | }
81 | }
82 | }
83 | setTimeout(() => this.loop(), 1)
84 | }
85 |
86 | _hijackUpdate(type, callback) {
87 | if (typeof callback === 'function') {
88 | this.hijackers[type] = callback
89 | } else {
90 | delete this.hijackers[type]
91 | }
92 | }
93 |
94 | async handleAuth(update) {
95 | switch (update['authorization_state']['@type']) {
96 | case 'authorizationStateWaitTdlibParameters': {
97 | this._send({
98 | '@type': 'setTdlibParameters',
99 | 'parameters': {
100 | ...this.options.tdlibParameters,
101 | '@type': 'tdlibParameters',
102 | 'api_id': this.options.apiId,
103 | 'api_hash': this.options.apiHash,
104 | 'database_directory': this.options.databaseDir,
105 | 'files_directory': this.options.filesDir,
106 | },
107 | })
108 | break
109 | }
110 | case 'authorizationStateWaitEncryptionKey': {
111 | this._send({ '@type': 'checkDatabaseEncryptionKey' })
112 | break
113 | }
114 | case 'authorizationStateWaitPhoneNumber': {
115 | const type = await this.callbacks['td:getInput']({
116 | string: 'tglib.input.AuthorizationType',
117 | })
118 | const value = await this.callbacks['td:getInput']({
119 | string: 'tglib.input.AuthorizationValue',
120 | })
121 | if (type === 'user') {
122 | this._send({
123 | '@type': 'setAuthenticationPhoneNumber',
124 | 'phone_number': value,
125 | })
126 | } else {
127 | this._send({
128 | '@type': 'checkAuthenticationBotToken',
129 | 'token': value,
130 | })
131 | }
132 | break
133 | }
134 | case 'authorizationStateWaitCode': {
135 | const payload = { '@type': 'checkAuthenticationCode' }
136 | payload['code'] = await this.callbacks['td:getInput']({
137 | string: 'tglib.input.AuthorizationCode',
138 | })
139 | this._send(payload)
140 | break
141 | }
142 | case 'authorizationStateWaitRegistration': {
143 | const payload = { '@type': 'registerUser' };
144 | console.log(`User has not yet been registered with Telegram`);
145 | payload['first_name'] = await this.callbacks['td:getInput']({
146 | string: 'tglib.input.FirstName',
147 | });
148 | this._send(payload);
149 | break
150 | }
151 | case 'authorizationStateWaitPassword': {
152 | this.authFlowPasswordHint = update['authorization_state']['password_hint']
153 | const password = await this.callbacks['td:getInput']({
154 | string: 'tglib.input.AuthorizationPassword',
155 | extras: { hint: this.authFlowPasswordHint },
156 | })
157 | this._send({
158 | '@type': 'checkAuthenticationPassword',
159 | 'password': password,
160 | })
161 | break
162 | }
163 | case 'authorizationStateReady':
164 | delete this.authFlowPasswordHint
165 | this.resolver()
166 | break
167 | }
168 | }
169 |
170 | async handleError(update) {
171 | const id = update['@extra']
172 | if (this.fetching[id]) {
173 | delete update['@extra']
174 | this.fetching[id].reject(new ClientFetchError(update))
175 | delete this.fetching[id]
176 | return
177 | }
178 | switch (update['message']) {
179 | case 'PHONE_CODE_EMPTY':
180 | case 'PHONE_CODE_INVALID': {
181 | const code = await this.callbacks['td:getInput']({
182 | string: 'tglib.input.AuthorizationCodeIncorrect',
183 | })
184 | this._send({
185 | '@type': 'checkAuthenticationCode',
186 | 'code': code,
187 | })
188 | break
189 | }
190 | case 'PASSWORD_HASH_INVALID': {
191 | const password = await this.callbacks['td:getInput']({
192 | string: 'tglib.input.AuthorizationPasswordIncorrect',
193 | extras: { hint: this.authFlowPasswordHint },
194 | })
195 | this._send({
196 | '@type': 'checkAuthenticationPassword',
197 | 'password': password,
198 | })
199 | break
200 | }
201 | case 'ACCESS_TOKEN_INVALID': {
202 | this.rejector(new InvalidBotTokenError())
203 | break
204 | }
205 | default: {
206 | this.callbacks['td:error'].call(null, update)
207 | }
208 | }
209 | }
210 |
211 | async handleUpdate(update) {
212 | const id = update['@extra']
213 | if (this.fetching[id]) {
214 | delete update['@extra']
215 | this.fetching[id].resolve(update)
216 | delete this.fetching[id]
217 | return
218 | }
219 | switch (update['@type']) {
220 | case 'updateFile': {
221 | const fileId = update['file']['id']
222 | if (!this.downloading[fileId]) {
223 | // default handle
224 | this.callbacks['td:update'].call(null, update)
225 | return
226 | }
227 | if (!update['file']['local']['path'].length) {
228 | // not yet downloaded
229 | return
230 | }
231 | // resolve downloaded
232 | this.downloading[fileId](update['file'])
233 | }
234 | case 'updateOption': {
235 | if (update['name'] === 'my_id' && update['value']['@type'] === 'optionValueEmpty') {
236 | // session has been signed out
237 | await this._destroy()
238 | break
239 | }
240 | }
241 | default: {
242 | this.callbacks['td:update'].call(null, update)
243 | }
244 | }
245 | }
246 |
247 | async fetch(query) {
248 | const id = crc32(Math.random().toString()).toString()
249 | query['@extra'] = id
250 | const receiveUpdate = new Promise((resolve, reject) => {
251 | this.fetching[id] = { resolve, reject }
252 | // timeout after 15 seconds
253 | setTimeout(() => {
254 | delete this.fetching[id]
255 | reject('Query timed out after 30 seconds.')
256 | }, 1000 * 30)
257 | })
258 | await this._send(query)
259 | return receiveUpdate
260 | }
261 | }
262 |
263 | export default Client
264 |
--------------------------------------------------------------------------------
/src/TG.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'event-emitter'
2 |
3 | // combine the user-supplied options with the referencing base options
4 | function combine(object, options, whitelistedKeys) {
5 | Object.keys(object).forEach((key) => {
6 | if (object[key] instanceof Object) {
7 | combine(object[key], options, whitelistedKeys)
8 | } else if (options[key] !== undefined && whitelistedKeys.indexOf(key) >= 0) {
9 | object[key] = options[key]
10 | }
11 | })
12 | }
13 |
14 | class TG {
15 | constructor(client) {
16 | this.client = client
17 | }
18 |
19 | /*
20 | * Send text message to an existing chat.
21 | * Method "parseTextEntities" requires TDLib 1.1.0 (git.io/tdlibchanges).
22 | */
23 | async sendTextMessage(args = {}) {
24 | const { $text, ...options } = args
25 | if (!$text) {
26 | throw new Error('No text defined for method "sendTextMessage".')
27 | }
28 | const payload = {
29 | '@type': 'sendMessage',
30 | 'chat_id': 0,
31 | 'reply_to_message_id': 0,
32 | 'disable_notification': false,
33 | 'from_background': true,
34 | 'reply_markup': null,
35 | 'input_message_content': {
36 | '@type': 'inputMessageText',
37 | 'text': await $text._format(this.client),
38 | 'disable_web_page_preview': true,
39 | 'clear_draft': true,
40 | },
41 | }
42 | combine(payload, options, [
43 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background',
44 | 'disable_web_page_preview', 'clear_draft',
45 | ])
46 | return this.client.fetch(payload)
47 | }
48 |
49 | /*
50 | * Send photo message to an existing chat.
51 | */
52 | async sendPhotoMessage(args = {}) {
53 | const { $caption, ...options } = args
54 | const payload = {
55 | '@type': 'sendMessage',
56 | 'chat_id': 0,
57 | 'reply_to_message_id': 0,
58 | 'disable_notification': false,
59 | 'from_background': true,
60 | 'reply_markup': null,
61 | 'input_message_content': {
62 | '@type': 'inputMessagePhoto',
63 | 'photo': {
64 | '@type': 'inputFileLocal',
65 | 'path': null,
66 | },
67 | 'thumbnail': null,
68 | 'added_sticker_file_ids': [],
69 | 'width': 0,
70 | 'height': 0,
71 | 'caption': $caption ? await $caption._format(this.client) : null,
72 | 'ttl': 0,
73 | },
74 | }
75 | combine(payload, options, [
76 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background',
77 | 'path', 'thumbnail', 'added_sticker_file_ids', 'width', 'height', 'ttl',
78 | ])
79 | return this.client.fetch(payload)
80 | }
81 |
82 | /*
83 | * Send sticker message to an existing chat.
84 | */
85 | async sendStickerMessage(args = {}) {
86 | const { $caption, ...options } = args
87 | if (!options.path || !options.path.endsWith('webp')) {
88 | throw 'WebP image must be passed for [sendStickerMessage] method'
89 | }
90 | const payload = {
91 | '@type': 'sendMessage',
92 | 'chat_id': 0,
93 | 'reply_to_message_id': 0,
94 | 'disable_notification': false,
95 | 'from_background': true,
96 | 'reply_markup': null,
97 | 'input_message_content': {
98 | '@type': 'inputMessageSticker',
99 | 'sticker': {
100 | '@type': 'inputFileLocal',
101 | 'path': null,
102 | },
103 | 'thumbnail': null,
104 | 'width': 0,
105 | 'height': 0,
106 | },
107 | }
108 | combine(payload, options, [
109 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background',
110 | 'path', 'thumbnail', 'width', 'height',
111 | ])
112 | return this.client.fetch(payload)
113 | }
114 |
115 | /*
116 | * Get all chats.
117 | */
118 | async getAllChats() {
119 | let { chat_ids: chats } = await this.client.fetch({
120 | '@type': 'getChats',
121 | 'offset_order': '9223372036854775807',
122 | 'offset_chat_id': 0,
123 | 'limit': Math.floor(Math.random() * 9999999),
124 | })
125 | chats = chats.map((chatId) => this.client.fetch({
126 | '@type': 'getChat',
127 | 'chat_id': chatId,
128 | }))
129 | chats = await Promise.all(chats)
130 | return chats
131 | }
132 |
133 | /*
134 | * Update user's username.
135 | * Method "checkChatUsername" requires TDLib 1.2.0 (git.io/tdlibchanges).
136 | */
137 | updateUsername(username, supergroupId = null) {
138 | return new Promise(async (resolve, reject) => {
139 | let payload
140 | // Check the username
141 | payload = { '@type': 'checkChatUsername', username }
142 | if (supergroupId) {
143 | const { id } = await this.client.fetch({ '@type': 'createSupergroupChat', 'supergroup_id': supergroupId })
144 | payload['chat_id'] = id
145 | } else {
146 | const { id } = await this.client.fetch({ '@type': 'getMe' })
147 | payload['chat_id'] = id
148 | }
149 | const { '@type': checkResult } = await this.client.fetch(payload)
150 | if (checkResult !== 'checkChatUsernameResultOk') {
151 | return reject(checkResult)
152 | }
153 | // Update the username
154 | payload = { '@type': 'setUsername', username }
155 | if (supergroupId) {
156 | payload['@type'] = 'setSupergroupUsername'
157 | payload['supergroup_id'] = supergroupId
158 | }
159 | const result = await this.client.fetch(payload)
160 | resolve(result)
161 | })
162 | }
163 |
164 | /*
165 | * Opens a secret chat with given user ID.
166 | */
167 | async openSecretChat(userId) {
168 | let secretChat = await this.getAllChats()
169 | secretChat = secretChat.find(({ type: { '@type': type, user_id: user } }) => {
170 | return type === 'chatTypeSecret' && user === userId
171 | })
172 | // created already
173 | if (secretChat) {
174 | return secretChat
175 | }
176 | // create new secret chat
177 | secretChat = await this.client.fetch({
178 | '@type': 'createNewSecretChat',
179 | 'user_id': userId,
180 | })
181 | return secretChat
182 | }
183 |
184 | /*
185 | * Close and remove a chat.
186 | */
187 | async deleteChat(chatId) {
188 | const chat = await this.client.fetch({
189 | '@type': 'getChat',
190 | 'chat_id': chatId,
191 | })
192 |
193 | let payload = {}
194 | switch (chat.type['@type']) {
195 | case 'chatTypeBasicGroup':
196 | case 'chatTypeSupergroup': {
197 | const { id } = await this.client.fetch({ '@type': 'getMe' })
198 | payload['@type'] = 'setChatMemberStatus'
199 | payload['user_id'] = id
200 | payload['chat_id'] = chat.id
201 | payload['status'] = { '@type': 'chatMemberStatusLeft' }
202 | break
203 | }
204 | case 'chatTypeSecret': {
205 | payload['@type'] = 'closeSecretChat'
206 | payload['secret_chat_id'] = chat.type.secret_chat_id
207 | break
208 | }
209 | default: {
210 | payload['@type'] = 'closeChat'
211 | payload['chat_id'] = chat.id
212 | break
213 | }
214 | }
215 | await this.client.fetch(payload)
216 | await this.client.fetch({
217 | '@type': 'deleteChatHistory',
218 | 'chat_id': chat.id,
219 | 'remove_from_chat_list': true,
220 | })
221 | }
222 |
223 | /*
224 | * Get chat by username or id
225 | */
226 | async getChat(args = {}) {
227 | const { username, chat_id } = args
228 | let chat = {}
229 | if (username) {
230 | chat = await this.client.fetch({
231 | '@type': 'searchPublicChat',
232 | username,
233 | })
234 | } else if (chat_id) {
235 | chat = await this.client.fetch({
236 | '@type': 'getChat',
237 | chat_id,
238 | })
239 | } else {
240 | throw new Error('Neither username nor chat_id were specified for method "getChat"')
241 | }
242 | return chat
243 | }
244 |
245 | /*
246 | * Download a file
247 | */
248 | async download(remoteFileId) {
249 | const { id } = await this.client.fetch({
250 | '@type': 'getRemoteFile',
251 | 'remote_file_id': remoteFileId,
252 | })
253 | let file = await this.client.fetch({
254 | '@type': 'downloadFile',
255 | 'file_id': id,
256 | 'priority': 1,
257 | })
258 | if (!file['local']['path'].length) {
259 | const downloadPromise = new Promise((resolve) => {
260 | this.client.downloading[id] = resolve
261 | })
262 | file = await downloadPromise
263 | }
264 | return file
265 | }
266 |
267 |
268 | /*
269 | * Call an user
270 | */
271 | call(userId) {
272 | return new Promise(async (resolve, reject) => {
273 | try {
274 | const { id } = await this.client.fetch({
275 | '@type': 'createCall',
276 | 'user_id': userId,
277 | 'protocol': {
278 | '@type': 'callProtocol',
279 | 'udp_p2p': true,
280 | 'udp_reflector': true,
281 | 'min_layer': 65,
282 | 'max_layer': 65,
283 | },
284 | })
285 | const emitter = EventEmitter()
286 | this.client._hijackUpdate('updateCall', (update) => {
287 | // If call failed due to USER_PRIVACY_RESTRICTED
288 | if (update.call.state['@type'] === 'callStateError') {
289 | this.client._hijackUpdate('updateCall', false)
290 | return reject(update.call.state.error)
291 | }
292 | if (update.call.state['@type'] === 'callStateReady') {
293 | emitter.emit('ready', update.call)
294 | }
295 | if (update.call.state['@type'] === 'callStateDiscarded') {
296 | emitter.emit('discarded', update.call)
297 | this.client._hijackUpdate('updateCall', false)
298 | }
299 | return resolve(emitter)
300 | })
301 | } catch (err) {
302 | reject(err)
303 | }
304 | })
305 | }
306 | }
307 |
308 | export default TG
309 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## tglib
2 |
3 | TDLib (Telegram Database library) bindings for Node.js
4 |
5 | [](https://www.npmjs.com/package/tglib)
6 |
7 | -----
8 |
9 | ### Getting started
10 |
11 | 1. Build the binary (https://github.com/tdlib/td#building)
12 | 2. `npm i -S tglib`
13 |
14 | -----
15 |
16 | ### Node / WASM support
17 |
18 | tglib support both Node and WASM environments:
19 |
20 | ```js
21 | // CommonJS, loads Node version tglib
22 | const { Client } = require('tglib')
23 |
24 | // ESModule, loads WASM version tglib
25 | import { Client } from 'tglib'
26 | ```
27 |
28 | By default, `package.json` will automatically handle it for you.
29 |
30 | If use CommonJS (`require()` function, usally Node.js), it loads `require('tglib/node')`
31 |
32 | If use ESModule (`import` syntax, usally bundlers), it loads `import ... from 'tglib/wasm'`
33 |
34 | In case something misimported, you can manually set to the correct version you need:
35 |
36 | ```js
37 | import { Client } from 'tglib/node'
38 | ```
39 |
40 | -----
41 |
42 | ### Options
43 |
44 | ```js
45 | new Client({
46 | apiId: '', // specify your API ID
47 | apiHash: '', // specify your API Hash
48 | verbosityLevel: 2, // specify TDLib verbosity level to control logging, default 2
49 | tdlibParameters: {}, // specify custom tdlibParameters object
50 |
51 | // Node only options
52 | appDir: '', // specify where to place tglib files, default "__tglib__" folder
53 | binaryPath: '', // specify the TDLib static binary path, default "libtdjson" in cwd
54 |
55 | // WASM only options
56 | wasmModule: null, // specify the WebAssembly module
57 | filesDir: '', // specify the files directory path
58 | databaseDir: '', // specify the database directory path
59 | })
60 | ```
61 |
62 | -----
63 |
64 | #### Callbacks / Authorization
65 |
66 | You can register callbacks for the following events:
67 |
68 | - `td:update`, default empty function
69 | - `td:error`, default empty function
70 | - `td:getInput`, default console prompt in Node, exception in WASM
71 |
72 | ```js
73 | // register callback for updates
74 | client.registerCallback('td:update', (update) => {
75 | console.log('[update]', update)
76 | })
77 |
78 | // register callback for errors
79 | client.registerCallback('td:error', (error) => {
80 | console.log('[error]', error)
81 | })
82 | ```
83 |
84 | The `td:getInput` event can be configured to ask for user input.
85 |
86 | It also be used for the authorization flow too.
87 |
88 | You MUST register an `async` function or a function that returns a `Promise`.
89 |
90 | To authorize a bot:
91 |
92 | ```js
93 | // Save tglib default handler which prompt input at console
94 | const defaultHandler = client.callbacks['td:getInput']
95 |
96 | // Register own callback for returning auth details
97 | client.registerCallback('td:getInput', async (args) => {
98 | if (args.string === 'tglib.input.AuthorizationType') {
99 | return 'bot'
100 | } else if (args.string === 'tglib.input.AuthorizationValue') {
101 | return 'YOUR_BOT_TOKEN'
102 | }
103 | return await defaultHandler(args)
104 | })
105 | ```
106 |
107 | To authorize an user:
108 |
109 | ```js
110 | // Save tglib default handler which prompt input at console
111 | const defaultHandler = client.callbacks['td:getInput']
112 |
113 | // Register own callback for returning auth details
114 | client.registerCallback('td:getInput', async (args) => {
115 | if (args.string === 'tglib.input.AuthorizationType') {
116 | return 'user'
117 | } else if (args.string === 'tglib.input.AuthorizationValue') {
118 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER'
119 | }
120 | return await defaultHandler(args)
121 | })
122 | ```
123 |
124 | The `string` property in `td:getInput` argument object is used to determine what the input is:
125 |
126 | - `tglib.input.AuthorizationType`, authorization type: `user` or `bot`
127 | - `tglib.input.AuthorizationValue`, authorization value: bot token or phone number
128 | - `tglib.input.FirstName`, first name for new account creation
129 | - `tglib.input.AuthorizationCode`, authorization code received in Telegram or SMS
130 | - `tglib.input.AuthorizationCodeIncorrect`, authorization code re-input if wrong
131 | - `tglib.input.AuthorizationPassword`, authorization password (two-step verification)
132 | - `tglib.input.AuthorizationPasswordIncorrect`, authorization password re-input if wrong
133 |
134 | These string can be used as identifier for i18n purpose.
135 |
136 | An `extras` property may present in `td:getInput` argument object as well in some events.
137 |
138 | Currently, a `extras` property will come up with `hint` property in the following events:
139 |
140 | - `tglib.input.AuthorizationPassword`
141 | - `tglib.input.AuthorizationPasswordIncorrect`
142 |
143 | The `hint` property here indicates user cloud password hint.
144 |
145 | ```js
146 | client.registerCallback('td:getInput', async ({ string, extras: { hint } = {} }) => {
147 | const result = window.prompt(`${string}${hint ? ` ${hint}` : ''}`)
148 | return result
149 | })
150 | ```
151 |
152 | -----
153 |
154 | ### Connect with Telegram
155 |
156 | tglib provide a `ready` Promise in client instances.
157 |
158 | This Promise will resolve automatically when the authentication flow finishes.
159 |
160 | In order words, when user successfully logged into their account, the Promise will be resolved.
161 |
162 | ```js
163 | const client = new Client(...)
164 |
165 | await client.ready
166 |
167 | // You are now connected!
168 | await client.tg.sendTextMessage(...)
169 | ```
170 |
171 | -----
172 |
173 | ### APIs
174 |
175 | tglib provide some useful methods that makes your Telegram app development easier.
176 |
177 | Most API classes/methods can be found in the official [TDLib documentation](https://core.telegram.org/tdlib/docs/classes.html).
178 |
179 | ####  Low Level APIs
180 |
181 |
182 | ##### `client._send(query)` -> Promise -> Object
183 |
184 |
185 |
186 | Expand
187 |
188 |
189 | This API is provided by TDLib, you can use this API to send asynchronous message to Telegram.
190 |
191 | ```js
192 | await client._send({
193 | '@type': 'sendMessage',
194 | 'chat_id': -123456789,
195 | 'input_message_content': {
196 | '@type': 'inputMessageText',
197 | 'text': {
198 | '@type': 'formattedText',
199 | 'text': '👻',
200 | },
201 | },
202 | })
203 | ```
204 |
205 |
206 |
207 |
208 | ##### `client._execute(query)` -> Promise -> Object
209 |
210 |
211 |
212 | Expand
213 |
214 |
215 | This API is provided by TDLib, you can use this API to execute synchronous action to Telegram.
216 |
217 | ```js
218 | await client._execute({
219 | '@type': 'getTextEntities',
220 | 'text': '@telegram /test_command https://telegram.org telegram.me',
221 | })
222 | ```
223 |
224 |
225 |
226 |
227 | ##### `client._destroy()` -> Promise -> Void
228 |
229 |
230 |
231 | Expand
232 |
233 |
234 | This API is provided by TDLib, you can use this API to destroy the client.
235 |
236 | ```js
237 | await client._destroy()
238 | ```
239 |
240 |
241 |
242 |
243 | ##### `client.fetch(query)` -> Promise -> Object
244 |
245 |
246 |
247 | Expand
248 |
249 |
250 | This API is provided by tglib, you can use this API to send asynchronous message to Telegram and receive response.
251 |
252 | ```js
253 | const chats = await client.fetch({
254 | '@type': 'getChats',
255 | 'offset_order': '9223372036854775807',
256 | 'offset_chat_id': 0,
257 | 'limit': 100,
258 | })
259 | ```
260 |
261 |
262 |
263 |
264 | ####  High Level APIs
265 |
266 |
267 | tglib provides a collection of APIs that designed for ease of use and handiness. These APIs are located under `client.tg` property.
268 |
269 |
270 | ##### `client.tg.sendTextMessage(args = {})` -> Promise -> Void
271 |
272 |
273 |
274 | Expand
275 |
276 |
277 | This API is provided by tglib, you can use this API to send message to a chat. The function will combine custom options specified in `args` with its default.
278 |
279 | The `TextStruct` struct uses "parseTextEntities" method which requires TDLib 1.1.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details.
280 |
281 | ```js
282 | const { TextStruct } = require('tglib/structs')
283 |
284 | await client.tg.sendTextMessage({
285 | '$text': new TextStruct('`Hello` world!', 'textParseModeMarkdown'),
286 | 'chat_id': 123456789,
287 | 'disable_notification': true,
288 | 'clear_draft': false,
289 | })
290 | ```
291 |
292 |
293 |
294 |
295 | ##### `client.tg.sendPhotoMessage(args = {})` -> Promise -> Void
296 |
297 |
298 |
299 | Expand
300 |
301 |
302 | This API is provided by tglib, you can use this API to send photo to a chat. The function will combine custom options specified in `args` with its default.
303 |
304 | The `TextStruct` struct uses "parseTextEntities" method which requires TDLib 1.1.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details.
305 |
306 | ```js
307 | const { TextStruct } = require('tglib/structs')
308 |
309 | await client.tg.sendPhotoMessage({
310 | '$caption': new TextStruct('Such doge much wow'),
311 | 'chat_id': 123456789,
312 | 'path': '/tmp/doge.jpg',
313 | 'ttl': 5,
314 | })
315 | ```
316 |
317 |
318 |
319 |
320 | ##### `client.tg.sendStickerMessage(args = {})` -> Promise -> Void
321 |
322 |
323 |
324 | Expand
325 |
326 |
327 | This API is provided by tglib, you can use this API to send sticker to a chat. The function will combine custom options specified in `args` with its default.
328 |
329 | ```js
330 | await client.tg.sendStickerMessage({
331 | 'chat_id': 123456789,
332 | 'path': '/tmp/doge.webp',
333 | })
334 | ```
335 |
336 |
337 |
338 |
339 | ##### `client.tg.updateUsername(username, supergroupId = null)` -> Promise -> Void
340 |
341 |
342 |
343 | Expand
344 |
345 |
346 | This API is provided by tglib, you can use this API to update the username for session user or a supergroup chat.
347 |
348 | This API uses "checkChatUsername" method which requires TDLib 1.2.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details.
349 |
350 | ```js
351 | await client.tg.updateUsername('a_new_username')
352 | ```
353 |
354 |
355 |
356 |
357 | ##### `client.tg.getAllChats()` -> Promise -> Array
358 |
359 |
360 |
361 | Expand
362 |
363 |
364 | This API is provided by tglib, you can use this API to get all available chats of session user.
365 |
366 | ```js
367 | const chats = await client.tg.getAllChats()
368 | ```
369 |
370 |
371 |
372 |
373 | ##### `client.tg.openSecretChat(userId)` -> Promise -> Object
374 |
375 |
376 |
377 | Expand
378 |
379 |
380 | This API is provided by tglib, you can use this API to open a secret chat with given user ID.
381 |
382 | Note: Secret chats are associated with the corresponding TDLib folder. (i.e. only available on the same device).
383 |
384 | ```js
385 | const chat = await client.tg.openSecretChat(123456789)
386 | ```
387 |
388 |
389 |
390 |
391 | ##### `client.tg.deleteChat(chatId)` -> Promise -> Void
392 |
393 |
394 |
395 | Expand
396 |
397 |
398 | This API is provided by tglib, you can use this API to delete a chat and remove it from the chat list. You can use this API to delete "private", "secret", "basicGroup", and "supergroup" chats.
399 |
400 | ```js
401 | await client.tg.deleteChat(-12345678901234)
402 | ```
403 |
404 |
405 |
406 |
407 | ##### `client.tg.getChat(args = {})` -> Promise -> Object
408 |
409 |
410 |
411 | Expand
412 |
413 |
414 | This API is provided by tglib, you can use this API to get a chat by username or chat id. This method requires either `username` option, or `chat_id` option.
415 |
416 | ```js
417 | const chat1 = await client.tg.getChat({ username: 'chat_username' })
418 | const chat2 = await client.tg.getChat({ chat_id: '-12345678901234' })
419 | ```
420 |
421 |
422 |
423 |
424 | ##### `client.tg.call(userId)` -> Promise -> EventEmitter
425 |
426 |
427 |
428 | Expand
429 |
430 |
431 | This API is provided by tglib, you can use this API to call an user.
432 |
433 | The promise will resolve with an EventEmitter when call succceeded.
434 |
435 | The EventEmitter will emit `ready` and `discarded` events.
436 |
437 | ```js
438 | const emitter = await client.tg.call(4000000001)
439 | emitter.on('ready', (call) => console.log(call))
440 | emitter.on('discarded', (call) => console.log(call))
441 | ```
442 |
443 |
444 |
445 | -----
446 |
447 | ### Requirements
448 |
449 | - Node.js 10 preferred (minimum >= 9.0.0)
450 | > Note: If you are using Node.js 9.x, you may encounter a warning message `Warning: N-API is an experimental feature and could change at any time.`, this can be suppressed by upgrading to version 10.
451 |
452 | - TDLib binary (see build instructions below)
453 |
454 | > [Windows](https://github.com/c0re100/F9TelegramUtils#compile-tdlib-on-windows)
455 |
456 | > [macOS](https://github.com/tdlib/td#macos)
457 |
458 | > [Linux - CentOS 7.5](https://gist.github.com/nodegin/e3849aa1e5170c2e05942ffe86e4f8c9)
459 |
460 | Note: building TDLib binary requires at least 8GB of memory (or more...), otherwise building process will fail. Build in containers are recommended.
461 |
462 | -----
463 |
464 | ### License
465 |
466 | tglib uses the same license as TDLib. See [tdlib/td](https://github.com/tdlib/td) for more information.
467 |
--------------------------------------------------------------------------------