0
const { TelegramClient } = require('telegram')
const { StringSession } = require('telegram/sessions')
const input = require('input') // npm i input

const apiId = 123456
const apiHash = '123456abcdfg'
const stringSession = new StringSession(''); // fill this later with the value from session.save()
(async () => {
    console.log('Loading interactive example...')
    const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: 5 })
    await client.start({
        phoneNumber: async () => await input.text('number ?'),
        password: async () => await input.text('password?'),
        phoneCode: async () => await input.text('Code ?'),   //<<<<<<This line
        onError: (err) => console.log(err),
    });
    console.log('You should now be connected.')
    console.log(client.session.save()) // Save this string to avoid logging in again
    await client.sendMessage('me', { message: 'Hello!' });
})()

This is the example of authorization with gramjs (https://painor.gitbook.io/gramjs/getting-started/authorization)

I'm trying to setup a nodejs-express environment to render pages that can help the user to setup credentials by submitting through a webpage and save/process it back-end.

Since the above code will wait for user input when calling client.start(), is there any way to break it down to 2 http request that posts the phoneNumber and password, then another http post to post the phoneCode from telegram for authentication?

I tried some Promise but it won't work.. The idea is to have 2 steps, 1st one is to call /setup and post the initial credentials, then hope the client will call the telegram api to send a phone code to myself. After that I can call /setup API once more and submit the code as well.

The behavior of the following code is that, it can call and receive telegram's phone code. But the second call to POST /setup API wouldn't work and got stuck. So I can't resolve the phone code for the client.

Is there any way that I could ignore the first Promise in the first http post, return a response, then call the API second time or a different route/url and send the data as the first Promise's resolve?

let code = null;
async function waitForPhoneCode() {
    return new Promise(function(resolve, reject) {
        while(code==null){
            //wait for code to be set on next http post
        }
        resolve(code);
    });
}

app.post('/setup', async (req, res) => {
    const setupStep = req.body.setupStep;
    const apiId = req.body.apiId;
    const apiHash = req.body.apiHash;
    const phoneNumber = req.body.phoneNumber;
    const password = req.body.password;
    const obtainedCode = req.body.code;

    credentials = {
        "apiId": apiId,
        "apiHash": apiHash
    }

    if(setupStep == 1){
        client = new TelegramClient(new StringSession(""), credentials.apiId, credentials.apiHash, {
            connectionRetries: 5,
        });
        res.json({
            "status": "ok",
        })
        client.start({
            phoneNumber: phoneNumber,
            password: password,
            phoneCode: waitForPhoneCode(),
            onError: (err) => console.log(err),
        });
    } else if(setupStep == 2){
        code = obtainedCode;
        console.log("You should now be connected.");
        credentials = {
            apiId: apiId,
            apiHash: apiHash,
            stringSession: new sessionStorage(client.session.save())
        }
        fs.writeFileSync(credentialsPath, JSON.stringify({
            apiId: apiId,
            apiHash: apiHash,
            stringSession: client.session.save(),
        }));
        await client.sendMessage("me", { message: "Setup Completed!" });
        res.redirect('/');
    }
})

2 Answers2

2

Here's the full working example that has @Coxxs's promise implemented. Sleep a few seconds worked too. I tried with some more promise for the client.start callbacks and here's the working result. I know I could chain the then() method but I think this looked more logically appealing.

Thank you so much for your help! I really appreciate that!

let globalPhoneCodePromise;
let clientStartPromise;
function generatePromise() {
    let resolve, reject;
    let promise = new Promise((_resolve, _reject) => {
        resolve = _resolve;
        reject = _reject;
    })
    return { resolve, reject, promise };
}

app.post('/setup', async (req, res) => {
    const setupStep = req.body.setupStep;
    const apiIdStr = req.body.apiId;
    const apiHash = req.body.apiHash;
    const phoneNumber = req.body.phoneNumber;
    const password = req.body.password;
    const obtainedCode = req.body.code;

    if(Number.isNaN(Number(apiIdStr))){
        res.json({
            "status": "error",
            "message": "apiId is not a number"
        })
    }
    let apiId = Number(apiIdStr) // Convert apiId to number

    credentials = {
        "apiId": apiId,
        "apiHash": apiHash
    }

    if(setupStep == 1){
        client = new TelegramClient(new StringSession(""), credentials.apiId, credentials.apiHash, {
            connectionRetries: 5,
        });
        
        globalPhoneCodePromise = generatePromise()
        clientStartPromise = client.start({
            phoneNumber: phoneNumber,
            password: async () => {return password},
            phoneCode: async () => {
                let code = await globalPhoneCodePromise.promise;
                globalPhoneCodePromise = generatePromise();
                return code;
            },
            onError: (err) => {
                console.log(err)
                res.json({
                    "status": "error",
                    "setupStep": 9,
                    "message": err
                })
            },
        });
        
        res.json({
            "status": "ok",
            "setupStep": 1
        })
    } else if(setupStep == 2){
        globalPhoneCodePromise.resolve(obtainedCode);

        clientStartPromise.then(async () => {
            console.log(client.session.save());
            console.log("You should now be connected.");
            credentials = {
                "apiId": apiId,
                "apiHash": apiHash,
                "stringSession": client.session.save()
            }
            fs.writeFileSync(credentialsPath, JSON.stringify(credentials));
            await client.sendMessage("me", { message: "Setup Completed!" });

            res.json({
                "status": "ok",
                "setupStep": 2
            })

        }).catch(err => {
            res.json({
                "status": "error",
                "setupStep": 2,
                "message": err
            })
        })
    }
})

Note: If you had tried a lot of times authenticating (testing with a wrong code?), you might get this error and have to wait for a day before testing the code again

FloodWaitError: A wait of 78660 seconds is required (caused by auth.SendCode)

{
  code: 420,
  errorMessage: 'FLOOD',
  seconds: 78660
}
1

We can save the resolve provided by the Promise to a global variable, and resolve it later.

Here's the fixed code:

let globalPhoneCodePromise

function generatePromise() {
    let resolve
    let reject
    let promise = new Promise((_resolve, _reject) => {
        resolve = _resolve
        reject = _reject
    })

    return { resolve, reject, promise }
}

// step 1
globalPhoneCodePromise = generatePromise()
await client.start({
    // ...
    phoneCode: async () => {
        let code = await globalPhoneCodePromise.promise

        // In case the user provided a wrong code, gram.js will try to call this function again
        // We generate a new promise here to allow user enter a new code later
        globalPhoneCodePromise = generatePromise()

        return code
    },
    // ...
})

// step 2
globalPhoneCodePromise.resolve('12345')
Coxxs
  • 460
  • 4
  • 9
  • Tried the solution, almost worked but there's still some unsolved strange issues.. After doing the resolve on the next http post, and trying to send a message, this happened: RPCError: 401: AUTH_KEY_UNREGISTERED (caused by messages.SendMessage) Seems like the client.session.save() is doing it's job and getting the StringSession but the value is incorrect. There is some session string output but I need to replace it to the correct StringSession in order to work. – Super Elephant Aug 21 '22 at 12:20
  • @SuperElephant This might because after we called `.resolve`, we immediately called `sendMessage`. At this time, gram.js is still submitting the verification code to the server, and the login process has not been completed. – Coxxs Aug 21 '22 at 21:12
  • We can just [sleep](https://stackoverflow.com/a/49139664/3054394) a few seconds after we called `.resolve`. We can also store the promise that `client.start` returns, and await it after we called `.resolve`. – Coxxs Aug 21 '22 at 21:14