feat: app config manager

This commit is contained in:
alina 🌸 2024-02-05 01:44:51 +03:00
parent e6c7af6ed2
commit 27e14472ff
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
18 changed files with 1317 additions and 110 deletions

View file

@ -0,0 +1,25 @@
const fs = require('fs')
const path = require('path')
const spec = require('@mtcute/tl/app-config.json')
const OUT_FILE = path.join(__dirname, '../src/highlevel/types/misc/app-config.ts')
const out = fs.createWriteStream(OUT_FILE)
out.write(`// This file is generated automatically, do not modify!
/* eslint-disable */
export interface AppConfigSchema {
`)
const indent = (str) => str.split('\n').map((x) => ' ' + x).join('\n')
for (const [key, { type, description }] of Object.entries(spec)) {
out.write(indent(description) + '\n')
out.write(indent(`${key}?: ${type}`) + '\n')
}
out.write(' [key: string]: unknown\n')
out.write('}\n')
out.close()

View file

@ -16,6 +16,7 @@ import {
} from '../utils/index.js' } from '../utils/index.js'
import { LogManager } from '../utils/logger.js' import { LogManager } from '../utils/logger.js'
import { ITelegramClient } from './client.types.js' import { ITelegramClient } from './client.types.js'
import { AppConfigManager } from './managers/app-config-manager.js'
import { ITelegramStorageProvider } from './storage/provider.js' import { ITelegramStorageProvider } from './storage/provider.js'
import { TelegramStorageManager, TelegramStorageManagerExtraOptions } from './storage/storage.js' import { TelegramStorageManager, TelegramStorageManagerExtraOptions } from './storage/storage.js'
import { UpdatesManager } from './updates/manager.js' import { UpdatesManager } from './updates/manager.js'
@ -52,6 +53,7 @@ export class BaseTelegramClient implements ITelegramClient {
provider: this.params.storage, provider: this.params.storage,
...this.params.storageOptions, ...this.params.storageOptions,
}) })
readonly appConfig = new AppConfigManager(this)
private _prepare = asyncResettable(async () => { private _prepare = asyncResettable(async () => {
await this.mt.prepare() await this.mt.prepare()

View file

@ -4,6 +4,7 @@ import type { ConnectionKind, RpcCallOptions } from '../network/index.js'
import type { MustEqual, PublicPart } from '../types/utils.js' import type { MustEqual, PublicPart } from '../types/utils.js'
import type { Logger } from '../utils/logger.js' import type { Logger } from '../utils/logger.js'
import type { StringSessionData } from '../utils/string-session.js' import type { StringSessionData } from '../utils/string-session.js'
import type { AppConfigManager } from './managers/app-config-manager.js'
import type { TelegramStorageManager } from './storage/storage.js' import type { TelegramStorageManager } from './storage/storage.js'
import type { RawUpdateHandler } from './updates/types.js' import type { RawUpdateHandler } from './updates/types.js'
@ -14,6 +15,7 @@ import type { RawUpdateHandler } from './updates/types.js'
export interface ITelegramClient { export interface ITelegramClient {
readonly log: Logger readonly log: Logger
readonly storage: PublicPart<TelegramStorageManager> readonly storage: PublicPart<TelegramStorageManager>
readonly appConfig: PublicPart<AppConfigManager>
prepare(): Promise<void> prepare(): Promise<void>
connect(): Promise<void> connect(): Promise<void>

View file

@ -0,0 +1,57 @@
import { tl } from '@mtcute/tl'
import { MtTypeAssertionError } from '../../types/errors.js'
import { Reloadable } from '../../utils/reloadable.js'
import { tlJsonToJson } from '../../utils/tl-json.js'
import { BaseTelegramClient } from '../base.js'
import { AppConfigSchema } from '../types/misc/app-config.js'
export class AppConfigManager {
constructor(private client: BaseTelegramClient) {}
private _reloadable = new Reloadable<tl.help.RawAppConfig>({
reload: this._reload.bind(this),
getExpiresAt: () => 3_600_000,
disableAutoReload: true,
})
private async _reload(old?: tl.help.RawAppConfig) {
const res = await this.client.call({
_: 'help.getAppConfig',
hash: old?.hash ?? 0,
})
if (res._ === 'help.appConfigNotModified') return old!
return res
}
private _object?: AppConfigSchema
async get(): Promise<AppConfigSchema> {
if (!this._reloadable.isStale && this._object) return this._object
const obj = tlJsonToJson((await this._reloadable.get()).config)
if (!obj || typeof obj !== 'object') {
throw new MtTypeAssertionError('appConfig', 'object', typeof obj)
}
this._object = obj as AppConfigSchema
return this._object
}
async getField<K extends keyof AppConfigSchema>(field: K): Promise<AppConfigSchema[K]>
async getField<K extends keyof AppConfigSchema>(
field: K,
fallback: NonNullable<AppConfigSchema[K]>,
): Promise<NonNullable<AppConfigSchema[K]>>
async getField<K extends keyof AppConfigSchema>(
field: K,
fallback?: NonNullable<AppConfigSchema[K]>,
): Promise<AppConfigSchema[K]> {
const obj = await this.get()
return obj[field] ?? fallback
}
}

View file

@ -0,0 +1,896 @@
// This file is generated automatically, do not modify!
/* eslint-disable */
export interface AppConfigSchema {
/**
* <a href="https://corefork.telegram.org/api/animated-emojis">Animated
* emojis</a> and
* <a href="https://corefork.telegram.org/api/dice">animated
* dice</a> should be scaled by this factor before being shown
* to the user (float)
*/
emojies_animated_zoom?: number
/**
* Whether app clients should start a keepalive service to keep
* the app running and fetch updates even when the app is
* closed (boolean)
*/
keep_alive_service?: boolean
/**
* Whether app clients should start a background TCP connection
* for MTProto update fetching (boolean)
*/
background_connection?: boolean
/**
* A list of supported
* <a href="https://corefork.telegram.org/api/dice">animated
* dice</a> stickers (array of strings).
*/
emojies_send_dice?: string[]
/**
* For
* <a href="https://corefork.telegram.org/api/dice">animated
* dice</a> emojis other than the basic <img class="emoji"
* src="//telegram.org/img/emoji/40/F09F8EB2.png" width="20"
* height="20" alt="🎲">, indicates the winning dice value and
* the final frame of the animated sticker, at which to show
* the fireworks <img class="emoji"
* src="//telegram.org/img/emoji/40/F09F8E86.png" width="20"
* height="20" alt="🎆"> (object with emoji keys and object
* values, containing <code>value</code> and
* <code>frame_start</code> float values)
*/
emojies_send_dice_success?: Record<
string,
{
value: number
frame_start: number
}
>
/**
* A map of soundbites to be played when the user clicks on the
* specified
* <a href="https://corefork.telegram.org/api/animated-emojis">animated
* emoji</a>; the
* <a href="https://corefork.telegram.org/api/file_reference">file
* reference field</a> should be base64-decoded before
* <a href="https://corefork.telegram.org/api/files">downloading
* the file</a> (map of
* <a href="https://corefork.telegram.org/api/files">file
* IDs</a> ({@link RawInputDocument}.id), with emoji string
* keys)
*/
emojies_sounds?: Record<
string,
{
id: string
access_hash: string
file_reference_base64: string
}
>
/**
* Specifies the name of the service providing GIF search
* through
* <a href="#mtproto-configuration">gif_search_username</a>
* (string)
*/
gif_search_branding?: string
/**
* Specifies a list of emojis that should be suggested as
* search term in a bar above the GIF search box (array of
* string emojis)
*/
gif_search_emojies?: string[]
/**
* Specifies that the app should not display
* <a href="https://corefork.telegram.org/api/stickers#sticker-suggestions">local
* sticker suggestions »</a> for emojis at all and just use the
* result of {@link messages.RawGetStickersRequest} (bool)
*/
stickers_emoji_suggest_only_api?: boolean
/**
* Specifies the validity period of the local cache of
* {@link messages.RawGetStickersRequest}, also relevant when
* generating the
* <a href="https://corefork.telegram.org/api/offsets#hash-generation">pagination
* hash</a> when invoking the method. (integer)
*/
stickers_emoji_cache_time?: number
/**
* Whether the Settings-&gt;Devices menu should show an option
* to scan a
* <a href="https://corefork.telegram.org/api/qr-login">QR
* login code</a> (boolean)
*/
qr_login_camera?: boolean
/**
* Whether the login screen should show a
* <a href="https://corefork.telegram.org/api/qr-login">QR code
* login option</a>, possibly as default login method (string,
* "disabled", "primary" or "secondary")
*/
qr_login_code?: 'disabled' | 'primary' | 'secondary'
/**
* Whether clients should show an option for managing
* <a href="https://corefork.telegram.org/api/folders">dialog
* filters AKA folders</a> (boolean)
*/
dialog_filters_enabled?: boolean
/**
* Whether clients should actively show a tooltip, inviting the
* user to configure
* <a href="https://corefork.telegram.org/api/folders">dialog
* filters AKA folders</a>; typically this happens when the
* chat list is long enough to start getting cluttered.
* (boolean)
*/
dialog_filters_tooltip?: boolean
/**
* Whether clients <em>can</em> invoke
* {@link account.RawSetGlobalPrivacySettingsRequest} with
* {@link RawGlobalPrivacySettings}, to automatically archive
* and mute new incoming chats from non-contacts. (boolean)
*/
autoarchive_setting_available?: boolean
/**
* Contains a list of suggestions that should be actively shown
* as a tooltip to the user. (Array of strings, possible values
* shown <a href="#suggestions">in the suggestions section
* »</a>.
*/
pending_suggestions?: string[]
/**
* Maximum number of
* <a href="https://corefork.telegram.org/api/forum#forum-topics">topics</a>
* that can be pinned in a single
* <a href="https://corefork.telegram.org/api/forum">forum</a>.
* (integer)
*/
topics_pinned_limit?: number
/**
* The ID of the official
* <a href="https://corefork.telegram.org/api/antispam">native
* antispam bot</a>, that will automatically delete spam
* messages if enabled as specified in the
* <a href="https://corefork.telegram.org/api/antispam">native
* antispam documentation »</a>.
*
*
* When fetching the admin list of a supergroup using
* {@link channels.RawGetParticipantsRequest}, if native
* antispam functionality in the specified supergroup, the bot
* should be manually added to the admin list displayed to the
* user. (numeric string that represents a Telegram user/bot
* ID, should be casted to an int64)
*/
telegram_antispam_user_id?: string
/**
* Minimum number of group members required to enable
* <a href="https://corefork.telegram.org/api/antispam">native
* antispam functionality</a>. (integer)
*/
telegram_antispam_group_size_min?: number
/**
* List of phone number prefixes for anonymous
* <a href="https://fragment.com/">Fragment</a> phone numbers.
* (array of strings).
*/
fragment_prefixes?: string[]
/**
* Minimum number of participants required to hide the
* participants list of a supergroup using
* {@link channels.RawToggleParticipantsHiddenRequest}.
* (integer)
*/
hidden_members_group_size_min?: number
/**
* A list of domains that support automatic login with manual
* user confirmation,
* <a href="https://corefork.telegram.org/api/url-authorization#link-url-authorization">click
* here for more info on URL authorization »</a>. (array of
* strings)
*/
url_auth_domains?: string[]
/**
* A list of Telegram domains that support automatic login with
* no user confirmation,
* <a href="https://corefork.telegram.org/api/url-authorization#link-url-authorization">click
* here for more info on URL authorization »</a>. (array of
* strings)
*/
autologin_domains?: string[]
/**
* A list of Telegram domains that can always be opened without
* additional user confirmation, when clicking on in-app links
* where the URL is not fully displayed (i.e.
* {@link RawMessageEntityTextUrl} entities). (array of
* strings)Note that when opening
* <a href="https://corefork.telegram.org/api/links#named-mini-app-links">named
* Mini App links</a> for the first time, confirmation should
* still be requested from the user, even if the domain of the
* containing deep link is whitelisted (i.e.
* <code>t.me/&lt;bot_username&gt;/&lt;short_name&gt;?startapp=&lt;start_parameter&gt;</code>,
* where <code>t.me</code> is whitelisted). Confirmation
* should <strong>always</strong> be asked, even if we already
* opened the
* <a href="https://corefork.telegram.org/api/links#named-mini-app-links">named
* Mini App</a> before, if the link is not visible (i.e.
* {@link RawMessageEntityTextUrl} text links, inline buttons
* etc.).
*/
whitelisted_domains?: string[]
/**
* Contains a set of recommended codec parameters for round
* videos. (object, as described in the example)
*/
round_video_encoding?: {
diameter: number
video_bitrate: number
audio_bitrate: number
max_size: number
}
/**
* Per-user read receipts, fetchable using
* {@link messages.RawGetMessageReadParticipantsRequest}, will
* be available in groups with an amount of participants less
* or equal to <code>chat_read_mark_size_threshold</code>.
* (integer)
*/
chat_read_mark_size_threshold?: number
/**
* To protect user privacy, read receipts for chats are only
* stored for <code>chat_read_mark_expire_period</code> seconds
* after the message was sent. (integer)
*/
chat_read_mark_expire_period?: number
/**
* To protect user privacy, read receipts for private chats are
* only stored for <code>pm_read_date_expire_period</code>
* seconds after the message was sent. (integer)
*/
pm_read_date_expire_period?: number
/**
* Maximum number of participants in a group call (livestreams
* allow participants) (integer)
*/
groupcall_video_participants_max?: number
/**
* Maximum number of unique reactions for any given message:
* for example, if there are 2000 <img class="emoji"
* src="//telegram.org/img/emoji/40/F09F918D.png" width="20"
* height="20" alt="👍"> and 1000 custom emoji <img
* class="emoji" src="//telegram.org/img/emoji/40/F09F9881.png"
* width="20" height="20" alt="😁"> reactions and
* reactions_uniq_max = 2, you can't add a <img class="emoji"
* src="//telegram.org/img/emoji/40/F09F918E.png" width="20"
* height="20" alt="👎"> reaction, because that would raise the
* number of unique reactions to 3 &gt; 2. (integer)
*/
reactions_uniq_max?: number
/**
* Maximum number of reactions that can be marked as allowed in
* a chat using {@link RawChatReactionsSome}. (integer)
*/
reactions_in_chat_max?: number
/**
* Maximum number of reactions that can be added to a single
* message by a non-Premium user. (integer)
*/
reactions_user_max_default?: number
/**
* Maximum number of reactions that can be added to a single
* message by a Premium user. (integer)
*/
reactions_user_max_premium?: number
/**
* Default emoji status stickerset ID. (integer)
*
*
* Note that the stickerset can be fetched using
* {@link RawInputStickerSetEmojiDefaultStatuses}.
*/
default_emoji_statuses_stickerset_id?: number
/**
* The maximum duration in seconds of
* <a href="https://corefork.telegram.org/api/ringtones">uploadable
* notification sounds »</a> (integer)
*/
ringtone_duration_max?: number
/**
* The maximum post-conversion size in bytes of
* <a href="https://corefork.telegram.org/api/ringtones">uploadable
* notification sounds »</a>
*/
ringtone_size_max?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/ringtones">saveable
* notification sounds »</a>
*/
ringtone_saved_count_max?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/custom-emoji">custom
* emojis</a> that may be present in a message. (integer)
*/
message_animated_emoji_max?: number
/**
* Defines how many
* <a href="https://corefork.telegram.org/api/premium">Premium
* stickers</a> to show in the sticker suggestion popup when
* entering an emoji into the text field, see the
* <a href="https://corefork.telegram.org/api/stickers#sticker-suggestions">sticker
* docs for more info</a> (integer, defaults to 0)
*/
stickers_premium_by_emoji_num?: number
/**
* For
* <a href="https://corefork.telegram.org/api/premium">Premium
* users</a>, used to define the suggested sticker list, see
* the
* <a href="https://corefork.telegram.org/api/stickers#sticker-suggestions">sticker
* docs for more info</a> (integer, defaults to 2)
*/
stickers_normal_by_emoji_per_premium_num?: number
/**
* The user can't purchase
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a>. The app must also hide all Premium features,
* including stars for other users, et cetera. (boolean)
*/
premium_purchase_blocked?: boolean
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/channel">channels
* and supergroups</a> a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may join (integer)
*/
channels_limit_default?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/channel">channels
* and supergroups</a> a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may join (integer)
*/
channels_limit_premium?: number
/**
* The maximum number of GIFs a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may save (integer)
*/
saved_gifs_limit_default?: number
/**
* The maximum number of GIFs a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may save (integer)
*/
saved_gifs_limit_premium?: number
/**
* The maximum number of stickers a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may
* <a href="https://corefork.telegram.org/api/stickers#favorite-stickersets">add
* to Favorites »</a> (integer)
*/
stickers_faved_limit_default?: number
/**
* The maximum number of stickers a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may
* <a href="https://corefork.telegram.org/api/stickers#favorite-stickersets">add
* to Favorites »</a> (integer)
*/
stickers_faved_limit_premium?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/folders">folders</a>
* a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may create (integer)
*/
dialog_filters_limit_default?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/folders">folders</a>
* a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may create (integer)
*/
dialog_filters_limit_premium?: number
/**
* The maximum number of chats a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may add to a
* <a href="https://corefork.telegram.org/api/folders">folder</a>
* (integer)
*/
dialog_filters_chats_limit_default?: number
/**
* The maximum number of chats a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may add to a
* <a href="https://corefork.telegram.org/api/folders">folder</a>
* (integer)
*/
dialog_filters_chats_limit_premium?: number
/**
* The maximum number of chats a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may pin (integer)
*/
dialogs_pinned_limit_default?: number
/**
* The maximum number of chats a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may pin (integer)
*/
dialogs_pinned_limit_premium?: number
/**
* The maximum number of chats a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may pin in a folder (integer)
*/
dialogs_folder_pinned_limit_default?: number
/**
* The maximum number of chats a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may pin in a folder (integer)
*/
dialogs_folder_pinned_limit_premium?: number
/**
* The maximum number of public
* <a href="https://corefork.telegram.org/api/channel">channels
* or supergroups</a> a
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may create (integer)
*/
channels_public_limit_default?: number
/**
* The maximum number of public
* <a href="https://corefork.telegram.org/api/channel">channels
* or supergroups</a> a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* user may create (integer)
*/
channels_public_limit_premium?: number
/**
* The maximum UTF-8 length of media captions sendable by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer)
*/
caption_length_limit_default?: number
/**
* The maximum UTF-8 length of media captions sendable by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer)
*/
caption_length_limit_premium?: number
/**
* The maximum number of file parts uploadable by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer, the maximum file size can be extrapolated by
* multiplying this value by <code>524288</code>, the biggest
* possible chunk size)
*/
upload_max_fileparts_default?: number
/**
* The maximum number of file parts uploadable by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer, the maximum file size can be extrapolated by
* multiplying this value by <code>524288</code>, the biggest
* possible chunk size)
*/
upload_max_fileparts_premium?: number
/**
* The maximum UTF-8 length of bios of
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer)
*/
about_length_limit_default?: number
/**
* The maximum UTF-8 length of bios of
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer)
*/
about_length_limit_premium?: number
/**
* Array of string identifiers, indicating the order of
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> features in the Telegram Premium promotion
* popup,
* <a href="https://corefork.telegram.org/api/premium#telegram-premium-features">see
* here for the possible values »</a>
*/
premium_promo_order?: string[]
/**
* Contains the username of the official
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> bot that may be used to buy a
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> subscription, see
* <a href="https://corefork.telegram.org/api/premium">here for
* detailed instructions »</a> (string)
*/
premium_bot_username?: string
/**
* Contains an
* <a href="https://corefork.telegram.org/api/payments">invoice
* slug</a> that may be used to buy a
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> subscription, see
* <a href="https://corefork.telegram.org/api/premium">here for
* detailed instructions »</a> (string)
*/
premium_invoice_slug?: string
/**
* Whether a gift icon should be shown in the attachment menu
* in private chats with users, offering the current user to
* gift a
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> subscription to the other user in the chat.
* (boolean)
*/
premium_gift_attach_menu_icon?: boolean
/**
* Whether a gift icon should be shown in the text bar in
* private chats with users (ie like the <code>/</code> icon in
* chats with bots), offering the current user to gift a
* <a href="https://corefork.telegram.org/api/premium">Telegram
* Premium</a> subscription to the other user in the chat. Can
* only be true if <code>premium_gift_attach_menu_icon</code>
* is also true. (boolean)
*/
premium_gift_text_field_icon?: boolean
/**
* Users that import a folder using a
* <a href="https://corefork.telegram.org/api/links#chat-folder-links">chat
* folder deep link »</a> should retrieve additions made to the
* folder by invoking
* {@link chatlists.RawGetChatlistUpdatesRequest} at most every
* <code>chatlist_update_period</code> seconds. (integer)
*/
chatlist_update_period?: number
/**
* Maximum number of per-folder
* <a href="https://corefork.telegram.org/api/links#chat-folder-links">chat
* folder deep links »</a> that can be created by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
chatlist_invites_limit_default?: number
/**
* Maximum number of per-folder
* <a href="https://corefork.telegram.org/api/links#chat-folder-links">chat
* folder deep links »</a> that can be created by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
chatlist_invites_limit_premium?: number
/**
* Maximum number of
* <a href="https://corefork.telegram.org/api/links#chat-folder-links">shareable
* folders</a>
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users may have. (integer)
*/
chatlists_joined_limit_default?: number
/**
* Maximum number of
* <a href="https://corefork.telegram.org/api/links#chat-folder-links">shareable
* folders</a>
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users may have. (integer)
*/
chatlists_joined_limit_premium?: number
/**
* A soft limit, specifying the maximum number of files that
* should be downloaded in parallel from the same DC, for files
* smaller than 20MB. (integer)
*/
small_queue_max_active_operations_count?: number
/**
* A soft limit, specifying the maximum number of files that
* should be downloaded in parallel from the same DC, for files
* bigger than 20MB. (integer)
*/
large_queue_max_active_operations_count?: number
/**
* An
* <a href="https://corefork.telegram.org/api/auth#confirming-login">unconfirmed
* session »</a> will be autoconfirmed this many seconds after
* login. (integer)
*/
authorization_autoconfirm_period?: number
/**
* The exact list of users that viewed the story will be hidden
* from the poster this many seconds after the story expires.
* (integer)This limit applies <strong>only</strong> to
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users,
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users can <strong>always</strong> access the viewer list.
*/
story_viewers_expire_period?: number
/**
* The maximum number of active
* <a href="https://corefork.telegram.org/api/stories">stories</a>
* for
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer).
*/
story_expiring_limit_default?: number
/**
* The maximum number of active
* <a href="https://corefork.telegram.org/api/stories">stories</a>
* for
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users (integer).
*/
story_expiring_limit_premium?: number
/**
* The maximum UTF-8 length of story captions for
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
story_caption_length_limit_premium?: number
/**
* The maximum UTF-8 length of story captions for
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
story_caption_length_limit_default?: number
/**
* Indicates whether users can post stories. (string)One of:
* <li><code>enabled</code> - Any user can post stories.</li>
* <li><code>premium</code> - Only users with a
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* subscription can post stories.</li>
* <li><code>disabled</code> - Users can't post stories.</li>
*
*/
stories_posting?: string
/**
* Enabling
* <a href="https://corefork.telegram.org/api/stories#stealth-mode">stories
* stealth mode</a> with the <code>past</code> flag will erase
* views of any story opened in the past
* <code>stories_stealth_past_period</code> seconds. (integer)
*/
stories_stealth_past_period?: number
/**
* Enabling
* <a href="https://corefork.telegram.org/api/stories#stealth-mode">stories
* stealth mode</a> with the <code>future</code> flag will hide
* views of any story opened in the next
* <code>stories_stealth_future_period</code> seconds.
* (integer)
*/
stories_stealth_future_period?: number
/**
* After enabling
* <a href="https://corefork.telegram.org/api/stories#stealth-mode">stories
* stealth mode</a>, this many seconds must elapse before the
* user is allowed to enable it again. (integer)
*/
stories_stealth_cooldown_period?: number
/**
* Maximum number of stories that can be sent in a week by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_sent_weekly_limit_default?: number
/**
* Maximum number of stories that can be sent in a week by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_sent_weekly_limit_premium?: number
/**
* Maximum number of stories that can be sent in a month by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_sent_monthly_limit_default?: number
/**
* Maximum number of stories that can be sent in a month by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_sent_monthly_limit_premium?: number
/**
* Maximum number of
* <a href="https://corefork.telegram.org/api/stories#media-areas">story
* reaction media areas »</a> that can be added to a story by
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_suggested_reactions_limit_default?: number
/**
* Maximum number of
* <a href="https://corefork.telegram.org/api/stories#media-areas">story
* reaction media areas »</a> that can be added to a story by
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
stories_suggested_reactions_limit_premium?: number
/**
* Username of the inline bot to use to generate venue location
* tags for stories, see
* <a href="https://corefork.telegram.org/api/stories#location-tags">here
* »</a> for more info. (string)
*/
stories_venue_search_username?: string
/**
* ID of the official Telegram user that will post stories
* about new Telegram features: stories posted by this user
* should be shown on the
* <a href="https://corefork.telegram.org/api/stories#watching-stories">active
* or active and hidden stories bar</a> just like for contacts,
* even if the user was removed from the contact list.
* (integer, defaults to <code>777000</code>)
*/
stories_changelog_user_id?: number
/**
* Whether
* <a href="https://corefork.telegram.org/api/entities">styled
* text entities</a> and links in story text captions can be
* used by all users (<code>enabled</code>), only
* [Premium](/api/premium users) (<code>premium</code>), or no
* one (<code>disabled</code>). (string)This field is used both
* when posting stories, to indicate to the user whether they
* can use entities, and when viewing stories, to hide entities
* (client-side) on stories posted by users whose
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* subscription has expired (if <code>stories_entities ==
* "premium"</code> and {@link RawUser}.<code>premium</code> is
* not set, or if <code>stories_entities == "disabled"</code>).
*
*/
stories_entities?: string
/**
* Whether
* <a href="https://corefork.telegram.org/api/giveaways">giveaways</a>
* can be started by the current user. (boolean)
*/
giveaway_gifts_purchase_available?: boolean
/**
* The maximum number of users that can be specified when
* making a
* <a href="https://corefork.telegram.org/api/giveaways">direct
* giveaway</a>. (integer)
*/
giveaway_add_peers_max?: number
/**
* The maximum number of countries that can be specified when
* restricting the set of participating countries in a
* <a href="https://corefork.telegram.org/api/giveaways">giveaway</a>.
* (itneger)
*/
giveaway_countries_max?: number
/**
* The number of
* <a href="https://corefork.telegram.org/api/boost">boosts</a>
* that will be gained by a channel for each winner of a
* <a href="https://corefork.telegram.org/api/giveaways">giveaway</a>.
* (integer)
*/
giveaway_boosts_per_premium?: number
/**
* The maximum duration in seconds of a
* <a href="https://corefork.telegram.org/api/giveaways">giveaway</a>.
* (integer)
*/
giveaway_period_max?: number
/**
* Maximum
* <a href="https://corefork.telegram.org/api/boost">boost
* level</a> for channels. (integer)
*/
boosts_channel_level_max?: number
/**
* The number of additional
* <a href="https://corefork.telegram.org/api/boost">boost
* slots</a> that the current user will receive when
* <a href="https://corefork.telegram.org/api/premium#gifting-telegram-premium">gifting
* a Telegram Premium subscription</a>.
*/
boosts_per_sent_gift?: number
/**
* The maximum number of
* <a href="https://corefork.telegram.org/api/transcribe">speech
* recognition »</a> calls per week for
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
transcribe_audio_trial_weekly_number?: number
/**
* The maximum allowed duration of media in seconds for
* <a href="https://corefork.telegram.org/api/transcribe">speech
* recognition »</a> for
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
transcribe_audio_trial_duration_max?: number
/**
* The maximum number of similar channels that can be
* recommended by
* {@link channels.RawGetChannelRecommendationsRequest} to
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
recommended_channels_limit_default?: number
/**
* The maximum number of similar channels that can be
* recommended by
* {@link channels.RawGetChannelRecommendationsRequest} to
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
recommended_channels_limit_premium?: number
/**
* Maximum UTF-8 length of {@link RawInputReplyToMessage}.
* (integer)
*/
quote_length_max?: number
/**
* After reaching at least this
* <a href="https://corefork.telegram.org/api/boost">boost
* level »</a>, channels gain the ability to change their
* <a href="https://corefork.telegram.org/api/colors">message
* accent palette emoji »</a>. (integer)
*/
channel_bg_icon_level_min?: number
/**
* After reaching at least this
* <a href="https://corefork.telegram.org/api/boost">boost
* level »</a>, channels gain the ability to change their
* <a href="https://corefork.telegram.org/api/colors">profile
* accent palette emoji »</a>. (integer)
*/
channel_profile_bg_icon_level_min?: number
/**
* After reaching at least this
* <a href="https://corefork.telegram.org/api/boost">boost
* level »</a>, channels gain the ability to change their
* <a href="https://corefork.telegram.org/api/emoji-status">status
* emoji »</a>. (integer)
*/
channel_emoji_status_level_min?: number
/**
* After reaching at least this
* <a href="https://corefork.telegram.org/api/boost">boost
* level »</a>, channels gain the ability to set a
* <a href="https://corefork.telegram.org/api/wallpapers#channel-wallpapers">fill
* channel wallpaper, see here » for more info</a>. (integer)
*/
channel_wallpaper_level_min?: number
/**
* After reaching at least this
* <a href="https://corefork.telegram.org/api/boost">boost
* level »</a>, channels gain the ability to set any custom
* <a href="https://corefork.telegram.org/api/wallpapers">wallpaper</a>,
* not just
* <a href="https://corefork.telegram.org/api/wallpapers">fill
* channel wallpapers, see here » for more info</a>. (integer)
*/
channel_custom_wallpaper_level_min?: number
/**
* Maximum number of pinned dialogs in
* <a href="https://corefork.telegram.org/api/saved-messages">saved
* messages</a> for
* non-<a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
saved_dialogs_pinned_limit_default?: number
/**
* Maximum number of pinned dialogs in
* <a href="https://corefork.telegram.org/api/saved-messages">saved
* messages</a> for
* <a href="https://corefork.telegram.org/api/premium">Premium</a>
* users. (integer)
*/
saved_dialogs_pinned_limit_premium?: number
[key: string]: unknown
}

View file

@ -1,3 +1,4 @@
export * from './app-config.js'
export * from './entities.js' export * from './entities.js'
export * from './input-privacy-rule.js' export * from './input-privacy-rule.js'
export * from './sticker-set.js' export * from './sticker-set.js'

View file

@ -1167,7 +1167,7 @@ export class UpdatesManager {
const config = client.mt.network.config.getNow() const config = client.mt.network.config.getNow()
if (config) { if (config) {
client.mt.network.config.setConfig({ client.mt.network.config.setData({
...config, ...config,
dcOptions: upd.dcOptions, dcOptions: upd.dcOptions,
}) })

View file

@ -0,0 +1,12 @@
import { PublicPart } from '../../types/utils.js'
import { AppConfigManager } from '../managers/app-config-manager.js'
import { WorkerInvoker } from './invoker.js'
export class AppConfigManagerProxy implements PublicPart<AppConfigManager> {
constructor(readonly invoker: WorkerInvoker) {}
private _bind = this.invoker.makeBinder<AppConfigManager>('app-config')
readonly get = this._bind('get')
readonly getField = this._bind('getField')
}

View file

@ -4,6 +4,7 @@ import { LogManager } from '../../utils/logger.js'
import { ITelegramClient } from '../client.types.js' import { ITelegramClient } from '../client.types.js'
import { PeersIndex } from '../types/peers/peers-index.js' import { PeersIndex } from '../types/peers/peers-index.js'
import { RawUpdateHandler } from '../updates/types.js' import { RawUpdateHandler } from '../updates/types.js'
import { AppConfigManagerProxy } from './app-config.js'
import { WorkerInvoker } from './invoker.js' import { WorkerInvoker } from './invoker.js'
import { connectToWorker } from './platform/connect.js' import { connectToWorker } from './platform/connect.js'
import { ClientMessageHandler, SomeWorker, WorkerCustomMethods } from './protocol.js' import { ClientMessageHandler, SomeWorker, WorkerCustomMethods } from './protocol.js'
@ -64,6 +65,7 @@ export class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements I
private _bind = this._invoker.makeBinder<ITelegramClient>('client') private _bind = this._invoker.makeBinder<ITelegramClient>('client')
readonly storage = new TelegramStorageProxy(this._invoker) readonly storage = new TelegramStorageProxy(this._invoker)
readonly appConfig = new AppConfigManagerProxy(this._invoker)
private _destroyed = false private _destroyed = false
destroy(terminate = false): void { destroy(terminate = false): void {

View file

@ -4,45 +4,39 @@ import { tl } from '@mtcute/tl'
import { SerializedError } from './errors.js' import { SerializedError } from './errors.js'
export type WorkerInboundMessage = export type WorkerInboundMessage = {
| { type: 'invoke'
type: 'invoke' id: number
id: number target: 'custom' | 'client' | 'storage' | 'storage-self' | 'storage-peers' | 'app-config'
target: method: string
| 'custom' args: unknown[]
| 'client' void: boolean
| 'storage' }
| 'storage-self'
| 'storage-peers'
method: string
args: unknown[]
void: boolean
}
export type WorkerOutboundMessage = export type WorkerOutboundMessage =
| { type: 'server_update'; update: tl.TypeUpdates } | { type: 'server_update'; update: tl.TypeUpdates }
| { | {
type: 'update' type: 'update'
update: tl.TypeUpdate update: tl.TypeUpdate
users: Map<number, tl.TypeUser> users: Map<number, tl.TypeUser>
chats: Map<number, tl.TypeChat> chats: Map<number, tl.TypeChat>
hasMin: boolean hasMin: boolean
} }
| { type: 'error'; error: unknown } | { type: 'error'; error: unknown }
| { | {
type: 'log' type: 'log'
color: number color: number
level: number level: number
tag: string tag: string
fmt: string fmt: string
args: unknown[] args: unknown[]
} }
| { | {
type: 'result' type: 'result'
id: number id: number
result?: unknown result?: unknown
error?: SerializedError error?: SerializedError
} }
export type SomeWorker = NodeWorker | Worker | SharedWorker export type SomeWorker = NodeWorker | Worker | SharedWorker

View file

@ -33,6 +33,9 @@ export function makeTelegramWorker<T extends WorkerCustomMethods>(params: Telegr
case 'storage-peers': case 'storage-peers':
target = client.storage.peers target = client.storage.peers
break break
case 'app-config':
target = client.appConfig
break
default: { default: {
respond({ respond({

View file

@ -48,7 +48,7 @@ describe('ConfigManager', () => {
const cm = new ConfigManager(getConfig) const cm = new ConfigManager(getConfig)
expect(cm.isStale).toBe(true) expect(cm.isStale).toBe(true)
cm.setConfig(config) cm.setData(config)
expect(cm.isStale).toBe(false) expect(cm.isStale).toBe(false)
vi.setSystemTime(300_000) vi.setSystemTime(300_000)
@ -69,8 +69,14 @@ describe('ConfigManager', () => {
const cm = new ConfigManager(getConfig) const cm = new ConfigManager(getConfig)
await cm.update() await cm.update()
vi.setSystemTime(300_000) getConfig.mockImplementation(() =>
Promise.resolve({
...config,
expires: 600,
}),
)
getConfig.mockClear() getConfig.mockClear()
await vi.advanceTimersByTimeAsync(301_000)
await Promise.all([cm.update(), cm.update()]) await Promise.all([cm.update(), cm.update()])
expect(getConfig).toHaveBeenCalledOnce() expect(getConfig).toHaveBeenCalledOnce()
@ -79,11 +85,11 @@ describe('ConfigManager', () => {
it('should call listeners on config update', async () => { it('should call listeners on config update', async () => {
const cm = new ConfigManager(getConfig) const cm = new ConfigManager(getConfig)
const listener = vi.fn() const listener = vi.fn()
cm.onConfigUpdate(listener) cm.onReload(listener)
await cm.update() await cm.update()
vi.setSystemTime(300_000) vi.setSystemTime(300_000)
cm.offConfigUpdate(listener) cm.onReload(listener)
await cm.update() await cm.update()
expect(listener).toHaveBeenCalledOnce() expect(listener).toHaveBeenCalledOnce()

View file

@ -1,74 +1,19 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { Reloadable } from '../utils/reloadable.js'
/** /**
* Config manager is responsible for keeping * Config manager is responsible for keeping
* the current server configuration up-to-date * the current server configuration up-to-date
* and providing methods to find the best DC * and providing methods to find the best DC
* option for the current session. * option for the current session.
*/ */
export class ConfigManager { export class ConfigManager extends Reloadable<tl.RawConfig> {
constructor(private _update: () => Promise<tl.RawConfig>) {} constructor(update: () => Promise<tl.RawConfig>) {
super({
private _destroyed = false reload: update,
private _config?: tl.RawConfig getExpiresAt: (data) => data.expires * 1000,
private _cdnConfig?: tl.RawCdnConfig })
private _updateTimeout?: NodeJS.Timeout
private _updatingPromise?: Promise<void>
private _listeners: ((config: tl.RawConfig) => void)[] = []
get isStale(): boolean {
return !this._config || this._config.expires <= Date.now() / 1000
}
update(force = false): Promise<void> {
if (!force && !this.isStale) return Promise.resolve()
if (this._updatingPromise) return this._updatingPromise
return (this._updatingPromise = this._update().then((config) => {
if (this._destroyed) return
this._updatingPromise = undefined
this.setConfig(config)
}))
}
setConfig(config: tl.RawConfig): void {
this._config = config
if (this._updateTimeout) clearTimeout(this._updateTimeout)
this._updateTimeout = setTimeout(
() => void this.update().catch(() => {}),
(config.expires - Date.now() / 1000) * 1000,
)
for (const cb of this._listeners) cb(config)
}
onConfigUpdate(cb: (config: tl.RawConfig) => void): void {
this._listeners.push(cb)
}
offConfigUpdate(cb: (config: tl.RawConfig) => void): void {
const idx = this._listeners.indexOf(cb)
if (idx >= 0) this._listeners.splice(idx, 1)
}
getNow(): tl.RawConfig | undefined {
return this._config
}
async get(): Promise<tl.RawConfig> {
if (this.isStale) await this.update()
return this._config!
}
destroy(): void {
if (this._updateTimeout) clearTimeout(this._updateTimeout)
this._listeners.length = 0
this._destroyed = true
} }
async findOption(params: { async findOption(params: {
@ -81,7 +26,7 @@ export class ConfigManager {
}): Promise<tl.RawDcOption | undefined> { }): Promise<tl.RawDcOption | undefined> {
if (this.isStale) await this.update() if (this.isStale) await this.update()
const options = this._config!.dcOptions.filter((opt) => { const options = this._data!.dcOptions.filter((opt) => {
if (opt.tcpoOnly) return false // unsupported if (opt.tcpoOnly) return false // unsupported
if (opt.ipv6 && !params.allowIpv6) return false if (opt.ipv6 && !params.allowIpv6) return false
if (opt.mediaOnly && !params.allowMedia) return false if (opt.mediaOnly && !params.allowMedia) return false

View file

@ -3,7 +3,14 @@ import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { StorageManager } from '../storage/storage.js' import { StorageManager } from '../storage/storage.js'
import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js' import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js'
import { ControllablePromise, createControllablePromise, DcOptions, ICryptoProvider, Logger, sleep } from '../utils/index.js' import {
ControllablePromise,
createControllablePromise,
DcOptions,
ICryptoProvider,
Logger,
sleep,
} from '../utils/index.js'
import { assertTypeIs } from '../utils/type-assertions.js' import { assertTypeIs } from '../utils/type-assertions.js'
import { ConfigManager } from './config-manager.js' import { ConfigManager } from './config-manager.js'
import { MultiSessionConnection } from './multi-session-connection.js' import { MultiSessionConnection } from './multi-session-connection.js'
@ -462,7 +469,7 @@ export class NetworkManager {
this._updateHandler = params.onUpdate this._updateHandler = params.onUpdate
this._onConfigChanged = this._onConfigChanged.bind(this) this._onConfigChanged = this._onConfigChanged.bind(this)
config.onConfigUpdate(this._onConfigChanged) config.onReload(this._onConfigChanged)
} }
private async _findDcOptions(dcId: number): Promise<DcOptions> { private async _findDcOptions(dcId: number): Promise<DcOptions> {
@ -627,8 +634,7 @@ export class NetworkManager {
} else { } else {
if (auth.bot) { if (auth.bot) {
// bots may receive tmpSessions, which we should respect // bots may receive tmpSessions, which we should respect
this.config.update(true) this.config.update(true).catch((e: Error) => this.params.emitError(e))
.catch((e: Error) => this.params.emitError(e))
} }
user = auth user = auth
@ -837,6 +843,6 @@ export class NetworkManager {
for (const dc of this._dcConnections.values()) { for (const dc of this._dcConnections.values()) {
dc.destroy() dc.destroy()
} }
this.config.offConfigUpdate(this._onConfigChanged) this.config.offReload(this._onConfigChanged)
} }
} }

View file

@ -0,0 +1,77 @@
import { asyncResettable } from './function-utils.js'
export interface ReloadableParams<Data> {
reload: (old?: Data) => Promise<Data>
getExpiresAt: (data: Data) => number
onError?: (err: unknown) => void
disableAutoReload?: boolean
}
export class Reloadable<Data> {
constructor(readonly params: ReloadableParams<Data>) {}
protected _data?: Data
protected _expiresAt = 0
protected _listeners: ((data: Data) => void)[] = []
protected _timeout?: NodeJS.Timeout
private _reload = asyncResettable(async () => {
const data = await this.params.reload(this._data)
this.setData(data)
this._listeners.forEach((cb) => cb(data))
})
get isStale(): boolean {
return !this._data || this._expiresAt <= Date.now()
}
setData(data: Data): void {
const expiresAt = this.params.getExpiresAt(data)
this._data = data
this._expiresAt = expiresAt
if (this._timeout) clearTimeout(this._timeout)
if (!this.params.disableAutoReload) {
this._timeout = setTimeout(() => {
this._reload.reset()
this.update().catch((err: unknown) => {
this.params.onError?.(err)
})
}, expiresAt - Date.now())
}
}
update(force = false): Promise<void> {
if (!force && !this.isStale) return Promise.resolve()
return this._reload.run()
}
onReload(cb: (data: Data) => void): void {
this._listeners.push(cb)
}
offReload(cb: (data: Data) => void): void {
const idx = this._listeners.indexOf(cb)
if (idx >= 0) this._listeners.splice(idx, 1)
}
getNow(): Data | undefined {
return this._data
}
async get(): Promise<Data> {
await this.update()
return this._data!
}
destroy(): void {
if (this._timeout) clearTimeout(this._timeout)
this._listeners.length = 0
this._reload.reset()
}
}

File diff suppressed because one or more lines are too long

View file

@ -9,6 +9,7 @@ export const API_SCHEMA_JSON_FILE = join(__dirname, '../api-schema.json')
export const API_SCHEMA_DIFF_JSON_FILE = join(__dirname, '../diff.json') export const API_SCHEMA_DIFF_JSON_FILE = join(__dirname, '../diff.json')
export const MTP_SCHEMA_JSON_FILE = join(__dirname, '../mtp-schema.json') export const MTP_SCHEMA_JSON_FILE = join(__dirname, '../mtp-schema.json')
export const ERRORS_JSON_FILE = join(__dirname, '../raw-errors.json') export const ERRORS_JSON_FILE = join(__dirname, '../raw-errors.json')
export const APP_CONFIG_JSON_FILE = join(__dirname, '../app-config.json')
export const CORE_DOMAIN = 'https://core.telegram.org' export const CORE_DOMAIN = 'https://core.telegram.org'
export const COREFORK_DOMAIN = 'https://corefork.telegram.org' export const COREFORK_DOMAIN = 'https://corefork.telegram.org'

View file

@ -7,6 +7,7 @@ import { createInterface } from 'readline'
import { import {
camelToPascal, camelToPascal,
jsComment,
PRIMITIVE_TO_TS, PRIMITIVE_TO_TS,
snakeToCamel, snakeToCamel,
splitNameToNamespace, splitNameToNamespace,
@ -16,6 +17,7 @@ import {
import { import {
API_SCHEMA_JSON_FILE, API_SCHEMA_JSON_FILE,
APP_CONFIG_JSON_FILE,
BLOGFORK_DOMAIN, BLOGFORK_DOMAIN,
CORE_DOMAIN, CORE_DOMAIN,
COREFORK_DOMAIN, COREFORK_DOMAIN,
@ -98,6 +100,13 @@ function extractDescription($: cheerio.CheerioAPI) {
.trim() .trim()
} }
function htmlAll($: cheerio.CheerioAPI, search: cheerio.Cheerio<cheerio.Element>) {
return search
.get()
.map((el) => $(el).html() ?? '')
.join('')
}
// from https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json // from https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json
const PROGRESS_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] const PROGRESS_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
@ -127,6 +136,168 @@ async function chooseDomainForDocs(headers: Record<string, string>): Promise<[nu
return [maxLayer, maxDomain] return [maxLayer, maxDomain]
} }
function lastParensGroup(text: string): string | undefined {
const groups = []
let depth = 0
let current = ''
for (let i = 0; i < text.length; i++) {
if (text[i] === ')') depth--
if (depth > 0) {
current += text[i]
}
if (text[i] === '(') depth++
if (current && depth === 0) {
groups.push(current)
current = ''
}
}
return groups[groups.length - 1]
}
async function fetchAppConfigDocumentation() {
const headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/87.0.4280.88 Safari/537.36',
}
const [, domain] = await chooseDomainForDocs(headers)
const page = await fetchRetry(`${domain}/api/config`, { headers })
const $ = cheerio.load(page)
const fields = $('p:icontains(typical fields included)').nextUntil('h3')
normalizeLinks(`${domain}/api/config`, fields)
const fieldNames = fields.filter('h4')
const _example = $('p:icontains(example value)').next('pre').find('code')
const example = JSON.parse(_example.text().trim()) as Record<string, unknown>
const result: Record<string, unknown> = {}
function valueToTypescript(value: unknown, record = false): string {
if (value === undefined) return 'unknown'
if (value === null) return 'null'
if (Array.isArray(value)) {
const types = new Set(value.map((v) => typeof v))
if (types.size === 1) {
return valueToTypescript(value[0]) + '[]'
}
return `(${value.map((v) => valueToTypescript(v)).join(' | ')})[]`
}
if (typeof value === 'object') {
if (record) {
const inner = Object.values(value)[0] as unknown
return `Record<string, ${valueToTypescript(inner)}>`
}
return (
'{\n' +
Object.entries(value)
.map(([k, v]) => ` ${k}: ${valueToTypescript(v)}`)
.join('\n') +
'\n}'
)
}
return typeof value
}
function docsTypeToTypescript(field: string, type: string): string {
let m
if ((m = type.match(/(.*), defaults to .+$/i))) {
return docsTypeToTypescript(field, m[1])
}
if ((m = type.match(/^(?:array of )(.+?)s?$/i))) {
return docsTypeToTypescript(field, m[1]) + '[]'
}
switch (type) {
case 'integer':
return 'number'
case 'itneger':
return 'number'
case 'float':
return 'number'
case 'string':
return 'string'
case 'string emoji':
return 'string'
case 'boolean':
return 'boolean'
case 'bool':
return 'boolean'
}
if (type.match(/^object with .+? keys|^map of/i)) {
return valueToTypescript(example[field], true)
}
if (type.match(/^strings?, /)) {
if (type.includes('or')) {
const options = type.slice(8).split(/, | or /)
return options.map((o) => (o[0] === '"' ? o : JSON.stringify(o))).join(' | ')
}
return 'string'
}
if (type.includes(',')) {
return docsTypeToTypescript(field, type.split(',')[0])
}
if (type.match(/^numeric string/)) {
return 'string'
}
if (type.includes('as described')) {
return valueToTypescript(example[field])
}
console.log(`Failed to parse type at ${field}: ${type}`)
return valueToTypescript(example[field])
}
for (const fieldName of fieldNames.toArray()) {
const name = $(fieldName).text().trim()
const description = htmlAll($, $(fieldName).nextUntil('h3, h4'))
let type = 'unknown'
let typeStr = lastParensGroup(description)
if (!typeStr) {
typeStr = description.match(/\s+\((.+?)(?:\)|\.|\)\.)$/)?.[1]
}
if (typeStr) {
type = docsTypeToTypescript(name, typeStr)
} else if (name in example) {
type = valueToTypescript(example[name])
}
result[name] = {
type,
description: jsComment(description),
}
}
return result
}
export async function fetchDocumentation( export async function fetchDocumentation(
schema: TlFullSchema, schema: TlFullSchema,
layer: number, layer: number,
@ -366,10 +537,11 @@ async function main() {
console.log('1. Update documentation') console.log('1. Update documentation')
console.log('2. Apply descriptions.yaml') console.log('2. Apply descriptions.yaml')
console.log('3. Apply documentation to schema') console.log('3. Apply documentation to schema')
console.log('4. Fetch app config documentation')
const act = parseInt(await input('[0-3] > ')) const act = parseInt(await input('[0-4] > '))
if (isNaN(act) || act < 0 || act > 3) { if (isNaN(act) || act < 0 || act > 4) {
console.log('Invalid action') console.log('Invalid action')
continue continue
} }
@ -412,15 +584,20 @@ async function main() {
applyDocumentation(schema, cached) applyDocumentation(schema, cached)
await writeFile(API_SCHEMA_JSON_FILE, JSON.stringify(packTlSchema(schema, layer))) await writeFile(API_SCHEMA_JSON_FILE, JSON.stringify(packTlSchema(schema, layer)))
} }
if (act === 4) {
const appConfig = await fetchAppConfigDocumentation()
console.log('Fetched app config documentation')
await writeFile(APP_CONFIG_JSON_FILE, JSON.stringify(appConfig))
}
} }
} }
if (import.meta.url.startsWith('file:')) { if (import.meta.url.startsWith('file:')) {
// (A)
const modulePath = fileURLToPath(import.meta.url) const modulePath = fileURLToPath(import.meta.url)
if (process.argv[1] === modulePath) { if (process.argv[1] === modulePath) {
// (B)
main().catch((err) => { main().catch((err) => {
console.error(err) console.error(err)
process.exit(1) process.exit(1)