Link Search Menu Expand Document

Handling state changes (Logging-In)

CatraProto sends updates regarding the current session to the OnSessionUpdateAsync method of the IEventHandler interface. This means you will not just receive updates when the authorization flow changes, but also if the session becomes invalid. In the examples below, login is implemented in this method.

This first part only covers logging in, to see what the other states mean, navigate to other states.
Note: This method is invoked in a sequential-manner, this means that you won’t receive a new update until you finished processing the old one.

How it works

The way login works is pretty easy. Some methods either return Task<RpcError?> or SomeValue?. In the first case, null is returned when no error has occured, in the second case null is returned when the operation could not be performed (check the logs).
The app advances state (i.e from asking for the phone number to asking the sms-code) when the corresponding update is received.

If receivedState >= LoginState.LoggedOut the app must close the instance, delete the session file and create a new instance, because the session is now invalid.

Handling errors

Some errors are recoverable, which means you can just re-try the query with different parameters.
The only recoverable errors are:

  • PhoneCodeIncorrectError which is returned when the provided phone code was entered incorrectly.
  • PasswordIncorrectError which is returned when the provided password was entered incorrectly.

When other errors are returned, a new state is sent to the app.
Every error is supposed to be shown to the user, even if it is UnknownError.

Restarting the flow

Even though it is not present in these examples, if the state is not higher or equal to LoggedIn you can call CancelAsync() to cancel the current operation.
This method does not return cannot return errors. As always, you will receive a new state.

Example of user login

public async Task OnSessionUpdateAsync(LoginState loginState)
{
    Task<RpcError?>? task = null;
    if (loginState is LoginState.AwaitingLogin)
    {
        char finalChoice;
        while (true)
        {
            Console.Write("Welcome! Would you like to login as a bot or as a user? (b/u): ");
            var readLine = Console.ReadLine();
            if (readLine is null || readLine.Length != 1 || (readLine[0] != 'u' && readLine[0] != 'b'))
            {
                Console.WriteLine("I'm sorry but this option is invalid");
                continue;
            }

            finalChoice = readLine[0];
            break;
        }

        if (finalChoice == 'u')
        {
            Console.Write("You've selected login as user. Please enter the phone number: ");
            task = _client.LoginManager.UsePhoneNumberAsync(Console.ReadLine() ?? "", new CodeSettings());
        }
        else
        {
            Console.Write("You've selected login as bot. Please enter the bot token: ");
            task = _client.LoginManager.UseBotTokenAsync(Console.ReadLine() ?? "");
        }

        var result = await task;
        if (result is BotTokenIncorrectError)
        {
            Console.WriteLine("I'm sorry, but this token does not work. Make sure you're copying it correctly.");
            return;
        }
        else if (result is PhoneNumberIncorrectError)
        {
            Console.WriteLine("I'm sorry, but this phone number does not work. Make sure you're typing every digit correctly.");
            return;
        }
    }
    else if (loginState is LoginState.AwaitingCode)
    {
        Console.Write("Please insert the login code or r to resend it: ");
        while (true)
        {
            var read = Console.ReadLine();
            if (read is null)
            {
                continue;
            }

            if (read == "r")
            {
                task = _client.LoginManager.ResendCodeAsync();
                var result = await task;
                if (result is null)
                {
                    var codeType = _client.LoginManager.GetCodeTypes()!.Value.CodeType;
                    Console.WriteLine($"The code was resent successfully. Code type {codeType}");
                }
            }
            else
            {
                task = _client.LoginManager.UseLoginCodeAsync(read);
                var result = await task;
                if (result is not null)
                {
                    if (result is PhoneCodeIncorrectError)
                    {
                        Console.WriteLine("The given phone code was invalid, try again or press r to resend it: ");
                        continue;
                    }
                }
                break;
            }
        }
    }
    else if (loginState is LoginState.AwaitingTermsAcceptance)
    {
        Console.WriteLine(_client.LoginManager.GetTermsOfService()!.Text);
        Console.Write("Great! Please accept the terms of service (y/n): ");
        _client.LoginManager.SetTerms(Console.ReadKey().KeyChar == 'y');
        Console.WriteLine();
    }
    else if (loginState is LoginState.AwaitingRegistration)
    {
        Console.WriteLine("It looks like this phone number is not registered.");
        var name = string.Empty;
        while (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(name))
        {
            Console.Write("What's your first name? ");
            name = Console.ReadLine();
        }

        Console.Write($"Ok {name}. What about your last name (Press enter to leave blank)? ");
        var lastName = Console.ReadLine() ?? "";
        task = _client.LoginManager.UseProfileDataAsync(name, lastName);
    }
    else if (loginState is LoginState.AwaitingPassword)
    {
        Console.Write("Yay! Please type your 2FA password here: ");
        while (true)
        {
            task = _client.LoginManager.UsePasswordAsync(Console.ReadLine() ?? "");
            var error = await task;
            if (error is not null)
            {
                if (error is PasswordIncorrectError)
                {
                    Console.WriteLine("Oops! The password is incorrect. Please try again.");
                    continue;
                }

                break;
            }
        }
    }
    else if (loginState is LoginState.LoggedIn)
    {
        var getSelf = await _client.Api.CloudChatsApi.Users.GetSelfAsync();
        if (getSelf.RpcCallFailed)
        {
            Console.WriteLine($"Something went wrong and I could not fetch the bot's profile. Error {getSelf.Error}");
        }
        Console.WriteLine($"Wow! We are logged in! We are: {getSelf.Response}");
    }
    else if (loginState >= LoginState.LoggedOut)
    {
        Console.WriteLine($"Hey! The session is dead. I received the following state: {loginState}");
    }

    if (task is not null)
    {
        var result = await task;
        if (result is not null)
        {
            Console.WriteLine($"I'm sorry, the following error occurred while logging in {result}");
        }
    }
}

