Client session to tdata
Turn a logged-in Telethon, mtcute or grammers client into a Telegram Desktop tdata folder. There
are two ways, and the choice decides which session ends up in the folder:
- A new session. The logged-in client authorises a new session by QR login (the 2FA password included when the account has one), on the device of the API profile you pass. The folder gets the new key. Needs the network.
- The current session. The folder gets the client’s own auth key. Nothing is logged in, and the same device keeps using the key.
A QR login adds a real session to the account, visible in its active sessions. Run it on an account you own, keep the 2FA password out of your source (read it from the environment) and never share the folder it produces.
Generate the device
A new session gets a device of its own. Generate picks it from the client’s catalog, seeded by a
unique id you store with the account, so the same id gives the same device in Python, Node and
Rust. See API profiles and devices.
from opentele2.api import API
device = API.TelegramDesktop.Generate("windows", "my-id")
print(device.device_model, device.system_version) # Vostro 3520 Windows 11 x64import { generate } from 'opentele2'
const device = generate('TelegramDesktop', 'my-id', 'windows')
console.log(device.deviceModel, device.systemVersion) // Vostro 3520 Windows 11 x64use opentele2_core::api::{self, Versions};
fn main() -> opentele2_core::Result<()> {
let device = api::generate_desktop(Some("windows"), Some("my-id"), &Versions::offline())?;
println!("{} {} {}", device.device_model, device.system_version, device.app_version);
Ok(())
}Log a new session in by QR code
The flow is the same in every package: align the new client with the authorised client’s DC, export a login token on the new client, accept it on the authorised one, answer the 2FA check with the password through SRP, then send the requests an official client sends after a login. It makes up to five attempts.
import asyncio
import os
from opentele2.api import API, CreateNewSession
from opentele2.exception import NoPasswordProvided, PasswordIncorrect
from opentele2.tl import TelegramClient
async def main():
# A logged-in session, on the profile it was created with.
client = TelegramClient("telethon.session", api=API.TelegramDesktop)
try:
tdesk = await client.ToTDesktop(
CreateNewSession,
api=API.TelegramDesktop.Generate("windows", "my-id"),
password=os.environ.get("TG_PASSWORD"),
)
except (NoPasswordProvided, PasswordIncorrect) as error:
print("2FA:", error)
return
tdesk.SaveTData("tdata_new")
asyncio.run(main())ToTDesktop connects the client when it is not connected and needs it authorised.
client.QRLoginToNewClient(session, api, password) runs the same login and returns the new
TelegramClient instead of a folder.
import { TelegramClient } from '@mtcute/node'
import { generate, profile } from 'opentele2'
import { createCryptoProvider, initConnectionOptions, qrLoginToNewClient } from 'opentele2/mtcute'
const options = async (p) => ({
apiId: p.apiId,
apiHash: p.apiHash,
initConnectionOptions: initConnectionOptions(p),
crypto: await createCryptoProvider(),
})
const desktop = profile('TelegramDesktop')
const device = generate('TelegramDesktop', 'my-id', 'windows')
// authorized.session: a logged-in mtcute session, for example the one from tdata to mtcute.
const authorized = new TelegramClient({ ...(await options(desktop)), storage: 'authorized.session' })
const fresh = new TelegramClient({ ...(await options(device)), storage: 'fresh.session' })
const { client, userId } = await qrLoginToNewClient(authorized, fresh, {
profile: device,
password: process.env.TG_PASSWORD,
})
console.log('new session for', userId)fresh must start with empty storage; onUrl receives each token’s qrLoginUrl and expiry. A 2FA account without password rejects with code
OT_NO_PASSWORD_PROVIDED, a wrong one with OT_PASSWORD_INCORRECT; a login that never completes
rejects with OT_QR_LOGIN_TIMEOUT. postLogin: false skips the post-login requests.
// cargo add opentele2-grammers --features client
use std::sync::Arc;
use grammers_client::Client;
use grammers_mtsender::SenderPool;
use grammers_session::SessionData;
use grammers_session::storages::MemorySession;
use opentele2_core::api::{self, ApiProfile, Versions};
use opentele2_core::tdata::{DrawSource, NEW_FOLDER_ACTIVE_INDEX, TData};
use opentele2_grammers::client;
/// A grammers client over `session`, announcing `profile`, with its pool running.
fn connect(session: &Arc<MemorySession>, profile: &ApiProfile) -> Client {
let params = opentele2_grammers::connection_params(profile);
let SenderPool { runner, handle, .. } =
SenderPool::with_configuration(Arc::clone(session), profile.api_id, params);
tokio::spawn(runner.run());
Client::new(handle)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let versions = Versions::offline();
let tdata = TData::load("tdata", b"", None)?;
let account = tdata.main_account().expect("folder has an account");
// The authorised client: the folder's own session on the Telegram Desktop profile.
let desktop = api::profile(api::Client::Desktop, &versions);
let session = Arc::new(MemorySession::from(opentele2_grammers::session_data(account)));
let authorized = connect(&session, &desktop);
// The new client: a fresh session on the same DC, on a generated device.
let device = api::generate_desktop(Some("windows"), Some("my-id"), &versions)?;
let fresh = SessionData { home_dc: account.main_dc, ..SessionData::default() };
let new_session = Arc::new(MemorySession::from(fresh));
let new_client = connect(&new_session, &device);
let password = std::env::var("TG_PASSWORD").ok();
let (api_id, api_hash) = (device.api_id, device.api_hash.as_str());
let user_id =
client::qr_login(&authorized, &new_client, &*new_session, api_id, api_hash, password.as_deref())
.await?;
println!("new session for {user_id}");
let new_account = client::account_from_client(&new_client, &*new_session, 0).await?;
// No passcode, performance mode on, keys from the OS random source.
let folder =
TData::new_with_draws(vec![new_account], b"", true, NEW_FOLDER_ACTIVE_INDEX, DrawSource::os())?;
folder.save_random("tdata_new")?;
Ok(())
}grammers 0.11 delivers updates only to the pool’s owner, so qr_login exports the token again right
after accepting it instead of waiting for updateLoginToken. Building the new session, its DC and
an existing session are up to you; ClientError::NoPasswordProvided and
ClientError::PasswordIncorrect report 2FA problems.
Save the folder
# tdesk: the TDesktop from ToTDesktop
tdesk.SaveTData("tdata_new") # SaveTData("tdata_new", passcode="...") to lock itThe folder is written as Telegram Desktop 7.3.1 writes it. Without a passcode, performance mode
(TDesktop.kPerformanceMode, on by default) uses fixed keys and leaves the configs files out.
import { saveTdata } from 'opentele2'
import { tdataFromClient } from 'opentele2/mtcute'
// client: the new client from qrLoginToNewClient
const tdata = await tdataFromClient(client, { passcode: process.env.TDATA_PASSCODE })
await saveTdata(tdata, 'tdata_new')tdataFromClient reads the main DC and key from client.exportSession() and the user id from the
server.
The last lines of the previous step save it: account_from_client reads the home DC key and the
user id, TData::new_with_draws builds the folder and save_random writes it. With a passcode,
pass its bytes as the second argument. client::tdata_from_client(&client, &*session, passcode, rng) does both in one call.
Or store the current session offline
import asyncio
from opentele2.api import UseCurrentSession
from opentele2.td import TDesktop
async def from_pair():
# The companion's id is the user id, so nothing is sent.
tdesk = await TDesktop.FromSessionJson("account.session")
tdesk.SaveTData("tdata_current")
async def from_client(client):
# client: an opentele2 TelegramClient; get_me() runs first without a known user id.
tdesk = await client.ToTDesktop(UseCurrentSession)
tdesk.SaveTData("tdata_current")
asyncio.run(from_pair())import { newTdata, saveTdata } from 'opentele2'
import { loadSessionPair } from 'opentele2/telethon'
const { rows, json } = await loadSessionPair('account')
const tdata = await newTdata([
{ index: 0, userId: BigInt(json.id), mainDc: rows.dcId, authKey: rows.authKey },
])
await saveTdata(tdata, 'tdata_current')Over a live mtcute client, tdataFromClient(client) stores its own session; it asks the server for
the user id.
use grammers_session::SessionData;
use opentele2_core::tdata::{DrawSource, NEW_FOLDER_ACTIVE_INDEX, TData};
fn to_tdata(data: &SessionData) -> opentele2_core::Result<()> {
// The home DC key and the cached self user; nothing is sent.
let account = opentele2_grammers::account_from_session(data, 0)?;
let tdata =
TData::new_with_draws(vec![account], b"", true, NEW_FOLDER_ACTIVE_INDEX, DrawSource::os())?;
tdata.save_random("tdata_current")
}A folder that holds the current session shares its key with the client it came from: both are the same session to the server. Use a new session when the folder will run on another device.
Next
- tdata: passcodes, performance mode and what a saved folder holds.
- Consistency and sessions: see the new session in the account’s list and end old ones.
- Reference: Python · Node · Rust.