import axios from 'axios';
import * as htmlParser from 'node-html-parser';
import { ELogActions } from '../../enums/Logging';
import { calculateClientTransactionIdHeader } from '../../helper/TidUtils';
import { RettiwtConfig } from '../../models/RettiwtConfig';
import { ITidDynamicArgs } from '../../types/auth/TidDynamicArgs';
import { ITidProvider } from '../../types/auth/TidProvider';
import { LogService } from './LogService';
/**
* Handles transaction ID generation for requests to Twitter.
*
* @internal
*/
export class TidService implements ITidProvider {
private readonly _cdnUrl: string;
private readonly _config: RettiwtConfig;
private readonly _requestHeaders: NonNullable;
private _dynamicArgs?: ITidDynamicArgs;
/**
* @param config - The config for Rettiwt.
*/
public constructor(config: RettiwtConfig) {
this._cdnUrl = 'https://abs.twimg.com/responsive-web/client-web';
this._config = config;
this._requestHeaders = {
/* eslint-disable @typescript-eslint/naming-convention */
Authority: 'x.com',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache',
Referer: 'https://x.com',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
'X-Twitter-Active-User': 'yes',
'X-Twitter-Client-Language': 'en',
/* eslint-enable @typescript-eslint/naming-convention */
};
}
/**
* Fetches the dynamic args embedded in the homepage.
*
* @returns The new dynamic args.
*/
private async getDynamicArgs(): Promise {
const html = await this.getHomepageHtml();
const root = htmlParser.parse(html);
const keyElement = root.querySelector("[name='twitter-site-verification']");
const frameElements = root.querySelectorAll("[id^='loading-x-anim']");
return {
verificationKey: keyElement?.getAttribute('content') ?? '',
frames: frameElements.map((el) => this.parseFrameElement(el)),
indices: await this.getKeyBytesIndices(html),
};
}
/**
* Fetches the HTML content of Twitter's homepage.
*
* @returns The stringified HTML content of the homepage.
*/
private async getHomepageHtml(): Promise {
const response = await axios.get('https://x.com', {
headers: this._requestHeaders,
httpAgent: this._config.httpsAgent,
httpsAgent: this._config.httpsAgent,
});
return response.data;
}
private async getKeyBytesIndices(html: string): Promise {
const ondemandFileMatch = html.match(/ondemand\.s":"([^"]+)"/);
if (!ondemandFileMatch || !ondemandFileMatch[1]) {
LogService.log(ELogActions.WARNING, { message: 'ondemand.s file not found' });
return [0, 0, 0, 0];
}
const onDemandFileHash = ondemandFileMatch ? ondemandFileMatch[1] : '';
const response = await axios.get(`${this._cdnUrl}/ondemand.s.${onDemandFileHash}a.js`, {
httpAgent: this._config.httpsAgent,
httpsAgent: this._config.httpsAgent,
});
const match = response.data.matchAll(/(\(\w\[(\d{1,2})],\s*16\))+?/gm);
return Array.from(match).map((m) => Number(m[2]));
}
private parseFrameElement(element: htmlParser.HTMLElement): number[][] {
const pathElement = element.children[0].children[1];
const value = pathElement.getAttribute('d');
if (!value) {
return [[]];
}
const rawFrames = value.substring(9).split('C');
return rawFrames.map((str) => str.replaceAll(/\D+/g, ' ').trim().split(' ')).map((arr) => arr.map(Number));
}
/**
* Generate an `x-client-transaction-id` for the specific URL method and path.
*
* @param method - The target method.
* @param path - The target path.
*
* @returns The specific `x-client-transaction-id` token.
*/
public async generate(method: string, path: string): Promise {
try {
if (!this._dynamicArgs) {
this._dynamicArgs = await this.getDynamicArgs();
}
const { verificationKey, frames, indices } = this._dynamicArgs;
return calculateClientTransactionIdHeader({
keyword: 'obfiowerehiring',
method: method,
path: path,
verificationKey: verificationKey,
frames: frames,
indices: indices,
extraByte: 3,
});
} catch {
return;
}
}
/**
* Refreshes the dynamic args from the homepage.
*/
public async refreshDynamicArgs(): Promise {
this._dynamicArgs = await this.getDynamicArgs();
}
}