Please note that if you use Console.ReadLine to prevent the app from quitting, you’ll need to do it in another way or to call it only when the login is finished. The latter is done in the full example using a TaskCompletionSource.

Code explaination

Even if at first sight this might look very complex and hard to understand, this explaination will try to make it as simple as possible. Remember that each time an API call is made, it is assigned to the task variable so that at the end of the method, it will be awaited and the error, if present, will be shown to the user.

  • When the state is AwaitingLogin, the app keeps asking the user whether they want to login as a user or as a bot until a a valid answer is received.
    If they choose to login as a user, the app will ask for a phone number, if they choose to login as a bot the app will ask for a bot token.
    The app awaits the API call to see if an error occurred, and if it is one of the known login-related errors it will show a better message to the user.

  • When the state is AwaitingPhoneCode, the app sent the phone code to the user and keeps asking the user whether they want to try a code or to resend it, until no error is returned or another error (such as flood wait) is received.

  • When the state is AwaitingPassword, the account is protected by 2FA (Two-Factor Authentication) and the app keeps asking the user for a valid password until login succeeds or an error different from PasswordIncorrectError from is returned.

  • When the state is AwaitingTermsAcceptance the app shows the user the terms of service and asks the user whether they accept them or not.

  • When the state is AwaitingRegistration the app asks the user for a first name and a last name (optionally) to register the user to Telegram.

  • When the state is LoggedIn, the app calls GetSelfAsync to retrieve information about the bot and logs it in json-format. The app also checks whether the API call received an error, because the session could have been terminated while we were still handling this update.

  • When the state is >= LoggedOut, the app will simply let the user know that it has changed (i.e. the session was terminated and SESSION_REVOKED was received).

Example of bot login

This is what handling a bot session from a simple console app looks like:

public async Task OnSessionUpdateAsync(LoginState loginState)
{
    if (loginState is LoginState.AwaitingLogin)
    {
        var r = await _client.LoginManager.UseBotTokenAsync("5525446665:AAH95cataglrak0Mpro9Awto4gud0zbUM");
        if (r is not null)
        {
            Console.WriteLine($"Could not login, the following error occurred: {r}");
        }
    }
    else if (loginState is LoginState.LoggedIn)
    {
        var getSelf = await _client.Api.CloudChatsApi.Users.GetSelfAsync();
        if (getSelf.RpcCallFailed)
        {
            Console.WriteLine($"Something went wrong and I could not fetch the bot's profile. Error {getSelf.Error}");
        }

        Console.WriteLine($"We are logged-in as {r.Response.ToJson()}");
    }
    else if (loginState >= LoginState.LoggedOut)
    {
        Console.WriteLine($"Received state {loginState}");
    }
}

Code explaination

The code is pretty simple, each time the state changes OnSessionUpdateAsync is invoked and the app checks whether the state is AwaitingLogin, LoggedIn or something else.

  • When the state is AwaitingLogin, the app tries to login using the bot token provided in the first parameter of the UseBotTokenAsync method, if an error is returned, it is logged to the user otherwise the app waits until it receives a new state.
  • When the state is LoggedIn, the app calls GetSelfAsync to retrieve information about the bot and logs it in json-format. The app also checks whether the API call received an error, because the session could have been terminated while we were still handling this update.
  • When the state is >= LoggedOut, the app will simply let the user know that it has changed (i.e. the session was terminated and SESSION_REVOKED was received).

Handling state changes (Other states)

Other states that are not directly related to the login flow are the following:

  • LoggedOut - When log out is done by calling LoginManager.LogoutAsync()
  • SessionRevoked - When the session was revoked from another device
  • AccountBanned - When the account was banned by Telegram
  • AccountDeactivated - When the user deactivated their account
  • KeyDuplicated - When the same session was used at the same time (i.e same session file on two different servers)

Once those are received, the session cannot be used anymore and you will need to create a new session file or wipe the current one. Before doing so, remember to DisposeAsync() the current TelegramClient instance.