GramJS: Store test server flag with session (#5084)

This commit is contained in:
zubiden 2024-11-02 21:10:46 +04:00 committed by Alexander Zinchuk
parent e96ff349c3
commit e2a717dc29
15 changed files with 76 additions and 233 deletions

View File

@ -220,6 +220,7 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
export function buildApiConfig(config: GramJs.Config): ApiConfig { export function buildApiConfig(config: GramJs.Config): ApiConfig {
const defaultReaction = config.reactionsDefault && buildApiReaction(config.reactionsDefault); const defaultReaction = config.reactionsDefault && buildApiReaction(config.reactionsDefault);
return { return {
isTestServer: config.testMode,
expiresAt: config.expires, expiresAt: config.expires,
gifSearchUsername: config.gifSearchUsername, gifSearchUsername: config.gifSearchUsername,
defaultReaction, defaultReaction,

View File

@ -69,10 +69,11 @@ export async function init(initialArgs: ApiInitialArgs) {
} }
const { const {
userAgent, platform, sessionData, isTest, isWebmSupported, maxBufferSize, webAuthToken, dcId, userAgent, platform, sessionData, isWebmSupported, maxBufferSize, webAuthToken, dcId,
mockScenario, shouldForceHttpTransport, shouldAllowHttpTransport, mockScenario, shouldForceHttpTransport, shouldAllowHttpTransport,
shouldDebugExportedSenders, langCode, shouldDebugExportedSenders, langCode, isTestServerRequested,
} = initialArgs; } = initialArgs;
const session = new sessions.CallbackSession(sessionData, onSessionUpdate); const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
@ -93,9 +94,9 @@ export async function init(initialArgs: ApiInitialArgs) {
shouldDebugExportedSenders, shouldDebugExportedSenders,
shouldForceHttpTransport, shouldForceHttpTransport,
shouldAllowHttpTransport, shouldAllowHttpTransport,
testServers: isTest,
dcId, dcId,
langCode, langCode,
isTestServerRequested,
} as any, } as any,
); );

View File

@ -17,6 +17,7 @@ export interface ApiInitialArgs {
shouldForceHttpTransport?: boolean; shouldForceHttpTransport?: boolean;
shouldDebugExportedSenders?: boolean; shouldDebugExportedSenders?: boolean;
langCode: string; langCode: string;
isTestServerRequested?: boolean;
} }
export interface ApiOnProgress { export interface ApiOnProgress {
@ -96,6 +97,7 @@ export interface ApiWebSession {
export interface ApiSessionData { export interface ApiSessionData {
mainDcId: number; mainDcId: number;
isTest?: true;
keys: Record<number, string | number[]>; keys: Record<number, string | number[]>;
hashes: Record<number, string | number[]>; hashes: Record<number, string | number[]>;
} }
@ -242,6 +244,7 @@ export interface ApiConfig {
gifSearchUsername?: string; gifSearchUsername?: string;
maxGroupSize: number; maxGroupSize: number;
autologinToken?: string; autologinToken?: string;
isTestServer?: boolean;
} }
export type ApiPeerColorSet = string[]; export type ApiPeerColorSet = string[];

View File

@ -38,6 +38,7 @@ type StateProps = {
hasPasscode?: boolean; hasPasscode?: boolean;
isInactiveAuth?: boolean; isInactiveAuth?: boolean;
hasWebAuthTokenFailed?: boolean; hasWebAuthTokenFailed?: boolean;
isTestServer?: boolean;
theme: ThemeKey; theme: ThemeKey;
}; };
@ -57,6 +58,7 @@ const App: FC<StateProps> = ({
hasPasscode, hasPasscode,
isInactiveAuth, isInactiveAuth,
hasWebAuthTokenFailed, hasWebAuthTokenFailed,
isTestServer,
theme, theme,
}) => { }) => {
const { disconnect } = getActions(); const { disconnect } = getActions();
@ -221,6 +223,7 @@ const App: FC<StateProps> = ({
> >
{renderContent} {renderContent}
</Transition> </Transition>
{activeKey === AppScreens.auth && isTestServer && <div className="test-server-badge">Test server</div>}
</UiLoader> </UiLoader>
); );
}; };
@ -234,6 +237,7 @@ export default withGlobal(
isInactiveAuth: selectTabState(global).isInactive, isInactiveAuth: selectTabState(global).isInactive,
hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired, hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired,
theme: selectTheme(global), theme: selectTheme(global),
isTestServer: global.config?.isTestServer,
}; };
}, },
)(App); )(App);

View File

@ -219,6 +219,12 @@
word-break: normal !important; word-break: normal !important;
} }
.test-server-badge {
position: fixed;
bottom: 0.5rem;
right: 0.5rem;
}
@keyframes qr-show { @keyframes qr-show {
0% { 0% {
opacity: 0; opacity: 0;

View File

@ -397,6 +397,8 @@ const Main = ({
// Parse deep link // Parse deep link
useEffect(() => { useEffect(() => {
if (!isSynced) return; if (!isSynced) return;
updatePageTitle();
const parsedInitialLocationHash = parseInitialLocationHash(); const parsedInitialLocationHash = parseInitialLocationHash();
if (parsedInitialLocationHash?.tgaddr) { if (parsedInitialLocationHash?.tgaddr) {
processDeepLink(decodeURIComponent(parsedInitialLocationHash.tgaddr)); processDeepLink(decodeURIComponent(parsedInitialLocationHash.tgaddr));

View File

@ -40,11 +40,12 @@ import {
addActionHandler('initApi', (global, actions): ActionReturnType => { addActionHandler('initApi', (global, actions): ActionReturnType => {
const initialLocationHash = parseInitialLocationHash(); const initialLocationHash = parseInitialLocationHash();
const hasTestParam = window.location.search.includes('test') || initialLocationHash?.tgWebAuthTest === '1';
void initApi(actions.apiUpdate, { void initApi(actions.apiUpdate, {
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
platform: PLATFORM_ENV, platform: PLATFORM_ENV,
sessionData: loadStoredSession(), sessionData: loadStoredSession(),
isTest: window.location.search.includes('test') || initialLocationHash?.tgWebAuthTest === '1',
isWebmSupported: IS_WEBM_SUPPORTED, isWebmSupported: IS_WEBM_SUPPORTED,
maxBufferSize: MAX_BUFFER_SIZE, maxBufferSize: MAX_BUFFER_SIZE,
webAuthToken: initialLocationHash?.tgWebAuthToken, webAuthToken: initialLocationHash?.tgWebAuthToken,
@ -54,6 +55,7 @@ addActionHandler('initApi', (global, actions): ActionReturnType => {
shouldForceHttpTransport: global.settings.byKey.shouldForceHttpTransport, shouldForceHttpTransport: global.settings.byKey.shouldForceHttpTransport,
shouldDebugExportedSenders: global.settings.byKey.shouldDebugExportedSenders, shouldDebugExportedSenders: global.settings.byKey.shouldDebugExportedSenders,
langCode: global.settings.byKey.language, langCode: global.settings.byKey.language,
isTestServerRequested: hasTestParam,
}); });
void setShouldEnableDebugLog(Boolean(global.settings.byKey.shouldCollectDebugLogs)); void setShouldEnableDebugLog(Boolean(global.settings.byKey.shouldCollectDebugLogs));

View File

@ -243,10 +243,21 @@ function onUpdateConnectionState<T extends GlobalState>(
function onUpdateSession<T extends GlobalState>(global: T, actions: RequiredGlobalActions, update: ApiUpdateSession) { function onUpdateSession<T extends GlobalState>(global: T, actions: RequiredGlobalActions, update: ApiUpdateSession) {
const { sessionData } = update; const { sessionData } = update;
global = getGlobal();
const { authRememberMe, authState } = global; const { authRememberMe, authState } = global;
const isEmpty = !sessionData || !sessionData.mainDcId; const isEmpty = !sessionData || !sessionData.mainDcId;
const isTest = sessionData?.isTest;
if (isTest) {
global = {
...global,
config: {
...global.config,
isTestServer: isTest,
},
};
setGlobal(global);
}
if (!authRememberMe || authState !== 'authorizationStateReady' || isEmpty) { if (!authRememberMe || authState !== 'authorizationStateReady' || isEmpty) {
return; return;
} }

View File

@ -749,10 +749,12 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const { canDisplayChatInTitle } = global.settings.byKey; const { canDisplayChatInTitle } = global.settings.byKey;
const currentUserId = global.currentUserId; const currentUserId = global.currentUserId;
const isTestServer = global.config?.isTestServer;
const prefix = isTestServer ? '[T] ' : '';
if (document.title.includes(INACTIVE_MARKER)) { if (document.title.includes(INACTIVE_MARKER)) {
updateIcon(false); updateIcon(false);
setPageTitleInstant(`${PAGE_TITLE} ${INACTIVE_MARKER}`); setPageTitleInstant(`${prefix}${PAGE_TITLE} ${INACTIVE_MARKER}`);
return; return;
} }
@ -762,7 +764,7 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
const newUnread = notificationCount - global.initialUnreadNotifications; const newUnread = notificationCount - global.initialUnreadNotifications;
if (newUnread > 0) { if (newUnread > 0) {
setPageTitleInstant(`${newUnread} notification${newUnread > 1 ? 's' : ''}`); setPageTitleInstant(`${prefix}${newUnread} notification${newUnread > 1 ? 's' : ''}`);
updateIcon(true); updateIcon(true);
return; return;
} }
@ -779,16 +781,16 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
const title = getChatTitle(langProvider.oldTranslate, currentChat, chatId === currentUserId); const title = getChatTitle(langProvider.oldTranslate, currentChat, chatId === currentUserId);
const topic = selectTopic(global, chatId, threadId); const topic = selectTopic(global, chatId, threadId);
if (currentChat.isForum && topic) { if (currentChat.isForum && topic) {
setPageTitle(`${title} ${topic.title}`); setPageTitle(`${prefix}${title} ${topic.title}`);
return; return;
} }
setPageTitle(title); setPageTitle(`${prefix}${title}`);
return; return;
} }
} }
setPageTitleInstant(IS_ELECTRON ? '' : PAGE_TITLE); setPageTitleInstant(IS_ELECTRON ? '' : `${prefix}${PAGE_TITLE}`);
}); });
addActionHandler('closeInviteViaLinkModal', (global, actions, payload): ActionReturnType => { addActionHandler('closeInviteViaLinkModal', (global, actions, payload): ActionReturnType => {

View File

@ -291,6 +291,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
...INITIAL_GLOBAL_STATE, ...INITIAL_GLOBAL_STATE,
...pick(global, [ ...pick(global, [
'appConfig', 'appConfig',
'config',
'authState', 'authState',
'authPhoneNumber', 'authPhoneNumber',
'authRememberMe', 'authRememberMe',

View File

@ -77,8 +77,8 @@ class TelegramClient {
baseLogger: 'gramjs', baseLogger: 'gramjs',
useWSS: false, useWSS: false,
additionalDcsDisabled: false, additionalDcsDisabled: false,
testServers: false,
dcId: DEFAULT_DC_ID, dcId: DEFAULT_DC_ID,
isTestServerRequested: false,
shouldAllowHttpTransport: false, shouldAllowHttpTransport: false,
shouldForceHttpTransport: false, shouldForceHttpTransport: false,
shouldDebugExportedSenders: false, shouldDebugExportedSenders: false,
@ -218,10 +218,10 @@ class TelegramClient {
this._sender._disconnected = true; this._sender._disconnected = true;
const connection = new this._connection( const connection = new this._connection(
this.session.serverAddress, this.session.port, this.session.dcId, this._log, this._args.testServers, this.session.serverAddress, this.session.port, this.session.dcId, this._log, this.session.isTestServer,
); );
const fallbackConnection = new this._fallbackConnection( const fallbackConnection = new this._fallbackConnection(
this.session.serverAddress, this.session.port, this.session.dcId, this._log, this._args.testServers, this.session.serverAddress, this.session.port, this.session.dcId, this._log, this.session.isTestServer,
); );
const newConnection = await this._sender.connect(connection, undefined, fallbackConnection); const newConnection = await this._sender.connect(connection, undefined, fallbackConnection);
@ -257,7 +257,9 @@ class TelegramClient {
if (!this.session.serverAddress || (this.session.serverAddress.includes(':') !== this._useIPV6)) { if (!this.session.serverAddress || (this.session.serverAddress.includes(':') !== this._useIPV6)) {
const DC = utils.getDC(this.defaultDcId); const DC = utils.getDC(this.defaultDcId);
// TODO Fill IP addresses for when `this._useIPV6` is used // TODO Fill IP addresses for when `this._useIPV6` is used
this.session.setDC(this.defaultDcId, DC.ipAddress, this._args.useWSS ? 443 : 80); this.session.setDC(
this.defaultDcId, DC.ipAddress, this._args.useWSS ? 443 : 80, this._args.isTestServerRequested,
);
} }
} }
@ -417,7 +419,8 @@ class TelegramClient {
async _switchDC(newDc) { async _switchDC(newDc) {
this._log.info(`Reconnecting to new data center ${newDc}`); this._log.info(`Reconnecting to new data center ${newDc}`);
const DC = utils.getDC(newDc); const DC = utils.getDC(newDc);
this.session.setDC(newDc, DC.ipAddress, DC.port); const isTestServer = this.session.isTestServer || this._args.isTestServerRequested;
this.session.setDC(newDc, DC.ipAddress, DC.port, isTestServer);
// authKey's are associated with a server, which has now changed // authKey's are associated with a server, which has now changed
// so it's not valid anymore. Set to None to force recreating it. // so it's not valid anymore. Set to None to force recreating it.
await this._sender.authKey.setKey(undefined); await this._sender.authKey.setKey(undefined);
@ -495,7 +498,7 @@ class TelegramClient {
dc.port, dc.port,
dcId, dcId,
this._log, this._log,
this._args.testServers, this.session.isTestServer,
// Premium DCs are not stable for obtaining auth keys, so need to we first connect to regular ones // Premium DCs are not stable for obtaining auth keys, so need to we first connect to regular ones
hasAuthKey ? isPremium : false, hasAuthKey ? isPremium : false,
), undefined, new this._fallbackConnection( ), undefined, new this._fallbackConnection(
@ -503,7 +506,7 @@ class TelegramClient {
dc.port, dc.port,
dcId, dcId,
this._log, this._log,
this._args.testServers, this.session.isTestServer,
hasAuthKey ? isPremium : false, hasAuthKey ? isPremium : false,
)); ));

View File

@ -22,6 +22,7 @@ class Connection {
this._dcId = dcId; this._dcId = dcId;
this._log = loggers; this._log = loggers;
this._testServers = testServers; this._testServers = testServers;
this._isPremium = isPremium; this._isPremium = isPremium;
this._connected = false; this._connected = false;
this._sendTask = undefined; this._sendTask = undefined;

View File

@ -29,13 +29,14 @@ class CallbackSession extends MemorySession {
mainDcId, mainDcId,
keys, keys,
hashes, hashes,
isTest,
} = this._sessionData; } = this._sessionData;
const { const {
ipAddress, ipAddress,
port, port,
} = utils.getDC(mainDcId); } = utils.getDC(mainDcId);
this.setDC(mainDcId, ipAddress, port, true); this.setDC(mainDcId, ipAddress, port, isTest, true);
await Promise.all(Object.keys(keys) await Promise.all(Object.keys(keys)
.map(async (dcId) => { .map(async (dcId) => {
@ -56,10 +57,11 @@ class CallbackSession extends MemorySession {
})); }));
} }
setDC(dcId, serverAddress, port, skipOnUpdate = false) { setDC(dcId, serverAddress, port, isTestServer, skipOnUpdate = false) {
this._dcId = dcId; this._dcId = dcId;
this._serverAddress = serverAddress; this._serverAddress = serverAddress;
this._port = port; this._port = port;
this._isTestServer = isTestServer;
delete this._authKeys[dcId]; delete this._authKeys[dcId];
@ -83,6 +85,7 @@ class CallbackSession extends MemorySession {
mainDcId: this._dcId, mainDcId: this._dcId,
keys: {}, keys: {},
hashes: {}, hashes: {},
isTest: this._isTestServer || undefined,
}; };
Object Object

View File

@ -8,6 +8,7 @@ class MemorySession extends Session {
this._dcId = 0; this._dcId = 0;
this._port = undefined; this._port = undefined;
this._takeoutId = undefined; this._takeoutId = undefined;
this._isTestServer = false;
this._entities = new Set(); this._entities = new Set();
this._updateStates = {}; this._updateStates = {};
@ -33,222 +34,16 @@ class MemorySession extends Session {
this._authKey = value; this._authKey = value;
} }
setDC(dcId, serverAddress, port) { get isTestServer() {
return this._isTestServer;
}
setDC(dcId, serverAddress, port, isTestServer) {
this._dcId = dcId | 0; this._dcId = dcId | 0;
this._serverAddress = serverAddress; this._serverAddress = serverAddress;
this._port = port; this._port = port;
this._isTestServer = isTestServer;
} }
/* CONTEST
get takeoutId() {
return this._takeoutId
}
set takeoutId(value) {
this._takeoutId = value
}
getUpdateState(entityId) {
return this._updateStates[entityId]
}
setUpdateState(entityId, state) {
return this._updateStates[entityId] = state
}
close() {
}
save() {
}
async load() {
}
delete() {
}
_entityValuesToRow(id, hash, username, phone, name) {
// While this is a simple implementation it might be overrode by,
// other classes so they don't need to implement the plural form
// of the method. Don't remove.
return [id, hash, username, phone, name]
}
_entityToRow(e) {
if (!(e.classType === "constructor")) {
return
}
let p
let markedId
try {
p = utils.getInputPeer(e, false)
markedId = utils.getPeerId(p)
} catch (e) {
// Note: `get_input_peer` already checks for non-zero `accessHash`.
// See issues #354 and #392. It also checks that the entity
// is not `min`, because its `accessHash` cannot be used
// anywhere (since layer 102, there are two access hashes).
return
}
let pHash
if (p instanceof types.InputPeerUser || p instanceof types.InputPeerChannel) {
pHash = p.accessHash
} else if (p instanceof types.InputPeerChat) {
pHash = 0
} else {
return
}
let username = e.username
if (username) {
username = username.toLowerCase()
}
const phone = e.phone
const name = utils.getDisplayName(e)
return this._entityValuesToRow(markedId, pHash, username, phone, name)
}
_entitiesToRows(tlo) {
let entities = []
if (tlo.classType === "constructor" && utils.isListLike(tlo)) {
// This may be a list of users already for instance
entities = tlo
} else {
if (tlo instanceof Object) {
if ('user' in tlo) {
entities.push(tlo.user)
}
if ('chats' in tlo && utils.isListLike(tlo.chats)) {
entities.concat(tlo.chats)
}
if ('users' in tlo && utils.isListLike(tlo.users)) {
entities.concat(tlo.users)
}
}
}
const rows = [] // Rows to add (id, hash, username, phone, name)
for (const e of entities) {
const row = this._entityToRow(e)
if (row) {
rows.push(row)
}
}
return rows
}
processEntities(tlo) {
const entitiesSet = this._entitiesToRows(tlo)
for (const e of entitiesSet) {
this._entities.add(e)
}
}
getEntityRowsByPhone(phone) {
for (const e of this._entities) { // id, hash, username, phone, name
if (e[3] === phone) {
return [e[0], e[1]]
}
}
}
getEntityRowsByUsername(username) {
for (const e of this._entities) { // id, hash, username, phone, name
if (e[2] === username) {
return [e[0], e[1]]
}
}
}
getEntityRowsByName(name) {
for (const e of this._entities) { // id, hash, username, phone, name
if (e[4] === name) {
return [e[0], e[1]]
}
}
}
getEntityRowsById(id, exact = true) {
if (exact) {
for (const e of this._entities) { // id, hash, username, phone, name
if (e[0] === id) {
return [e[0], e[1]]
}
}
} else {
const ids = [utils.getPeerId(new types.PeerUser({ userId: id })),
utils.getPeerId(new types.PeerChat({ chatId: id })),
utils.getPeerId(new types.PeerChannel({ channelId: id })),
]
for (const e of this._entities) { // id, hash, username, phone, name
if (ids.includes(e[0])) {
return [e[0], e[1]]
}
}
}
}
getInputEntity(key) {
let exact
if (key.SUBCLASS_OF_ID !== undefined) {
if ([0xc91c90b6, 0xe669bf46, 0x40f202fd].includes(key.SUBCLASS_OF_ID)) {
// hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel'))
// We already have an Input version, so nothing else required
return key
}
// Try to early return if this key can be casted as input peer
return utils.getInputPeer(key)
} else {
// Not a TLObject or can't be cast into InputPeer
if (key.classType === 'constructor') {
key = utils.getPeerId(key)
exact = true
} else {
exact = !(typeof key == 'number') || key < 0
}
}
let result = null
if (typeof key === 'string') {
const phone = utils.parsePhone(key)
if (phone) {
result = this.getEntityRowsByPhone(phone)
} else {
const { username, isInvite } = utils.parseUsername(key)
if (username && !isInvite) {
result = this.getEntityRowsByUsername(username)
} else {
const tup = utils.resolveInviteLink(key)[1]
if (tup) {
result = this.getEntityRowsById(tup, false)
}
}
}
} else if (typeof key === 'number') {
result = this.getEntityRowsById(key, exact)
}
if (!result && typeof key === 'string') {
result = this.getEntityRowsByName(key)
}
if (result) {
let entityId = result[0] // unpack resulting tuple
const entityHash = result[1]
const resolved = utils.resolveId(entityId)
entityId = resolved[0]
const kind = resolved[1]
// removes the mark and returns type of entity
if (kind === types.PeerUser) {
return new types.InputPeerUser({ userId: entityId, accessHash: entityHash })
} else if (kind === types.PeerChat) {
return new types.InputPeerChat({ chatId: entityId })
} else if (kind === types.PeerChannel) {
return new types.InputPeerChannel({ channelId: entityId, accessHash: entityHash })
}
} else {
throw new Error('Could not find input entity with key ' + key)
}
} */
} }
module.exports = MemorySession; module.exports = MemorySession;

View File

@ -27,9 +27,15 @@ export function hasStoredSession() {
} }
export function storeSession(sessionData: ApiSessionData, currentUserId?: string) { export function storeSession(sessionData: ApiSessionData, currentUserId?: string) {
const { mainDcId, keys, hashes } = sessionData; const {
mainDcId, keys, hashes, isTest,
} = sessionData;
localStorage.setItem(SESSION_USER_KEY, JSON.stringify({ dcID: mainDcId, id: currentUserId })); localStorage.setItem(SESSION_USER_KEY, JSON.stringify({
dcID: mainDcId,
id: currentUserId,
test: isTest,
}));
localStorage.setItem('dc', String(mainDcId)); localStorage.setItem('dc', String(mainDcId));
Object.keys(keys).map(Number).forEach((dcId) => { Object.keys(keys).map(Number).forEach((dcId) => {
localStorage.setItem(`dc${dcId}_auth_key`, JSON.stringify(keys[dcId])); localStorage.setItem(`dc${dcId}_auth_key`, JSON.stringify(keys[dcId]));
@ -64,6 +70,7 @@ export function loadStoredSession(): ApiSessionData | undefined {
return undefined; return undefined;
} }
const mainDcId = Number(userAuth.dcID); const mainDcId = Number(userAuth.dcID);
const isTest = userAuth.test;
const keys: Record<number, string> = {}; const keys: Record<number, string> = {};
const hashes: Record<number, string> = {}; const hashes: Record<number, string> = {};
@ -93,6 +100,7 @@ export function loadStoredSession(): ApiSessionData | undefined {
mainDcId, mainDcId,
keys, keys,
hashes, hashes,
isTest,
}; };
} }