chore!: started extracting platform-specific stuff into separate packages

This commit is contained in:
alina 🌸 2024-02-28 00:33:23 +03:00
parent ceb606a347
commit a2739b678c
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
160 changed files with 1452 additions and 772 deletions

View file

@ -254,7 +254,7 @@ module.exports = {
},
},
{
files: ['**/scripts/**', '*.test.ts', 'packages/create-*/**', '**/build.config.cjs'],
files: ['**/scripts/**', '*.test.ts', 'packages/create-*/**', '**/build.config.cjs', 'packages/node/**'],
rules: {
'no-console': 'off',
'no-restricted-imports': [
@ -273,7 +273,7 @@ module.exports = {
},
},
{
files: ['e2e/**'],
files: ['e2e/**', 'packages/node/**'],
rules: {
'no-restricted-globals': 'off',
},

View file

@ -12,18 +12,12 @@
"gen-client": "node ./scripts/generate-client.cjs",
"gen-updates": "node ./scripts/generate-updates.cjs"
},
"browser": {
"./src/utils/platform/crypto.js": "./src/utils/platform/crypto.web.js",
"./src/utils/platform/transport.js": "./src/utils/platform/transport.web.js",
"./src/utils/platform/logging.js": "./src/utils/platform/logging.web.js",
"./src/utils/platform/random.js": "./src/utils/platform/random.web.js",
"./src/utils/platform/exit-hook.js": "./src/utils/platform/exit-hook.web.js",
"./src/highlevel/worker/platform/connect.js": "./src/highlevel/worker/platform/connect.web.js",
"./src/highlevel/worker/platform/register.js": "./src/highlevel/worker/platform/register.web.js",
"./src/highlevel/methods/files/_platform.js": "./src/highlevel/methods/files/_platform.web.js",
"./src/highlevel/methods/files/download-file.js": "./src/highlevel/methods/files/download-file.web.js",
"./src/highlevel/utils/platform/storage.js": "./src/highlevel/utils/platform/storage.web.js",
"./src/storage/json-file.js": false
"exports": {
".": "./src/index.ts",
"./utils.js": "./src/utils/index.ts",
"./client.js": "./src/highlevel/client.ts",
"./methods.js": "./src/highlevel/methods.ts",
"./platform.js": "./src/platform.ts"
},
"distOnlyFields": {
"exports": {
@ -35,25 +29,17 @@
"import": "./esm/utils/index.js",
"require": "./cjs/utils/index.js"
},
"./utils/crypto/*": {
"import": "./esm/utils/crypto/*",
"require": "./cjs/utils/crypto/*"
},
"./network/transports/*": {
"import": "./esm/network/transports/*",
"require": "./cjs/network/transports/*"
},
"./storage/*": {
"import": "./esm/storage/*",
"require": "./cjs/storage/*"
},
"./highlevel/*": {
"import": "./esm/highlevel/*",
"require": "./cjs/highlevel/*"
},
"./methods.js": {
"import": "./esm/highlevel/methods.js",
"require": "./cjs/highlevel/methods.js"
},
"./platform.js": {
"import": "./esm/platform.js",
"require": "./cjs/platform.js"
},
"./client.js": {
"import": "./esm/highlevel/client.js",
"require": "./cjs/highlevel/client.js"
}
}
},

View file

@ -273,6 +273,7 @@ async function addSingleMethod(state, fileName) {
}
const isExported = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
const isDeclare = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword)
const isInitialize = checkForFlag(stmt, '@initialize')
const isManualImpl = checkForFlag(stmt, '@manual-impl')
const isInitializeSuper = isInitialize === 'super'
@ -327,7 +328,7 @@ async function addSingleMethod(state, fileName) {
})
}
if (!isExported) continue
if (!isExported && !isDeclare) continue
const firstArg = stmt.parameters[0]
@ -344,7 +345,7 @@ async function addSingleMethod(state, fileName) {
state.methods.used[name] = relPath
}
if (isExported) {
if (isExported || isDeclare) {
const isPrivate = checkForFlag(stmt, '@internal')
const isManual = checkForFlag(stmt, '@manual')
const isNoemit = checkForFlag(stmt, '@noemit')
@ -358,6 +359,7 @@ async function addSingleMethod(state, fileName) {
isPrivate,
isManual,
isNoemit,
isDeclare,
shouldEmit,
func: stmt,
comment: getLeadingComments(stmt),
@ -369,6 +371,7 @@ async function addSingleMethod(state, fileName) {
hasOverloads: hasOverloads[name] && !isOverload,
})
if (!isDeclare) {
if (!(module in state.imports)) {
state.imports[module] = new Set()
}
@ -378,6 +381,7 @@ async function addSingleMethod(state, fileName) {
}
}
}
}
} else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) {
if (isCopy) {
state.copy.push({
@ -399,6 +403,9 @@ async function addSingleMethod(state, fileName) {
}
state.imports[module].add(stmt.name.escapedText)
state.exported[module] = state.exported[module] || new Set()
state.exported[module].add(stmt.name.escapedText)
continue
}
@ -429,6 +436,9 @@ async function addSingleMethod(state, fileName) {
}
state.imports[module].add(stmt.name.escapedText)
state.exported[module] = state.exported[module] || new Set()
state.exported[module].add(stmt.name.escapedText)
} else if (isCopy) {
state.copy.push({ from: relPath, code: stmt.getFullText().trim() })
} else if (isTypeExported) {
@ -442,6 +452,7 @@ async function main() {
const output = fs.createWriteStream(targetFile)
const state = {
imports: {},
exported: {},
fields: [],
init: [],
methods: {
@ -527,6 +538,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
available,
rawApiMethods,
dependencies,
isDeclare,
}) => {
if (!available && !overload) {
// no @available directive
@ -659,7 +671,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
output.write(`${name}${generics}(${parameters})${returnType}\n`)
}
if (!overload && !isManual) {
if (!overload && !isManual && !isDeclare) {
if (hasOverloads) {
classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc')
}
@ -737,9 +749,14 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
const outputMethods = fs.createWriteStream(targetFileMethods)
outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n')
state.methods.list.forEach(({ module, name, overload }) => {
if (overload) return
state.methods.list.forEach(({ module, name, overload, isDeclare }) => {
if (overload || isDeclare) return
outputMethods.write(`export { ${name} } from '${module}'\n`)
if (state.exported[module]) {
outputMethods.write(`export type { ${[...state.exported[module]].join(', ')} } from '${module}'\n`)
delete state.exported[module]
}
})
await new Promise((resolve) => { outputMethods.end(resolve) })

View file

@ -7,8 +7,7 @@ import Long from 'long'
import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { MemoryStorage } from '../storage/providers/memory/index.js'
import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../types/index.js'
import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../types/index.js'
import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js'
import { ITelegramClient } from './client.types.js'
import { checkPassword } from './methods/auth/check-password.js'
@ -95,7 +94,6 @@ import { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js'
import { iterDialogs } from './methods/dialogs/iter-dialogs.js'
import { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
import { downloadAsBuffer } from './methods/files/download-buffer.js'
import { downloadToFile } from './methods/files/download-file.js'
import { downloadAsIterable } from './methods/files/download-iterable.js'
import { downloadAsStream } from './methods/files/download-stream.js'
import { _normalizeInputFile } from './methods/files/normalize-input-file.js'
@ -314,21 +312,19 @@ import {
UserTypingUpdate,
} from './types/index.js'
import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from './updates/parsed.js'
import { _defaultStorageFactory } from './utils/platform/storage.js'
import { StringSessionData } from './utils/string-session.js'
// from methods/_init.ts
// @copy
type TelegramClientOptions = (
| (Omit<BaseTelegramClientOptions, 'storage'> & {
| (PartialOnly<Omit<BaseTelegramClientOptions, 'storage'>, 'transport' | 'crypto'> & {
/**
* Storage to use for this client.
*
* If a string is passed, it will be used as:
* - a path to a JSON file for Node.js
* - IndexedDB database name for browsers
* If a string is passed, it will be used as
* a name for the default platform-specific storage provider to use.
*
* If omitted, {@link MemoryStorage} is used
* @default `"client.session"`
*/
storage?: string | ITelegramStorageProvider
})
@ -2250,6 +2246,7 @@ export interface TelegramClient extends ITelegramClient {
* @param params File download parameters
*/
downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise<Uint8Array>
/**
* Download a remote file to a local file (only for NodeJS).
* Promise will resolve once the download is complete.
@ -5140,20 +5137,13 @@ export class TelegramClient extends EventEmitter implements ITelegramClient {
if ('client' in opts) {
this._client = opts.client
} else {
let storage: ITelegramStorageProvider
if (typeof opts.storage === 'string') {
storage = _defaultStorageFactory(opts.storage)
} else if (!opts.storage) {
storage = new MemoryStorage()
} else {
storage = opts.storage
if (!opts.storage || typeof opts.storage === 'string' || !opts.transport || !opts.crypto) {
throw new MtUnsupportedError(
'You need to explicitly provide storage, transport and crypto for @mtcute/core',
)
}
this._client = new BaseTelegramClient({
...opts,
storage,
})
this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions)
}
// @ts-expect-error codegen
@ -5448,9 +5438,6 @@ TelegramClient.prototype.setFoldersOrder = function (...args) {
TelegramClient.prototype.downloadAsBuffer = function (...args) {
return downloadAsBuffer(this._client, ...args)
}
TelegramClient.prototype.downloadToFile = function (...args) {
return downloadToFile(this._client, ...args)
}
TelegramClient.prototype.downloadAsIterable = function (...args) {
return downloadAsIterable(this._client, ...args)
}

View file

@ -1,5 +1,4 @@
export * from './base.js'
export * from './client.js'
export * from './client.types.js'
export * from './storage/index.js'
export * from './types/index.js'

View file

@ -86,7 +86,6 @@ export { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js'
export { iterDialogs } from './methods/dialogs/iter-dialogs.js'
export { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
export { downloadAsBuffer } from './methods/files/download-buffer.js'
export { downloadToFile } from './methods/files/download-file.js'
export { downloadAsIterable } from './methods/files/download-iterable.js'
export { downloadAsStream } from './methods/files/download-stream.js'
export { _normalizeInputFile } from './methods/files/normalize-input-file.js'
@ -96,6 +95,7 @@ export { uploadMedia } from './methods/files/upload-media.js'
export { createForumTopic } from './methods/forums/create-forum-topic.js'
export { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history.js'
export { editForumTopic } from './methods/forums/edit-forum-topic.js'
export type { GetForumTopicsOffset } from './methods/forums/get-forum-topics.js'
export { getForumTopics } from './methods/forums/get-forum-topics.js'
export { getForumTopicsById } from './methods/forums/get-forum-topics-by-id.js'
export { iterForumTopics } from './methods/forums/iter-forum-topics.js'
@ -109,6 +109,7 @@ export { editInviteLink } from './methods/invite-links/edit-invite-link.js'
export { exportInviteLink } from './methods/invite-links/export-invite-link.js'
export { getInviteLink } from './methods/invite-links/get-invite-link.js'
export { getInviteLinkMembers } from './methods/invite-links/get-invite-link-members.js'
export type { GetInviteLinksOffset } from './methods/invite-links/get-invite-links.js'
export { getInviteLinks } from './methods/invite-links/get-invite-links.js'
export { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link.js'
export { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests.js'
@ -117,15 +118,18 @@ export { iterInviteLinkMembers } from './methods/invite-links/iter-invite-link-m
export { iterInviteLinks } from './methods/invite-links/iter-invite-links.js'
export { revokeInviteLink } from './methods/invite-links/revoke-invite-link.js'
export { closePoll } from './methods/messages/close-poll.js'
export type { DeleteMessagesParams } from './methods/messages/delete-messages.js'
export { deleteMessagesById } from './methods/messages/delete-messages.js'
export { deleteMessages } from './methods/messages/delete-messages.js'
export { deleteScheduledMessages } from './methods/messages/delete-scheduled-messages.js'
export { editInlineMessage } from './methods/messages/edit-inline-message.js'
export { editMessage } from './methods/messages/edit-message.js'
export type { ForwardMessageOptions } from './methods/messages/forward-messages.js'
export { forwardMessagesById } from './methods/messages/forward-messages.js'
export { forwardMessages } from './methods/messages/forward-messages.js'
export { getCallbackQueryMessage } from './methods/messages/get-callback-query-message.js'
export { getDiscussionMessage } from './methods/messages/get-discussion-message.js'
export type { GetHistoryOffset } from './methods/messages/get-history.js'
export { getHistory } from './methods/messages/get-history.js'
export { getMessageByLink } from './methods/messages/get-message-by-link.js'
export { getMessageGroup } from './methods/messages/get-message-group.js'
@ -133,6 +137,7 @@ export { getMessageReactionsById } from './methods/messages/get-message-reaction
export { getMessageReactions } from './methods/messages/get-message-reactions.js'
export { getMessages } from './methods/messages/get-messages.js'
export { getMessagesUnsafe } from './methods/messages/get-messages-unsafe.js'
export type { GetReactionUsersOffset } from './methods/messages/get-reaction-users.js'
export { getReactionUsers } from './methods/messages/get-reaction-users.js'
export { getReplyTo } from './methods/messages/get-reply-to.js'
export { getScheduledMessages } from './methods/messages/get-scheduled-messages.js'
@ -143,7 +148,9 @@ export { iterSearchMessages } from './methods/messages/iter-search-messages.js'
export { pinMessage } from './methods/messages/pin-message.js'
export { readHistory } from './methods/messages/read-history.js'
export { readReactions } from './methods/messages/read-reactions.js'
export type { SearchGlobalOffset } from './methods/messages/search-global.js'
export { searchGlobal } from './methods/messages/search-global.js'
export type { SearchMessagesOffset } from './methods/messages/search-messages.js'
export { searchMessages } from './methods/messages/search-messages.js'
export { answerText } from './methods/messages/send-answer.js'
export { answerMedia } from './methods/messages/send-answer.js'
@ -151,10 +158,13 @@ export { answerMediaGroup } from './methods/messages/send-answer.js'
export { commentText } from './methods/messages/send-comment.js'
export { commentMedia } from './methods/messages/send-comment.js'
export { commentMediaGroup } from './methods/messages/send-comment.js'
export type { SendCopyParams } from './methods/messages/send-copy.js'
export { sendCopy } from './methods/messages/send-copy.js'
export type { SendCopyGroupParams } from './methods/messages/send-copy-group.js'
export { sendCopyGroup } from './methods/messages/send-copy-group.js'
export { sendMedia } from './methods/messages/send-media.js'
export { sendMediaGroup } from './methods/messages/send-media-group.js'
export type { QuoteParamsFrom } from './methods/messages/send-quote.js'
export { quoteWithText } from './methods/messages/send-quote.js'
export { quoteWithMedia } from './methods/messages/send-quote.js'
export { quoteWithMediaGroup } from './methods/messages/send-quote.js'
@ -179,6 +189,7 @@ export { resendPasswordEmail } from './methods/password/password-email.js'
export { cancelPasswordEmail } from './methods/password/password-email.js'
export { removeCloudPassword } from './methods/password/remove-cloud-password.js'
export { applyBoost } from './methods/premium/apply-boost.js'
export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
export { canApplyBoost } from './methods/premium/can-apply-boost.js'
export { getBoostStats } from './methods/premium/get-boost-stats.js'
export { getBoosts } from './methods/premium/get-boosts.js'
@ -194,6 +205,7 @@ export { getStickerSet } from './methods/stickers/get-sticker-set.js'
export { moveStickerInSet } from './methods/stickers/move-sticker-in-set.js'
export { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js'
export { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb.js'
export type { CanSendStoryResult } from './methods/stories/can-send-story.js'
export { canSendStory } from './methods/stories/can-send-story.js'
export { deleteStories } from './methods/stories/delete-stories.js'
export { editStory } from './methods/stories/edit-story.js'

View file

@ -6,7 +6,7 @@ import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
// @copy
import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../../types/index.js'
import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../../types/index.js'
// @copy
import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js'
// @copy

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// @copy
import { MemoryStorage } from '../../storage/providers/memory/index.js'
import { MtUnsupportedError, PartialOnly } from '../../types/index.js'
import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js'
import { TelegramClient } from '../client.js'
import { ITelegramClient } from '../client.types.js'
@ -11,19 +11,16 @@ import { ITelegramStorageProvider } from '../storage/provider.js'
import { Conversation } from '../types/conversation.js'
// @copy
import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from '../updates/parsed.js'
// @copy
import { _defaultStorageFactory } from '../utils/platform/storage.js'
// @copy
type TelegramClientOptions = ((Omit<BaseTelegramClientOptions, 'storage'> & {
type TelegramClientOptions = ((PartialOnly<Omit<BaseTelegramClientOptions, 'storage'>, 'transport' | 'crypto'> & {
/**
* Storage to use for this client.
*
* If a string is passed, it will be used as:
* - a path to a JSON file for Node.js
* - IndexedDB database name for browsers
* If a string is passed, it will be used as
* a name for the default platform-specific storage provider to use.
*
* If omitted, {@link MemoryStorage} is used
* @default `"client.session"`
*/
storage?: string | ITelegramStorageProvider
}) | ({ client: ITelegramClient })) & {
@ -37,41 +34,17 @@ type TelegramClientOptions = ((Omit<BaseTelegramClientOptions, 'storage'> & {
skipConversationUpdates?: boolean
}
// // @initialize=super
// /** @internal */
// function _initializeClientSuper(this: TelegramClient, opts: TelegramClientOptions) {
// if (typeof opts.storage === 'string') {
// opts.storage = _defaultStorageFactory(opts.storage)
// } else if (!opts.storage) {
// opts.storage = new MemoryStorage()
// }
// /* eslint-disable @typescript-eslint/no-unsafe-call */
// // @ts-expect-error codegen
// super(opts)
// /* eslint-enable @typescript-eslint/no-unsafe-call */
// }
// @initialize
/** @internal */
function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) {
if ('client' in opts) {
this._client = opts.client
} else {
let storage: ITelegramStorageProvider
if (typeof opts.storage === 'string') {
storage = _defaultStorageFactory(opts.storage)
} else if (!opts.storage) {
storage = new MemoryStorage()
} else {
storage = opts.storage
if (!opts.storage || typeof opts.storage === 'string' || !opts.transport || !opts.crypto) {
throw new MtUnsupportedError('You need to explicitly provide storage, transport and crypto for @mtcute/core')
}
this._client = new BaseTelegramClient({
...opts,
storage,
})
this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions)
}
// @ts-expect-error codegen

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { ITelegramClient } from '../../client.types.js'
import { InputMessageId, normalizeInputMessageId } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
@ -53,7 +53,7 @@ export async function getCallbackAnswer(
_: 'messages.getBotCallbackAnswer',
peer: await resolvePeer(client, chatId),
msgId: message,
data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data,
data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data,
password,
game: game,
},

View file

@ -1,11 +1,9 @@
// eslint-disable-next-line no-restricted-imports
import { createWriteStream, rmSync } from 'fs'
import { writeFile } from 'fs/promises'
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ITelegramClient } from '../../client.types.js'
import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js'
import { downloadAsIterable } from './download-iterable.js'
import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.js'
// @available=both
/**
* Download a remote file to a local file (only for NodeJS).
* Promise will resolve once the download is complete.
@ -13,30 +11,9 @@ import { downloadAsIterable } from './download-iterable.js'
* @param filename Local file name to which the remote file will be downloaded
* @param params File download parameters
*/
export async function downloadToFile(
declare function downloadToFile(
client: ITelegramClient,
filename: string,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): Promise<void> {
if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) {
// early return for inline files
await writeFile(filename, location.location)
}
const output = createWriteStream(filename)
if (params?.abortSignal) {
params.abortSignal.addEventListener('abort', () => {
client.log.debug('aborting file download %s - cleaning up', filename)
output.destroy()
rmSync(filename)
})
}
for await (const chunk of downloadAsIterable(client, location, params)) {
output.write(chunk)
}
output.end()
}
): Promise<void>

View file

@ -1,5 +0,0 @@
import { MtUnsupportedError } from '../../../types/errors.js'
export function downloadToFile() {
throw new MtUnsupportedError('Downloading to file is only supported in NodeJS')
}

View file

@ -2,6 +2,7 @@ import { parseFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { ConnectionKind } from '../../../network/network-manager.js'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError, MtUnsupportedError } from '../../../types/errors.js'
import { ConditionVariable } from '../../../utils/condition-variable.js'
import { ITelegramClient } from '../../client.types.js'
@ -56,7 +57,7 @@ export async function* downloadAsIterable(
if (!fileSize) fileSize = input.fileSize
location = locationInner
} else if (typeof input === 'string') {
const parsed = parseFileId(input)
const parsed = parseFileId(getPlatform(), input)
if (parsed.location._ === 'web') {
location = fileIdToInputWebFileLocation(parsed)

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { parseFileId, tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { isUploadedFile } from '../../types/files/uploaded-file.js'
@ -303,7 +304,7 @@ export async function _normalizeInputMedia(
} else if (typeof input === 'string' && input.match(/^file:/)) {
await upload(input.substring(5))
} else {
const parsed = typeof input === 'string' ? parseFileId(input) : input
const parsed = typeof input === 'string' ? parseFileId(getPlatform(), input) : input
if (parsed.location._ === 'photo') {
return {

View file

@ -7,7 +7,6 @@ import { UploadedFile, UploadFileLike } from '../../types/index.js'
import { guessFileMime } from '../../utils/file-type.js'
import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js'
import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js'
import { _createFileStream, _extractFileStreamMeta, _handleNodeStream, _isFileStream } from './_platform.js'
const OVERRIDE_MIME: Record<string, string> = {
// tg doesn't interpret `audio/opus` files as voice messages for some reason
@ -37,9 +36,6 @@ export async function uploadFile(
params: {
/**
* Upload file source.
*
* > **Note**: `fs.ReadStream` is a subclass of `stream.Readable` and contains
* > info about file name, thus you don't need to pass them explicitly.
*/
file: UploadFileLike
@ -113,19 +109,9 @@ export async function uploadFile(
if (typeof File !== 'undefined' && file instanceof File) {
fileName = file.name
fileSize = file.size
// file is now ReadableStream
file = file.stream()
}
if (typeof file === 'string') {
file = _createFileStream(file)
}
if (_isFileStream(file)) {
[fileName, fileSize] = await _extractFileStreamMeta(file)
// fs.ReadStream is a subclass of Readable, will be handled below
}
if (typeof file === 'object' && 'headers' in file && 'body' in file && 'url' in file) {
// fetch() response
const length = parseInt(file.headers.get('content-length') || '0')
@ -161,8 +147,6 @@ export async function uploadFile(
file = file.body
}
file = _handleNodeStream(file)
if (!(file instanceof ReadableStream)) {
throw new MtArgumentError('Could not convert input `file` to stream!')
}

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { assertNever } from '../../../types/utils.js'
import { toInputUser } from '../../utils/peer-utils.js'
import { BotKeyboardBuilder } from './keyboard-builder.js'
@ -218,7 +218,7 @@ export namespace BotKeyboard {
_: 'keyboardButtonCallback',
text,
requiresPassword,
data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data,
data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data,
}
}

View file

@ -1,6 +1,7 @@
import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { FileLocation } from '../files/index.js'
@ -123,7 +124,7 @@ export abstract class RawDocument extends FileLocation {
* representing this document.
*/
get fileId(): string {
return toFileId({
return toFileId(getPlatform(), {
type: this._fileIdType(),
dcId: this.raw.dcId,
fileReference: this.raw.fileReference,
@ -139,7 +140,7 @@ export abstract class RawDocument extends FileLocation {
* Get a unique File ID representing this document.
*/
get uniqueFileId(): string {
return toUniqueFileId(td.FileType.Document, {
return toUniqueFileId(getPlatform(), td.FileType.Document, {
_: 'common',
id: this.raw.id,
})

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
import { inflateSvgPath, strippedPhotoToJpg, svgPathToFile } from '../../utils/file-utils.js'
@ -192,7 +193,7 @@ export class Thumbnail extends FileLocation {
}
if (this._media._ === 'stickerSet') {
return toFileId({
return toFileId(getPlatform(), {
type: td.FileType.Thumbnail,
dcId: this.dcId!,
fileReference: null,
@ -210,7 +211,7 @@ export class Thumbnail extends FileLocation {
})
}
return toFileId({
return toFileId(getPlatform(), {
type: this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail,
dcId: this.dcId!,
fileReference: this._media.fileReference,
@ -239,7 +240,7 @@ export class Thumbnail extends FileLocation {
}
if (this._media._ === 'stickerSet') {
return toUniqueFileId(td.FileType.Thumbnail, {
return toUniqueFileId(getPlatform(), td.FileType.Thumbnail, {
_: 'photo',
id: Long.ZERO,
source: {
@ -251,7 +252,7 @@ export class Thumbnail extends FileLocation {
})
}
return toUniqueFileId(this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail, {
return toUniqueFileId(getPlatform(), this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail, {
_: 'photo',
id: this._media.id,
source: {

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js'
import { toggleChannelIdMark } from '../../../utils/peer-utils.js'
import { strippedPhotoToJpg } from '../../utils/file-utils.js'
@ -62,7 +63,7 @@ export class ChatPhotoSize extends FileLocation {
throw new MtArgumentError('Input peer was invalid')
}
return toFileId({
return toFileId(getPlatform(), {
dcId: this.obj.dcId,
type: tdFileId.FileType.ProfilePhoto,
fileReference: null,
@ -84,7 +85,7 @@ export class ChatPhotoSize extends FileLocation {
* TDLib and Bot API compatible unique File ID representing this size
*/
get uniqueFileId(): string {
return toUniqueFileId(tdFileId.FileType.ProfilePhoto, {
return toUniqueFileId(getPlatform(), tdFileId.FileType.ProfilePhoto, {
_: 'photo',
id: this.obj.photoId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import { utf8Decode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/index.js'
import { encodeInlineMessageId } from '../../utils/inline-utils.js'
@ -58,7 +58,7 @@ class BaseCallbackQuery {
get dataStr(): string | null {
if (!this.raw.data) return null
return utf8Decode(this.raw.data)
return getPlatform().utf8Decode(this.raw.data)
}
/**

View file

@ -6,6 +6,7 @@ import { tl } from '@mtcute/tl'
import { parseMarkedPeerId } from '../../utils/peer-utils.js'
import FileType = td.FileType
import { getPlatform } from '../../platform.js'
import { assertNever } from '../../types/utils.js'
const EMPTY_BUFFER = new Uint8Array(0)
@ -45,7 +46,7 @@ function dialogPhotoToInputPeer(
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToInputWebFileLocation(fileId: string | FileId): tl.RawInputWebFileLocation {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if (fileId.location._ !== 'web') {
throw new td.ConversionError('inputWebFileLocation')
@ -65,7 +66,7 @@ export function fileIdToInputWebFileLocation(fileId: string | FileId): tl.RawInp
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToInputFileLocation(fileId: string | FileId): tl.TypeInputFileLocation {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
const loc = fileId.location
@ -219,7 +220,7 @@ export function fileIdToInputFileLocation(fileId: string | FileId): tl.TypeInput
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocument {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if (
fileId.location._ !== 'common' ||
@ -256,7 +257,7 @@ export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocum
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToInputPhoto(fileId: string | FileId): tl.RawInputPhoto {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if (fileId.location._ !== 'photo') {
throw new td.ConversionError('inputPhoto')
@ -281,7 +282,7 @@ export function fileIdToInputPhoto(fileId: string | FileId): tl.RawInputPhoto {
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToEncryptedFile(fileId: string | FileId): tl.RawInputEncryptedFile {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if (fileId.location._ !== 'common' || fileId.type !== FileType.Encrypted) {
throw new td.ConversionError('inputEncryptedFile')
@ -301,7 +302,7 @@ export function fileIdToEncryptedFile(fileId: string | FileId): tl.RawInputEncry
* @param fileId File ID, either parsed or as a string
*/
export function fileIdToSecureFile(fileId: string | FileId): tl.RawInputSecureFile {
if (typeof fileId === 'string') fileId = parseFileId(fileId)
if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if (fileId.location._ !== 'common' || (fileId.type !== FileType.Secure && fileId.type !== FileType.SecureRaw)) {
throw new td.ConversionError('inputSecureFile')

View file

@ -1,9 +1,10 @@
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { guessFileMime } from './file-type.js'
const p = getPlatform()
describe('guessFileMime', () => {
it.each([
['424d', 'image/bmp'],
@ -60,6 +61,6 @@ describe('guessFileMime', () => {
])('should detect %s as %s', (header, mime) => {
header += '00'.repeat(16)
expect(guessFileMime(hexDecodeToBuffer(header))).toEqual(mime)
expect(guessFileMime(p.hexDecode(header))).toEqual(mime)
})
})

View file

@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import {
extractFileName,
inflateSvgPath,
@ -10,28 +9,30 @@ import {
svgPathToFile,
} from './file-utils.js'
const p = getPlatform()
describe('isProbablyPlainText', () => {
it('should return true for buffers only containing printable ascii', () => {
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text'))).to.be.true
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\nwith unix new lines'))).to.be.true
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\r\nwith windows new lines'))).to.be
expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text'))).to.be.true
expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\nwith unix new lines'))).to.be.true
expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\r\nwith windows new lines'))).to.be
.true
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\n\twith unix new lines and tabs')))
expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\n\twith unix new lines and tabs')))
.to.be.true
expect(
isProbablyPlainText(
utf8EncodeToBuffer('hello this is some ascii text\r\n\twith windows new lines and tabs'),
p.utf8Encode('hello this is some ascii text\r\n\twith windows new lines and tabs'),
),
).to.be.true
})
it('should return false for buffers containing some binary data', () => {
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is cedilla: ç'))).to.be.false
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text with emojis 🌸'))).to.be.false
expect(isProbablyPlainText(p.utf8Encode('hello this is cedilla: ç'))).to.be.false
expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text with emojis 🌸'))).to.be.false
// random strings of 16 bytes
expect(isProbablyPlainText(hexDecodeToBuffer('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false
expect(isProbablyPlainText(hexDecodeToBuffer('20e8e218e54254c813b261432b0330d7'))).to.be.false
expect(isProbablyPlainText(p.hexDecode('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false
expect(isProbablyPlainText(p.hexDecode('20e8e218e54254c813b261432b0330d7'))).to.be.false
})
})
@ -53,14 +54,14 @@ describe('svgPathToFile', () => {
it('should convert SVG path to a file', () => {
const path = 'M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z'
expect(utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot(
expect(p.utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot(
'"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><svg version=\\"1.1\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\"viewBox=\\"0 0 512 512\\" xml:space=\\"preserve\\"><path d=\\"M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z\\"/></svg>"',
)
})
})
describe('inflateSvgPath', () => {
const data = hexDecodeToBuffer(
const data = p.hexDecode(
'1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' +
'074e06414d80588863935fad74be4704854684518b528581904695498b488b56' +
'965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' +
@ -85,9 +86,9 @@ describe('inflateSvgPath', () => {
describe('strippedPhotoToJpg', () => {
// strippedThumb of @Channel_Bot
const dataPfp = hexDecodeToBuffer('010808b1f2f95fed673451457033ad1f')
const dataPfp = p.hexDecode('010808b1f2f95fed673451457033ad1f')
// photoStrippedSize of a random image
const dataPicture = hexDecodeToBuffer(
const dataPicture = p.hexDecode(
'012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' +
'816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' +
'32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' +
@ -99,7 +100,7 @@ describe('strippedPhotoToJpg', () => {
)
it('should inflate stripped jpeg (from profile picture)', () => {
expect(hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot(
expect(p.hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot(
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +
@ -124,7 +125,7 @@ describe('strippedPhotoToJpg', () => {
})
it('should inflate stripped jpeg (from a picture)', () => {
expect(hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot(
expect(p.hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot(
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +

View file

@ -1,5 +1,4 @@
import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { MtArgumentError } from '../../types/errors.js'
import { concatBuffers } from '../../utils/buffer-utils.js'
@ -33,7 +32,7 @@ export function isProbablyPlainText(buf: Uint8Array): boolean {
}
// from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
const JPEG_HEADER = hexDecodeToBuffer(
const JPEG_HEADER = () => getPlatform().hexDecode(
'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' +
'2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' +
'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' +
@ -54,6 +53,7 @@ const JPEG_HEADER = hexDecodeToBuffer(
'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' +
'3f4f5f6f7f8f9faffda000c03010002110311003f00',
)
let JPEG_HEADER_BYTES: Uint8Array | null = null
const JPEG_FOOTER = new Uint8Array([0xff, 0xd9])
/**
@ -64,7 +64,11 @@ export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array {
throw new MtArgumentError('Invalid stripped JPEG')
}
const result = concatBuffers([JPEG_HEADER, stripped.slice(3), JPEG_FOOTER])
if (JPEG_HEADER_BYTES === null) {
JPEG_HEADER_BYTES = JPEG_HEADER()
}
const result = concatBuffers([JPEG_HEADER_BYTES, stripped.slice(3), JPEG_FOOTER])
result[164] = stripped[1]
result[166] = stripped[2]
@ -108,7 +112,7 @@ export function inflateSvgPath(encoded: Uint8Array): string {
* @param path
*/
export function svgPathToFile(path: string): Uint8Array {
return utf8EncodeToBuffer(
return getPlatform().utf8Encode(
'<?xml version="1.0" encoding="utf-8"?>' +
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' +
'viewBox="0 0 512 512" xml:space="preserve">' +

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/tl'
import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { assertNever } from '../../types/utils.js'
/**
@ -9,7 +10,7 @@ import { assertNever } from '../../types/utils.js'
* @param id Inline message ID
*/
export function parseInlineMessageId(id: string): tl.TypeInputBotInlineMessageID {
const buf = base64DecodeToBuffer(id, true)
const buf = getPlatform().base64Decode(id, true)
const reader = TlBinaryReader.manual(buf)
if (buf.length === 20) {
@ -56,7 +57,7 @@ export function encodeInlineMessageId(id: tl.TypeInputBotInlineMessageID): strin
assertNever(id)
}
return base64Encode(writer.result(), true)
return getPlatform().base64Encode(writer.result(), true)
}
export function normalizeInlineId(id: string | tl.TypeInputBotInlineMessageID) {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */
import { base64Encode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom')
@ -52,7 +52,7 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
if (val && typeof val === 'object') {
if (val instanceof Uint8Array) {
val = base64Encode(val)
val = getPlatform().base64Encode(val)
} else if (typeof val.toJSON === 'function') {
val = val.toJSON(true)
}

View file

@ -1,7 +0,0 @@
import { MtUnsupportedError } from '../../../types/errors.js'
import { ITelegramStorageProvider } from '../../storage/provider.js'
/** @internal */
export const _defaultStorageFactory = (_name: string): ITelegramStorageProvider => {
throw new MtUnsupportedError('Please provide a storage explicitly (e.g. @mtcute/sqlite)')
}

View file

@ -1,11 +0,0 @@
import { IdbStorage } from '../../../storage/index.js'
import { MtUnsupportedError } from '../../../types/errors.js'
/** @internal */
export const _defaultStorageFactory = (name: string) => {
if (typeof indexedDB !== 'undefined') {
return new IdbStorage(name)
}
throw new MtUnsupportedError('No storage available!')
}

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/tl'
import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime'
import { TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { MtArgumentError } from '../../types/index.js'
import { BasicDcOption, DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js'
import { CurrentUserInfo } from '../storage/service/current-user.js'
@ -53,11 +54,11 @@ export function writeStringSession(data: StringSessionData): string {
writer.bytes(data.authKey)
return base64Encode(writer.result(), true)
return getPlatform().base64Encode(writer.result(), true)
}
export function readStringSession(readerMap: TlReaderMap, data: string): StringSessionData {
const buf = base64DecodeToBuffer(data, true)
const buf = getPlatform().base64Decode(data, true)
const version = buf[0]

View file

@ -1,14 +1,15 @@
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { decodeWaveform, encodeWaveform } from './voice-utils.js'
const p = getPlatform()
describe('decodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => {
expect(
decodeWaveform(
hexDecodeToBuffer(
p.hexDecode(
'0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' +
'08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f',
),
@ -25,7 +26,7 @@ describe('decodeWaveform', () => {
describe('encodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => {
expect(
hexEncode(
p.hexEncode(
encodeWaveform([
0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10,
10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18,

View file

@ -1,4 +1,4 @@
import { beforeExit } from '../../../utils/platform/exit-hook.js'
import { getPlatform } from '../../../platform.js'
import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js'
export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] {
@ -52,7 +52,7 @@ export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandle
worker.port.close()
}
beforeExit(close)
getPlatform().beforeExit(close)
return [send, close]
}

View file

@ -3,22 +3,19 @@ import { describe, expect, it, vi } from 'vitest'
import { defaultTestCryptoProvider } from '@mtcute/test'
import {
hexDecode,
hexDecodeToBuffer,
hexEncode,
TlBinaryReader,
TlReaderMap,
utf8Decode,
utf8EncodeToBuffer,
} from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { LogManager } from '../utils/index.js'
import { AuthKey } from './auth-key.js'
const authKey = new Uint8Array(256)
const p = getPlatform()
for (let i = 0; i < 256; i += 32) {
hexDecode(authKey.subarray(i, i + 32), '98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
authKey.subarray(i, i + 32).set(p.hexDecode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0'))
}
describe('AuthKey', () => {
@ -54,19 +51,19 @@ describe('AuthKey', () => {
it('should calculate derivatives', async () => {
const key = await create()
expect(hexEncode(key.key)).toEqual(hexEncode(authKey))
expect(hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
expect(hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
expect(hexEncode(key.id)).toEqual('40fa5bb7cb56a895')
expect(p.hexEncode(key.key)).toEqual(p.hexEncode(authKey))
expect(p.hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
expect(p.hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
expect(p.hexEncode(key.id)).toEqual('40fa5bb7cb56a895')
})
it('should encrypt a message', async () => {
const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!'))
const message = writeMessage(p.utf8Encode('hello, world!!!!'))
const key = await create()
const msg = key.encryptMessage(message, serverSalt, sessionId)
expect(hexEncode(msg)).toEqual(
expect(p.hexEncode(msg)).toEqual(
'40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' +
'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' +
'5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06',
@ -88,7 +85,7 @@ describe('AuthKey', () => {
}
it('should decrypt a message', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -98,11 +95,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo)
expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
})
it('should decrypt a message with padding', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' +
@ -113,11 +110,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo)
expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
})
it('should ignore messages with invalid message key', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -127,7 +124,7 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid session_id', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' +
'2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' +
'30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a',
@ -137,12 +134,12 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid length', async () => {
const messageTooLong = hexDecodeToBuffer(
const messageTooLong = p.hexDecode(
'40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' +
'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' +
'08d2859425685a55b21fab413bfabc42ec5da283853b28c0',
)
const messageUnaligned = hexDecodeToBuffer(
const messageUnaligned = p.hexDecode(
'40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' +
'5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' +
'2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf',
@ -155,7 +152,7 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid padding', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' +
'79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc',
)

View file

@ -11,9 +11,7 @@ import { StorageManager, StorageManagerExtraOptions } from '../storage/storage.j
import { MustEqual } from '../types/index.js'
import {
asyncResettable,
CryptoProviderFactory,
DcOptions,
defaultCryptoProviderFactory,
defaultProductionDc,
defaultProductionIpv6Dc,
defaultTestDc,
@ -49,10 +47,10 @@ export interface MtClientOptions {
storageOptions?: StorageManagerExtraOptions
/**
* Cryptography provider factory to allow delegating
* Cryptography provider to allow delegating
* crypto to native addon, worker, etc.
*/
crypto?: CryptoProviderFactory
crypto: ICryptoProvider
/**
* Whether to use IPv6 datacenters
@ -96,7 +94,7 @@ export interface MtClientOptions {
*
* @default platform-specific transport: WebSocket on the web, TCP in node
*/
transport?: TransportFactory
transport: TransportFactory
/**
* Reconnection strategy.
@ -254,7 +252,7 @@ export class MtClient extends EventEmitter {
this.log.mgr.level = params.logLevel
}
this.crypto = (params.crypto ?? defaultCryptoProviderFactory)()
this.crypto = params.crypto
this._testMode = Boolean(params.testMode)
let dc = params.defaultDcs

View file

@ -1,6 +1,7 @@
import { mtp, tl } from '@mtcute/tl'
import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { StorageManager } from '../storage/storage.js'
import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js'
import {
@ -18,7 +19,7 @@ import { PersistentConnectionParams } from './persistent-connection.js'
import { defaultReconnectionStrategy, ReconnectionStrategy } from './reconnection.js'
import { ServerSaltManager } from './server-salt.js'
import { SessionConnection, SessionConnectionParams } from './session-connection.js'
import { defaultTransportFactory, TransportFactory } from './transports/index.js'
import { TransportFactory } from './transports/index.js'
export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall'
@ -44,7 +45,7 @@ export interface NetworkManagerParams {
enableErrorReporting: boolean
apiId: number
initConnectionOptions?: Partial<Omit<tl.RawInitConnectionRequest, 'apiId' | 'query'>>
transport?: TransportFactory
transport: TransportFactory
reconnectionStrategy?: ReconnectionStrategy<PersistentConnectionParams>
floodSleepThreshold: number
maxRetryCount: number
@ -439,15 +440,7 @@ export class NetworkManager {
readonly params: NetworkManagerParams & NetworkManagerExtraParams,
readonly config: ConfigManager,
) {
let deviceModel = 'mtcute on '
/* eslint-disable no-restricted-globals */
if (typeof process !== 'undefined' && typeof require !== 'undefined') {
const os = require('os') as typeof import('os')
deviceModel += `${os.type()} ${os.arch()} ${os.release()}`
/* eslint-enable no-restricted-globals */
} else if (typeof navigator !== 'undefined') {
deviceModel += navigator.userAgent
} else deviceModel += 'unknown'
const deviceModel = `mtcute on ${getPlatform().getDeviceModel()}`
this._initConnectionParams = {
_: 'initConnection',
@ -463,7 +456,7 @@ export class NetworkManager {
query: null as any,
}
this._transportFactory = params.transport ?? defaultTransportFactory
this._transportFactory = params.transport
this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy
this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate
this._updateHandler = params.onUpdate

View file

@ -7,6 +7,7 @@ import { TlBinaryReader, TlBinaryWriter, TlReaderMap, TlSerializationCounter, Tl
import { MtArgumentError, MtcuteError, MtTimeoutError } from '../types/index.js'
import { createAesIgeForMessageOld } from '../utils/crypto/mtproto.js'
import { reportUnknownError } from '../utils/error-reporting.js'
import {
concatBuffers,
ControllablePromise,
@ -17,7 +18,6 @@ import {
randomLong,
removeFromLongArray,
} from '../utils/index.js'
import { reportUnknownError } from '../utils/platform/error-reporting.js'
import { doAuthorization } from './authorization.js'
import { MtprotoSession, PendingMessage, PendingRpc } from './mtproto-session.js'
import { PersistentConnection, PersistentConnectionParams } from './persistent-connection.js'

View file

@ -1,12 +1,5 @@
import { TransportFactory } from './abstract.js'
export * from './abstract.js'
export * from './intermediate.js'
export * from './obfuscated.js'
export * from './streamed.js'
export * from './wrapped.js'
import { _defaultTransportFactory } from '../../utils/platform/transport.js'
/** Platform-defined default transport factory */
export const defaultTransportFactory: TransportFactory = _defaultTransportFactory

View file

@ -1,13 +1,15 @@
import { describe, expect, it } from 'vitest'
import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js'
import { getPlatform } from '../../platform.js'
const p = getPlatform()
describe('IntermediatePacketCodec', () => {
it('should return correct tag', () => {
expect(hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee')
expect(p.hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee')
})
it('should correctly parse immediate framing', () =>
@ -17,7 +19,7 @@ describe('IntermediatePacketCodec', () => {
expect([...data]).eql([5, 1, 2, 3, 4])
done()
})
codec.feed(hexDecodeToBuffer('050000000501020304'))
codec.feed(p.hexDecode('050000000501020304'))
}))
it('should correctly parse incomplete framing', () =>
@ -27,8 +29,8 @@ describe('IntermediatePacketCodec', () => {
expect([...data]).eql([5, 1, 2, 3, 4])
done()
})
codec.feed(hexDecodeToBuffer('050000000501'))
codec.feed(hexDecodeToBuffer('020304'))
codec.feed(p.hexDecode('050000000501'))
codec.feed(p.hexDecode('020304'))
}))
it('should correctly parse multiple streamed packets', () =>
@ -46,9 +48,9 @@ describe('IntermediatePacketCodec', () => {
done()
}
})
codec.feed(hexDecodeToBuffer('050000000501'))
codec.feed(hexDecodeToBuffer('020304050000'))
codec.feed(hexDecodeToBuffer('000301020301'))
codec.feed(p.hexDecode('050000000501'))
codec.feed(p.hexDecode('020304050000'))
codec.feed(p.hexDecode('000301020301'))
}))
it('should correctly parse transport errors', () =>
@ -61,7 +63,7 @@ describe('IntermediatePacketCodec', () => {
done()
})
codec.feed(hexDecodeToBuffer('040000006cfeffff'))
codec.feed(p.hexDecode('040000006cfeffff'))
}))
it('should reset when called reset()', () =>
@ -73,15 +75,15 @@ describe('IntermediatePacketCodec', () => {
done()
})
codec.feed(hexDecodeToBuffer('ff0000001234567812345678'))
codec.feed(p.hexDecode('ff0000001234567812345678'))
codec.reset()
codec.feed(hexDecodeToBuffer('050000000102030405'))
codec.feed(p.hexDecode('050000000102030405'))
}))
it('should correctly frame packets', () => {
const data = hexDecodeToBuffer('6cfeffff')
const data = p.hexDecode('6cfeffff')
expect(hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff')
expect(p.hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff')
})
})
@ -96,12 +98,12 @@ describe('PaddedIntermediatePacketCodec', () => {
}
it('should return correct tag', async () => {
expect(hexEncode((await create()).tag())).eq('dddddddd')
expect(p.hexEncode((await create()).tag())).eq('dddddddd')
})
it('should correctly frame packets', async () => {
const data = hexDecodeToBuffer('6cfeffff')
const data = p.hexDecode('6cfeffff')
expect(hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f')
expect(p.hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f')
})
})

View file

@ -2,10 +2,13 @@ import { describe, expect, it, vi } from 'vitest'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test'
import { hexDecodeToBuffer, hexEncode, LogManager } from '../../utils/index.js'
import { getPlatform } from '../../platform.js'
import { LogManager } from '../../utils/index.js'
import { IntermediatePacketCodec } from './intermediate.js'
import { MtProxyInfo, ObfuscatedPacketCodec } from './obfuscated.js'
const p = getPlatform()
describe('ObfuscatedPacketCodec', () => {
const create = async (randomSource?: string, proxy?: MtProxyInfo) => {
const codec = new ObfuscatedPacketCodec(new IntermediatePacketCodec(), proxy)
@ -22,7 +25,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag()
expect(hexEncode(tag)).toEqual(
expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'fce8ab2203db2bff', // encrypted part
)
})
@ -40,7 +43,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag()
expect(hexEncode(tag)).toEqual(
expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbda8bb188b', // encrypted part with dcId = 1
)
})
@ -57,7 +60,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag()
expect(hexEncode(tag)).toEqual(
expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbdb89c188b', // encrypted part with dcId = 10001
)
})
@ -74,7 +77,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag()
expect(hexEncode(tag)).toEqual(
expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbd5644188b', // encrypted part with dcId = -1
)
})
@ -124,7 +127,7 @@ describe('ObfuscatedPacketCodec', () => {
it('should correctly create aes ctr for mtproxy', async () => {
const proxy: MtProxyInfo = {
dcId: 1,
secret: hexDecodeToBuffer('00112233445566778899aabbccddeeff'),
secret: p.hexDecode('00112233445566778899aabbccddeeff'),
test: true,
media: false,
}
@ -137,20 +140,20 @@ describe('ObfuscatedPacketCodec', () => {
expect(spyCreateAesCtr).toHaveBeenCalledTimes(2)
expect(spyCreateAesCtr).toHaveBeenNthCalledWith(
1,
hexDecodeToBuffer('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'),
p.hexDecode('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'),
u8HexDecode('936b33fa7f97bae025102532233abb26'),
true,
)
expect(spyCreateAesCtr).toHaveBeenNthCalledWith(
2,
hexDecodeToBuffer('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'),
p.hexDecode('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'),
u8HexDecode('db6aeee6883f45f95def566dadb4b610'),
false,
)
})
it('should correctly encrypt the underlying codec', async () => {
const data = hexDecodeToBuffer('6cfeffff')
const data = p.hexDecode('6cfeffff')
const msg1 = 'a1020630a410e940'
const msg2 = 'f53ff53f371db495'
@ -158,8 +161,8 @@ describe('ObfuscatedPacketCodec', () => {
await codec.tag()
expect(hexEncode(await codec.encode(data))).toEqual(msg1)
expect(hexEncode(await codec.encode(data))).toEqual(msg2)
expect(p.hexEncode(await codec.encode(data))).toEqual(msg1)
expect(p.hexEncode(await codec.encode(data))).toEqual(msg2)
})
it('should correctly decrypt the underlying codec', async () => {
@ -176,8 +179,8 @@ describe('ObfuscatedPacketCodec', () => {
log.push(e.toString())
})
codec.feed(hexDecodeToBuffer(msg1))
codec.feed(hexDecodeToBuffer(msg2))
codec.feed(p.hexDecode(msg1))
codec.feed(p.hexDecode(msg2))
await vi.waitFor(() => expect(log).toEqual(['Error: Transport error: 404', 'Error: Transport error: 404']))
})

View file

@ -2,10 +2,13 @@ import { describe, expect, it, Mock, MockedObject, vi } from 'vitest'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test'
import { defaultProductionDc, hexDecodeToBuffer, LogManager } from '../../utils/index.js'
import { getPlatform } from '../../platform.js'
import { defaultProductionDc, LogManager } from '../../utils/index.js'
import { TransportState } from './abstract.js'
import { WebSocketTransport } from './websocket.js'
const p = getPlatform()
describe('WebSocketTransport', () => {
const create = async () => {
const fakeWs = vi.fn().mockImplementation(() => ({
@ -74,9 +77,9 @@ describe('WebSocketTransport', () => {
const socket = getLastSocket(ws)
await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready))
await t.send(hexDecodeToBuffer('00010203040506070809'))
await t.send(p.hexDecode('00010203040506070809'))
expect(socket.send).toHaveBeenCalledWith(hexDecodeToBuffer('af020630c8ef14bcf53af33853ea'))
expect(socket.send).toHaveBeenCalledWith(p.hexDecode('af020630c8ef14bcf53af33853ea'))
})
it('should correctly close', async () => {
@ -101,7 +104,7 @@ describe('WebSocketTransport', () => {
const socket = getLastSocket(ws)
await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready))
const data = hexDecodeToBuffer('00010203040506070809')
const data = p.hexDecode('00010203040506070809')
const message = new MessageEvent('message', { data })
const onMessageCall = socket.addEventListener.mock.calls.find(([event]) => event === 'message') as unknown as [

View file

@ -0,0 +1,34 @@
import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { MtUnsupportedError } from './types/errors.js'
export interface ICorePlatform extends ITlPlatform {
beforeExit(fn: () => void): () => void
log(color: number, level: number, tag: string, fmt: string, args: unknown[]): void
getDefaultLogLevel(): number | null
getDeviceModel(): string
}
let _platform: ICorePlatform | null = null
export function setPlatform(platform: ICorePlatform): void {
if (_platform) {
if (_platform.constructor !== platform.constructor) {
throw new MtUnsupportedError('Platform may not be changed at runtime!')
}
return
}
_platform = platform
TlBinaryReader.platform = platform
TlBinaryWriter.platform = platform
}
export function getPlatform(): ICorePlatform {
if (!_platform) {
throw new MtUnsupportedError('Platform is not set! Have you instantiated the client?')
}
return _platform
}

View file

@ -1,6 +1,5 @@
export * from './driver.js'
export * from './memory/index.js'
export * from './provider.js'
export * from './providers/idb/index.js'
export * from './providers/memory/index.js'
export * from './repository/index.js'
export * from './storage.js'

View file

@ -1,4 +1,4 @@
import { IStorageDriver } from '../../driver.js'
import { IStorageDriver } from '../driver.js'
export class MemoryStorageDriver implements IStorageDriver {
readonly states: Map<string, object> = new Map()

View file

@ -1,5 +1,5 @@
import { ITelegramStorageProvider } from '../../../highlevel/storage/provider.js'
import { IMtStorageProvider } from '../../provider.js'
import { ITelegramStorageProvider } from '../../highlevel/storage/provider.js'
import { IMtStorageProvider } from '../provider.js'
import { MemoryStorageDriver } from './driver.js'
import { MemoryAuthKeysRepository } from './repository/auth-keys.js'
import { MemoryKeyValueRepository } from './repository/kv.js'

View file

@ -1,4 +1,4 @@
import { IAuthKeysRepository } from '../../../repository/auth-keys.js'
import { IAuthKeysRepository } from '../../repository/auth-keys.js'
import { MemoryStorageDriver } from '../driver.js'
interface AuthKeysState {

View file

@ -1,4 +1,4 @@
import { IKeyValueRepository } from '../../../repository/key-value.js'
import { IKeyValueRepository } from '../../repository/key-value.js'
import { MemoryStorageDriver } from '../driver.js'
export class MemoryKeyValueRepository implements IKeyValueRepository {

View file

@ -1,4 +1,4 @@
import { IPeersRepository } from '../../../../highlevel/storage/repository/peers.js'
import { IPeersRepository } from '../../../highlevel/storage/repository/peers.js'
import { MemoryStorageDriver } from '../driver.js'
interface PeersState {

View file

@ -1,4 +1,4 @@
import { IReferenceMessagesRepository } from '../../../../highlevel/storage/repository/ref-messages.js'
import { IReferenceMessagesRepository } from '../../../highlevel/storage/repository/ref-messages.js'
import { MemoryStorageDriver } from '../driver.js'
interface RefMessagesState {

View file

@ -2,7 +2,7 @@ import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { LogManager } from '../../utils/logger.js'
import { MemoryStorageDriver } from '../providers/memory/driver.js'
import { MemoryStorageDriver } from '../memory/driver.js'
import { ServiceOptions } from './base.js'
export function testServiceOptions(): ServiceOptions {

View file

@ -1,6 +1,7 @@
import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { asyncResettable, beforeExit } from '../utils/index.js'
import { getPlatform } from '../platform.js'
import { asyncResettable } from '../utils/index.js'
import { Logger } from '../utils/logger.js'
import { IMtStorageProvider } from './provider.js'
import { AuthKeysService } from './service/auth-keys.js'
@ -62,7 +63,7 @@ export class StorageManager {
this.driver.setup?.(this.log)
if (this.options.cleanup ?? true) {
this._cleanupRestore = beforeExit(() => {
this._cleanupRestore = getPlatform().beforeExit(() => {
this._destroy().catch((err) => this.log.error('cleanup error: %s', err))
})
}

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import { defaultTestCryptoProvider } from '@mtcute/test'
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import {
bigIntBitLength,
bigIntGcd,
@ -16,6 +16,8 @@ import {
twoMultiplicity,
} from './index.js'
const p = getPlatform()
describe('bigIntBitLength', () => {
it('should correctly calculate bit length', () => {
expect(bigIntBitLength(0n)).eq(0)
@ -33,7 +35,7 @@ describe('bigIntToBuffer', () => {
expect([...bigIntToBuffer(BigInt('10495708'), 8, false)]).eql([0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x26, 0xdc])
expect([...bigIntToBuffer(BigInt('3038102549'), 4, false)]).eql([0xb5, 0x15, 0xc4, 0x15])
expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, false)]).eql([
...hexDecodeToBuffer('81A33C81D2020550'),
...p.hexDecode('81A33C81D2020550'),
])
})
@ -43,12 +45,12 @@ describe('bigIntToBuffer', () => {
expect([...bigIntToBuffer(BigInt('10495708'), 8, true)]).eql([0xdc, 0x26, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00])
expect([...bigIntToBuffer(BigInt('3038102549'), 4, true)]).eql([0x15, 0xc4, 0x15, 0xb5])
expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, true)]).eql([
...hexDecodeToBuffer('81A33C81D2020550').reverse(),
...p.hexDecode('81A33C81D2020550').reverse(),
])
})
it('should handle large integers', () => {
const buf = hexDecodeToBuffer(
const buf = p.hexDecode(
'1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538',
)
const num = BigInt(
@ -74,7 +76,7 @@ describe('bufferToBigInt', () => {
})
it('should handle large integers', () => {
const buf = hexDecodeToBuffer(
const buf = p.hexDecode(
'1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538',
)
const num = BigInt(

View file

@ -1,13 +1,13 @@
// all available libraries either suck or are extremely large for the use case, so i made my own~
import { base64DecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
/**
* Parses a single PEM block to buffer.
* In fact just strips begin/end tags and parses the rest as Base64
*/
export function parsePemContents(pem: string): Uint8Array {
return base64DecodeToBuffer(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, ''))
return getPlatform().base64Decode(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, ''))
}
// based on https://git.coolaj86.com/coolaj86/asn1-parser.js/src/branch/master/asn1-parser.js
@ -66,7 +66,7 @@ export function parseAsn1(data: Uint8Array): Asn1Object {
if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length
// I think that buf->hex->int solves the problem of Endianness... not sure
asn1.length = parseInt(hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16)
asn1.length = parseInt(getPlatform().hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16)
index += asn1.lengthSize
}

View file

@ -55,5 +55,3 @@ export abstract class BaseCryptoProvider {
return buf
}
}
export type CryptoProviderFactory = () => ICryptoProvider

View file

@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest'
import { defaultCryptoProvider } from '@mtcute/test'
import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js'
import { factorizePQSync } from './factorization.js'
import { defaultCryptoProviderFactory } from './index.js'
describe('prime factorization', () => {
const testFactorization = (pq: bigint, p: bigint, q: bigint) => {
const [p_, q_] = factorizePQSync(defaultCryptoProviderFactory(), bigIntToBuffer(pq))
const [p_, q_] = factorizePQSync(defaultCryptoProvider, bigIntToBuffer(pq))
expect(bufferToBigInt(p_)).toBe(p)
expect(bufferToBigInt(q_)).toBe(q)
}

View file

@ -5,8 +5,4 @@ export * from './miller-rabin.js'
export * from './mtproto.js'
export * from './password.js'
export * from './utils.js'
import { _defaultCryptoProviderFactory } from '../platform/crypto.js'
import { CryptoProviderFactory } from './abstract.js'
export const defaultCryptoProviderFactory: CryptoProviderFactory = _defaultCryptoProviderFactory
export * from './wasm.js'

View file

@ -1,9 +1,10 @@
import { describe, expect, it } from 'vitest'
import { findKeyByFingerprints, parsePublicKey } from '../index.js'
import { NodeCryptoProvider } from './node.js'
import { defaultCryptoProvider } from '@mtcute/test'
const crypto = new NodeCryptoProvider()
import { findKeyByFingerprints, parsePublicKey } from '../index.js'
const crypto = defaultCryptoProvider
describe('parsePublicKey', () => {
it('should parse telegram public keys', () => {

View file

@ -1,8 +1,9 @@
import Long from 'long'
import { __publicKeyIndex as keysIndex, TlPublicKey } from '@mtcute/tl/binary/rsa-keys.js'
import { hexEncode, TlBinaryWriter } from '@mtcute/tl-runtime'
import { TlBinaryWriter } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { parseAsn1, parsePemContents } from '../binary/asn1-parser.js'
import { ICryptoProvider } from './abstract.js'
@ -25,13 +26,15 @@ export function parsePublicKey(crypto: ICryptoProvider, key: string, old = false
writer.bytes(modulus)
writer.bytes(exponent)
const platform = getPlatform()
const data = writer.result()
const sha = crypto.sha1(data)
const fp = hexEncode(sha.slice(-8).reverse())
const fp = platform.hexEncode(sha.slice(-8).reverse())
return {
modulus: hexEncode(modulus),
exponent: hexEncode(exponent),
modulus: platform.hexEncode(modulus),
exponent: platform.hexEncode(exponent),
fingerprint: fp,
old,
}

View file

@ -1,13 +1,14 @@
import { describe, expect, it } from 'vitest'
import { defaultCryptoProviderFactory } from './index.js'
import { defaultCryptoProvider } from '@mtcute/test'
import { millerRabin } from './miller-rabin.js'
describe(
'miller-rabin test',
function () {
// miller-rabin factorization relies on RNG, so we should use a real random number generator
const c = defaultCryptoProviderFactory()
const c = defaultCryptoProvider
const testMillerRabin = (n: number | string | bigint, isPrime: boolean) => {
expect(millerRabin(c, BigInt(n))).eq(isPrime)

View file

@ -2,12 +2,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test'
import { concatBuffers, hexDecodeToBuffer, hexEncode } from '../index.js'
import { getPlatform } from '../../platform.js'
import { concatBuffers } from '../index.js'
import { createAesIgeForMessage, createAesIgeForMessageOld, generateKeyAndIvFromNonce } from './mtproto.js'
const authKeyChunk = hexDecodeToBuffer('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
const p = getPlatform()
const authKeyChunk = p.hexDecode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
const authKey = concatBuffers(Array.from({ length: 8 }, () => authKeyChunk))
const messageKey = hexDecodeToBuffer('25d701f2a29205526757825a99eb2d32')
const messageKey = p.hexDecode('25d701f2a29205526757825a99eb2d32')
describe('mtproto 2.0', async () => {
const crypto = await defaultTestCryptoProvider()
@ -18,10 +21,10 @@ describe('mtproto 2.0', async () => {
it('should correctly derive message key and iv for client', () => {
createAesIgeForMessage(crypto, authKey, messageKey, true)
expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
'af3f8e1ffa75f4c981eec33a3e5bbaa2ea48f9bb93e91597627eb1f67960a0c9',
)
expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
'9874d77f95155b35221bff94b7df4594c6996e2a62e44fcb7d93c8c4e41b79ee',
)
})
@ -29,10 +32,10 @@ describe('mtproto 2.0', async () => {
it('should correctly derive message key and iv for server', () => {
createAesIgeForMessage(crypto, authKey, messageKey, false)
expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
'd4b378e1e0525f10ff9d4c42807ccce5b30a033a8088c0b922b5259421751648',
)
expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
'4d7194f42f0135d2fd83050b403265b4c40ee3e9e9fba56f0f4d8ea6bcb121f5',
)
})
@ -47,10 +50,10 @@ describe('mtproto 1.0', async () => {
it('should correctly derive message key and iv for client', () => {
createAesIgeForMessageOld(crypto, authKey, messageKey, true)
expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
'1fc7b40b1d9ffbdaf4d652525a748864259698f89214abf27c0d36cb9d4cd5db',
)
expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
'7251fbda39ec5e6e089f15ded5963b03d6d8d0f7078898431fc7b40b1d9ffbda',
)
})
@ -58,10 +61,10 @@ describe('mtproto 1.0', async () => {
it('should correctly derive message key and iv for server', () => {
createAesIgeForMessageOld(crypto, authKey, messageKey, false)
expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual(
'af0e4e01318654be40ab42b125909d43b44bdeef571ff1a5dfb81474ae26d467',
)
expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual(
'15c9ba6021d2c5cf04f0842540ae216a970b4eac8f46ef01af0e4e01318654be',
)
})

View file

@ -3,17 +3,19 @@ import { describe, expect, it } from 'vitest'
import { defaultTestCryptoProvider } from '@mtcute/test'
import { tl } from '@mtcute/tl'
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { computeNewPasswordHash, computePasswordHash, computeSrpParams } from './index.js'
const p = getPlatform()
// a real-world request from an account with "qwe123" password
const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = {
_: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
salt1: hexDecodeToBuffer('9b3accc457c0d5288e8cff31eb21094048bc11902f6614dbb9afb839ee7641c37619537d8ebe749e'),
salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
salt1: p.hexDecode('9b3accc457c0d5288e8cff31eb21094048bc11902f6614dbb9afb839ee7641c37619537d8ebe749e'),
salt2: p.hexDecode('6c619bb0786dc4ed1bf211d23f6e4065'),
g: 3,
p: hexDecodeToBuffer(
p: p.hexDecode(
'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
'48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
'20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
@ -30,7 +32,7 @@ const fakeRequest: tl.account.RawPassword = {
hasSecureValues: false,
hasPassword: true,
currentAlgo: fakeAlgo,
srpB: hexDecodeToBuffer(
srpB: p.hexDecode(
'1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' +
'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' +
'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' +
@ -43,10 +45,10 @@ const fakeRequest: tl.account.RawPassword = {
srpId: Long.fromBits(-2046015018, 875006452),
newAlgo: {
_: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
salt1: hexDecodeToBuffer('9b3accc457c0d528'),
salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
salt1: p.hexDecode('9b3accc457c0d528'),
salt2: p.hexDecode('6c619bb0786dc4ed1bf211d23f6e4065'),
g: 3,
p: hexDecodeToBuffer(
p: p.hexDecode(
'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
'48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
'20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
@ -59,7 +61,7 @@ const fakeRequest: tl.account.RawPassword = {
},
newSecureAlgo: {
_: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000',
salt: hexDecodeToBuffer('fdd59abc0bffb24d'),
salt: p.hexDecode('fdd59abc0bffb24d'),
},
secureRandom: new Uint8Array(), // unused
}
@ -68,16 +70,16 @@ const password = 'qwe123'
describe('SRP', () => {
it('should correctly compute password hash as defined by MTProto', async () => {
const crypto = await defaultTestCryptoProvider()
const hash = await computePasswordHash(crypto, utf8EncodeToBuffer(password), fakeAlgo.salt1, fakeAlgo.salt2)
const hash = await computePasswordHash(crypto, p.utf8Encode(password), fakeAlgo.salt1, fakeAlgo.salt2)
expect(hexEncode(hash)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b')
expect(p.hexEncode(hash)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b')
})
it('should correctly compute new password hash as defined by MTProto', async () => {
const crypto = await defaultTestCryptoProvider()
const hash = await computeNewPasswordHash(crypto, fakeAlgo, '123qwe')
expect(hexEncode(hash)).toEqual(
expect(p.hexEncode(hash)).toEqual(
'2540539ceeffd4543cd845bf319b8392e6b17bf7cf26bafcf6282ce9ae795368' +
'4ff49469c2863b17e6d65ddb16ae6f60bc07cc254c00e5ba389292f6cea0b3aa' +
'c459d1d08984d65319df8c5d124042169bbe2ab8c0c93bc7178827f2ea84e7c3' +
@ -94,7 +96,7 @@ describe('SRP', () => {
const params = await computeSrpParams(crypto, fakeRequest, password)
expect(params.srpId).toEqual(fakeRequest.srpId)
expect(hexEncode(params.A)).toEqual(
expect(p.hexEncode(params.A)).toEqual(
'363976f55edb57cc5cc0c4aaca9b7539eff98a43a93fa84be34860d18ac3a80f' +
'ffd57c4617896ff667677d0552a079eb189d25d147ec96edd4495c946a18652d' +
'31d78eede40a8b29da340c19b32ccac78f8482406e392102c03d850d1db87223' +
@ -104,6 +106,6 @@ describe('SRP', () => {
'4fa454aa69d9219d9c5fa3625f5c6f1ac03892a70aa17269c76cd9bf2949a961' +
'fad2a71e5fa961824b32db037130c7e9aad4c1e9f02ebc5b832622f98b59597e',
)
expect(hexEncode(params.M1)).toEqual('25a91b21c634ad670a144165a9829192d152e131a716f676abc48cd817f508c6')
expect(p.hexEncode(params.M1)).toEqual('25a91b21c634ad670a144165a9829192d152e131a716f676abc48cd817f508c6')
})
})

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js'
import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js'
import { concatBuffers } from '../buffer-utils.js'
@ -49,7 +49,7 @@ export async function computeNewPasswordHash(
crypto.randomFill(salt1.subarray(algo.salt1.length))
;(algo as tl.Mutable<typeof algo>).salt1 = salt1
const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2)
const _x = await computePasswordHash(crypto, getPlatform().utf8Encode(password), algo.salt1, algo.salt2)
const g = BigInt(algo.g)
const p = bufferToBigInt(algo.p)
@ -103,7 +103,7 @@ export async function computeSrpParams(
const _k = crypto.sha256(concatBuffers([algo.p, _g]))
const _u = crypto.sha256(concatBuffers([_gA, request.srpB]))
const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2)
const _x = await computePasswordHash(crypto, getPlatform().utf8Encode(password), algo.salt1, algo.salt2)
const k = bufferToBigInt(_k)
const u = bufferToBigInt(_u)
const x = bufferToBigInt(_x)

View file

@ -1,61 +1,62 @@
import { describe, expect, it } from 'vitest'
import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { xorBuffer, xorBufferInPlace } from './utils.js'
const p = getPlatform()
describe('xorBuffer', () => {
it('should xor buffers without modifying original', () => {
const data = utf8EncodeToBuffer('hello')
const key = utf8EncodeToBuffer('xor')
const data = p.utf8Encode('hello')
const key = p.utf8Encode('xor')
const xored = xorBuffer(data, key)
expect(utf8Decode(data)).eq('hello')
expect(utf8Decode(key)).eq('xor')
expect(hexEncode(xored)).eq('100a1e6c6f')
expect(p.utf8Decode(data)).eq('hello')
expect(p.utf8Decode(key)).eq('xor')
expect(p.hexEncode(xored)).eq('100a1e6c6f')
})
it('should be deterministic', () => {
const data = utf8EncodeToBuffer('hello')
const key = utf8EncodeToBuffer('xor')
const data = p.utf8Encode('hello')
const key = p.utf8Encode('xor')
const xored1 = xorBuffer(data, key)
expect(hexEncode(xored1)).eq('100a1e6c6f')
expect(p.hexEncode(xored1)).eq('100a1e6c6f')
const xored2 = xorBuffer(data, key)
expect(hexEncode(xored2)).eq('100a1e6c6f')
expect(p.hexEncode(xored2)).eq('100a1e6c6f')
})
it('second call should decode content', () => {
const data = utf8EncodeToBuffer('hello')
const key = utf8EncodeToBuffer('xor')
const data = p.utf8Encode('hello')
const key = p.utf8Encode('xor')
const xored1 = xorBuffer(data, key)
expect(hexEncode(xored1)).eq('100a1e6c6f')
expect(p.hexEncode(xored1)).eq('100a1e6c6f')
const xored2 = xorBuffer(xored1, key)
expect(utf8Decode(xored2)).eq('hello')
expect(p.utf8Decode(xored2)).eq('hello')
})
})
describe('xorBufferInPlace', () => {
it('should xor buffers by modifying original', () => {
const data = utf8EncodeToBuffer('hello')
const key = utf8EncodeToBuffer('xor')
const data = p.utf8Encode('hello')
const key = p.utf8Encode('xor')
xorBufferInPlace(data, key)
expect(hexEncode(data)).eq('100a1e6c6f')
expect(utf8Decode(key)).eq('xor')
expect(p.hexEncode(data)).eq('100a1e6c6f')
expect(p.utf8Decode(key)).eq('xor')
})
it('second call should decode content', () => {
const data = utf8EncodeToBuffer('hello')
const key = utf8EncodeToBuffer('xor')
const data = p.utf8Encode('hello')
const key = p.utf8Encode('xor')
xorBufferInPlace(data, key)
expect(hexEncode(data)).eq('100a1e6c6f')
expect(p.hexEncode(data)).eq('100a1e6c6f')
xorBufferInPlace(data, key)
expect(utf8Decode(data)).eq('hello')
expect(p.utf8Decode(data)).eq('hello')
})
})

View file

@ -6,36 +6,15 @@ import {
gunzip,
ige256Decrypt,
ige256Encrypt,
initAsync,
InitInput,
sha1,
sha256,
} from '@mtcute/wasm'
import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from './abstract.js'
export interface WasmCryptoProviderOptions {
/**
* WASM blob to use for crypto operations.
*
* Must conform to `@mtcute/wasm` interface.
*/
wasmInput?: InitInput
}
export abstract class WasmCryptoProvider extends BaseCryptoProvider implements Partial<ICryptoProvider> {
readonly wasmInput?: InitInput
abstract randomFill(buf: Uint8Array): void
constructor(params?: WasmCryptoProviderOptions) {
super()
this.wasmInput = params?.wasmInput
}
initialize(): Promise<void> {
return initAsync(this.wasmInput)
}
abstract initialize(): Promise<void>
sha1(data: Uint8Array): Uint8Array {
return sha1(data)

View file

@ -49,7 +49,6 @@ export interface DcOptions {
media: BasicDcOption
}
/** @internal */
export const defaultProductionDc: DcOptions = {
main: {
ipAddress: '149.154.167.50',
@ -64,7 +63,6 @@ export const defaultProductionDc: DcOptions = {
},
}
/** @internal */
export const defaultProductionIpv6Dc: DcOptions = {
main: {
ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000a',
@ -81,7 +79,6 @@ export const defaultProductionIpv6Dc: DcOptions = {
},
}
/** @internal */
export const defaultTestDc: DcOptions = {
main: {
ipAddress: '149.154.167.40',
@ -96,7 +93,6 @@ export const defaultTestDc: DcOptions = {
},
}
/** @internal */
export const defaultTestIpv6Dc: DcOptions = {
main: {
ipAddress: '2001:67c:4e8:f002::e',

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import { Logger } from '../logger.js'
import { Logger } from './logger.js'
export function reportUnknownError(log: Logger, error: tl.RpcError, method: string): void {
if (typeof fetch !== 'function') return

View file

@ -1,4 +1,10 @@
export * from '../highlevel/utils/index.js'
// todo: remove after 1.0.0
export * from '../highlevel/storage/service/current-user.js'
export * from '../highlevel/storage/service/updates.js'
export * from '../storage/service/base.js'
export * from '../storage/service/default-dcs.js'
// end todo
export * from './async-lock.js'
export * from './bigint-utils.js'
export * from './buffer-utils.js'
@ -17,7 +23,6 @@ export * from './lru-map.js'
export * from './lru-set.js'
export * from './misc-utils.js'
export * from './peer-utils.js'
export * from './platform/exit-hook.js'
export * from './sorted-array.js'
export * from './tl-json.js'
export * from './type-assertions.js'

View file

@ -1,25 +1,6 @@
import { hexEncode } from '@mtcute/tl-runtime'
import { _defaultLoggingHandler } from './platform/logging.js'
let defaultLogLevel = 3
/* c8 ignore start */
if (typeof process !== 'undefined') {
const envLogLevel = parseInt(process.env.MTCUTE_LOG_LEVEL ?? '')
if (!isNaN(envLogLevel)) {
defaultLogLevel = envLogLevel
}
} else if (typeof localStorage !== 'undefined') {
const localLogLevel = parseInt(localStorage.MTCUTE_LOG_LEVEL as string)
if (!isNaN(localLogLevel)) {
defaultLogLevel = localLogLevel
}
}
/* c8 ignore end */
import { getPlatform } from '../platform.js'
const DEFAULT_LOG_LEVEL = 3
const FORMATTER_RE = /%[a-zA-Z]/g
/**
@ -81,7 +62,7 @@ export class Logger {
args.splice(idx, 1)
if (m === '%h') {
if (ArrayBuffer.isView(val)) return hexEncode(val as Uint8Array)
if (ArrayBuffer.isView(val)) return this.mgr.platform.hexEncode(val as Uint8Array)
if (typeof val === 'number' || typeof val === 'bigint') return val.toString(16)
return String(val)
@ -96,10 +77,10 @@ export class Logger {
return JSON.stringify(val, (k, v) => {
if (
ArrayBuffer.isView(v) ||
(typeof v === 'object' && v.type === 'Buffer' && Array.isArray(v.data))
(typeof v === 'object' && v.type === 'Buffer' && Array.isArray(v.data)) // todo: how can we do this better?
) {
// eslint-disable-next-line
let str = v.data ? Buffer.from(v.data as number[]).toString('hex') : hexEncode(v)
let str = v.data ? Buffer.from(v.data as number[]).toString('hex') : this.mgr.platform.hexEncode(v)
if (str.length > 300) {
str = str.slice(0, 300) + '...'
@ -171,8 +152,10 @@ export class LogManager extends Logger {
private _filter: (tag: string) => boolean = defaultFilter
level = defaultLogLevel
handler = _defaultLoggingHandler
readonly platform = getPlatform()
level = this.platform.getDefaultLogLevel() ?? DEFAULT_LOG_LEVEL
handler = this.platform.log.bind(this.platform)
/**
* Create a {@link Logger} with the given tag

View file

@ -1,4 +0,0 @@
import { NodeCryptoProvider } from '../crypto/node.js'
/** @internal */
export const _defaultCryptoProviderFactory = () => new NodeCryptoProvider()

View file

@ -1,11 +0,0 @@
import { MtUnsupportedError } from '../../index.js'
import { WebCryptoProvider } from '../crypto/web.js'
/** @internal */
export const _defaultCryptoProviderFactory = () => {
if (typeof crypto === 'undefined' || typeof crypto.subtle === 'undefined') {
throw new MtUnsupportedError('WebCrypto API is not available')
}
return new WebCryptoProvider({ crypto })
}

View file

@ -1,4 +0,0 @@
import { TcpTransport } from '../../network/transports/tcp.js'
/** @internal */
export const _defaultTransportFactory = () => new TcpTransport()

View file

@ -1,14 +0,0 @@
import { WebSocketTransport } from '../../network/transports/websocket.js'
import { MtUnsupportedError } from '../../types/index.js'
/** @internal */
export const _defaultTransportFactory =
// if no websocket, throw an error i guess ¯\_(ツ)_/¯
// (user can still implement something on their own)
typeof WebSocket === 'undefined' ?
() => {
throw new MtUnsupportedError(
'Neither TCP nor WebSocket are available. Please pass a Transport factory explicitly',
)
} :
() => new WebSocketTransport()

View file

@ -19,20 +19,24 @@
"keepScripts": [
"install"
],
"exports": {
".": "./src/index.ts",
"./native.js": "./src/native.ts"
},
"distOnlyFields": {
"exports": {
".": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
},
"./native": {
"./native.js": {
"import": "./esm/native.cjs",
"require": "./cjs/native.cjs"
}
}
},
"dependencies": {
"@mtcute/core": "workspace:^"
"@mtcute/node": "workspace:^"
},
"devDependencies": {
"@mtcute/test": "workspace:^"

View file

@ -1,5 +1,5 @@
import { BaseNodeCryptoProvider } from '@mtcute/core/src/utils/crypto/node.js'
import { IEncryptionScheme } from '@mtcute/core/utils.js'
import { BaseNodeCryptoProvider } from '@mtcute/node'
import { IEncryptionScheme } from '@mtcute/node/utils.js'
import { native } from './native.cjs'

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { CallbackQuery, MtArgumentError, PeersIndex } from '@mtcute/core'
import { utf8EncodeToBuffer } from '@mtcute/core/utils.js'
import { getPlatform } from '@mtcute/core/platform.js'
import { createStub } from '@mtcute/test'
import { CallbackDataBuilder } from './callback-data-builder.js'
@ -53,7 +53,7 @@ describe('CallbackDataBuilder', () => {
const createCb = (data: string) =>
new CallbackQuery(
createStub('updateBotCallbackQuery', {
data: utf8EncodeToBuffer(data),
data: getPlatform().utf8Encode(data),
}),
new PeersIndex(),
)

View file

@ -1,4 +1,5 @@
import { ParsedUpdate, TelegramClient } from '@mtcute/core'
import { ParsedUpdate } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
export type UpdateContext<T> = T & {
client: TelegramClient

View file

@ -1,4 +1,5 @@
import { CallbackQuery, InlineCallbackQuery, MaybePromise, Message, TelegramClient } from '@mtcute/core'
import { CallbackQuery, InlineCallbackQuery, MaybePromise, Message } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './base.js'

View file

@ -1,4 +1,5 @@
import { BotChatJoinRequestUpdate, TelegramClient } from '@mtcute/core'
import { BotChatJoinRequestUpdate } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './base.js'

View file

@ -1,4 +1,5 @@
import { ChosenInlineResult, MtArgumentError, TelegramClient } from '@mtcute/core'
import { ChosenInlineResult, MtArgumentError } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './base.js'

View file

@ -1,4 +1,5 @@
import { InlineQuery, ParametersSkip1, TelegramClient } from '@mtcute/core'
import { InlineQuery, ParametersSkip1 } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './base.js'

View file

@ -1,9 +1,6 @@
import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer, TelegramClient } from '@mtcute/core'
// todo: fix these imports when packaging
import { DeleteMessagesParams } from '@mtcute/core/src/highlevel/methods/messages/delete-messages.js'
import { ForwardMessageOptions } from '@mtcute/core/src/highlevel/methods/messages/forward-messages.js'
import { SendCopyParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy.js'
import { SendCopyGroupParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy-group.js'
import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { DeleteMessagesParams, ForwardMessageOptions, SendCopyGroupParams, SendCopyParams } from '@mtcute/core/methods.js'
import { UpdateContext } from './base.js'

View file

@ -1,4 +1,5 @@
import { ParsedUpdate, TelegramClient } from '@mtcute/core'
import { ParsedUpdate } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContextDistributed } from './base.js'
import { CallbackQueryContext } from './callback-query.js'

View file

@ -1,4 +1,5 @@
import { PreCheckoutQuery, TelegramClient } from '@mtcute/core'
import { PreCheckoutQuery } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './base.js'

View file

@ -19,11 +19,11 @@ import {
PollUpdate,
PollVoteUpdate,
StoryUpdate,
TelegramClient,
tl,
UserStatusUpdate,
UserTypingUpdate,
} from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './context/base.js'
import {

View file

@ -12,11 +12,11 @@ import {
PollUpdate,
PollVoteUpdate,
StoryUpdate,
TelegramClient,
tl,
UserStatusUpdate,
UserTypingUpdate,
} from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './context/base.js'
import {

View file

@ -21,5 +21,8 @@
"dependencies": {
"@mtcute/tl-runtime": "workspace:^",
"long": "5.2.3"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
}
}

View file

@ -1,7 +1,7 @@
import Long from 'long'
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
import { defaultPlatform } from '@mtcute/test'
import { parseFileId } from './parse.js'
import { tdFileId as td } from './types.js'
@ -10,14 +10,14 @@ import { tdFileId as td } from './types.js'
describe('parsing file ids', () => {
const test = (id: string, expected: td.RawFullRemoteFileLocation) => {
expect(parseFileId(id)).eql(expected)
expect(parseFileId(defaultPlatform, id)).eql(expected)
}
it('parses common file ids', () => {
test('CAACAgIAAxkBAAEJny9gituz1_V_uSKBUuG_nhtzEtFOeQACXFoAAuCjggfYjw_KAAGSnkgfBA', {
_: 'remoteFileLocation',
dcId: 2,
fileReference: hexDecodeToBuffer('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'),
fileReference: defaultPlatform.hexDecode('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'),
location: {
_: 'common',
accessHash: Long.fromString('5232780349138767832'),
@ -28,7 +28,7 @@ describe('parsing file ids', () => {
test('BQACAgIAAxkBAAEJnzNgit00IDsKd07OdSeanwz8osecYAACdAwAAueoWEicaPvNdOYEwB8E', {
_: 'remoteFileLocation',
dcId: 2,
fileReference: hexDecodeToBuffer('0100099f33608add34203b0a774ece75279a9f0cfca2c79c60'),
fileReference: defaultPlatform.hexDecode('0100099f33608add34203b0a774ece75279a9f0cfca2c79c60'),
location: {
_: 'common',
accessHash: Long.fromString('-4610306729174144868'),
@ -42,7 +42,7 @@ describe('parsing file ids', () => {
test('AAMCAgADGQEAAQmfL2CK27PX9X-5IoFS4b-eG3MS0U55AAJcWgAC4KOCB9iPD8oAAZKeSK1c8w4ABAEAB20AA1kCAAIfBA', {
_: 'remoteFileLocation',
dcId: 2,
fileReference: hexDecodeToBuffer('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'),
fileReference: defaultPlatform.hexDecode('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'),
location: {
_: 'photo',
accessHash: Long.fromString('5232780349138767832'),

View file

@ -1,4 +1,4 @@
import { base64DecodeToBuffer, base64Encode, TlBinaryReader } from '@mtcute/tl-runtime'
import { ITlPlatform, TlBinaryReader } from '@mtcute/tl-runtime'
import { tdFileId as td } from './types.js'
import { telegramRleDecode } from './utils.js'
@ -11,7 +11,7 @@ function parseWebFileLocation(reader: TlBinaryReader): td.RawWebRemoteFileLocati
}
}
function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource {
function parsePhotoSizeSource(platform: ITlPlatform, reader: TlBinaryReader): td.TypePhotoSizeSource {
const variant = reader.int()
switch (variant) {
@ -24,14 +24,16 @@ function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource {
const fileType = reader.int()
if (fileType < 0 || fileType >= td.FileType.Size) {
throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(reader.uint8View)})`)
throw new td.UnsupportedError(
`Unsupported file type: ${fileType} (${platform.base64Encode(reader.uint8View)})`,
)
}
const thumbnailType = reader.int()
if (thumbnailType < 0 || thumbnailType > 255) {
throw new td.InvalidFileIdError(
`Wrong thumbnail type: ${thumbnailType} (${base64Encode(reader.uint8View)})`,
`Wrong thumbnail type: ${thumbnailType} (${platform.base64Encode(reader.uint8View)})`,
)
}
@ -110,24 +112,28 @@ function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource {
}
default:
throw new td.UnsupportedError(
`Unsupported photo size source ${variant} (${base64Encode(reader.uint8View)})`,
`Unsupported photo size source ${variant} (${platform.base64Encode(reader.uint8View)})`,
)
}
}
function parsePhotoFileLocation(reader: TlBinaryReader, version: number): td.RawPhotoRemoteFileLocation {
function parsePhotoFileLocation(
platform: ITlPlatform,
reader: TlBinaryReader,
version: number,
): td.RawPhotoRemoteFileLocation {
const id = reader.long()
const accessHash = reader.long()
let source: td.TypePhotoSizeSource
if (version >= 32) {
source = parsePhotoSizeSource(reader)
source = parsePhotoSizeSource(platform, reader)
} else {
const volumeId = reader.long()
let localId = 0
if (version >= 22) {
source = parsePhotoSizeSource(reader)
source = parsePhotoSizeSource(platform, reader)
localId = reader.int()
} else {
source = {
@ -190,9 +196,9 @@ function parseCommonFileLocation(reader: TlBinaryReader): td.RawCommonRemoteFile
}
}
function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRemoteFileLocation {
function fromPersistentIdV23(platform: ITlPlatform, binary: Uint8Array, version: number): td.RawFullRemoteFileLocation {
if (version < 0 || version > td.CURRENT_VERSION) {
throw new td.UnsupportedError(`Unsupported file ID v3 subversion: ${version} (${base64Encode(binary)})`)
throw new td.UnsupportedError(`Unsupported file ID v3 subversion: ${version} (${platform.base64Encode(binary)})`)
}
binary = telegramRleDecode(binary)
@ -208,7 +214,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem
fileType &= ~td.FILE_REFERENCE_FLAG
if (fileType < 0 || fileType >= td.FileType.Size) {
throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(binary)})`)
throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${platform.base64Encode(binary)})`)
}
const dcId = reader.int()
@ -237,7 +243,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem
case td.FileType.EncryptedThumbnail:
case td.FileType.Wallpaper: {
// location_type = photo
location = parsePhotoFileLocation(reader, version)
location = parsePhotoFileLocation(platform, reader, version)
// validate
switch (location.source._) {
@ -287,7 +293,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem
break
}
default:
throw new td.UnsupportedError(`Invalid file type: ${fileType} (${base64Encode(binary)})`)
throw new td.UnsupportedError(`Invalid file type: ${fileType} (${platform.base64Encode(binary)})`)
}
}
@ -300,14 +306,14 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem
}
}
function fromPersistentIdV2(binary: Uint8Array) {
return fromPersistentIdV23(binary.subarray(0, -1), 0)
function fromPersistentIdV2(platform: ITlPlatform, binary: Uint8Array) {
return fromPersistentIdV23(platform, binary.subarray(0, -1), 0)
}
function fromPersistentIdV3(binary: Uint8Array) {
function fromPersistentIdV3(platform: ITlPlatform, binary: Uint8Array) {
const subversion = binary[binary.length - 2]
return fromPersistentIdV23(binary.subarray(0, -2), subversion)
return fromPersistentIdV23(platform, binary.subarray(0, -2), subversion)
}
/**
@ -315,18 +321,18 @@ function fromPersistentIdV3(binary: Uint8Array) {
*
* @param fileId File ID as a base-64 encoded string or Buffer
*/
export function parseFileId(fileId: string | Uint8Array): td.RawFullRemoteFileLocation {
if (typeof fileId === 'string') fileId = base64DecodeToBuffer(fileId, true)
export function parseFileId(platform: ITlPlatform, fileId: string | Uint8Array): td.RawFullRemoteFileLocation {
if (typeof fileId === 'string') fileId = platform.base64Decode(fileId, true)
const version = fileId[fileId.length - 1]
if (version === td.PERSISTENT_ID_VERSION_OLD) {
return fromPersistentIdV2(fileId)
return fromPersistentIdV2(platform, fileId)
}
if (version === td.PERSISTENT_ID_VERSION) {
return fromPersistentIdV3(fileId)
return fromPersistentIdV3(platform, fileId)
}
throw new td.UnsupportedError(`Unsupported file ID version: ${version} (${base64Encode(fileId)})`)
throw new td.UnsupportedError(`Unsupported file ID version: ${version} (${platform.base64Encode(fileId)})`)
}

View file

@ -1,5 +1,7 @@
import { describe, expect, it } from 'vitest'
import { defaultPlatform } from '@mtcute/test'
import { parseFileId } from './parse.js'
import { toUniqueFileId } from './serialize-unique.js'
@ -7,7 +9,7 @@ import { toUniqueFileId } from './serialize-unique.js'
describe('serializing unique file ids', () => {
const test = (id: string, expected: string) => {
expect(toUniqueFileId(parseFileId(id))).eql(expected)
expect(toUniqueFileId(defaultPlatform, parseFileId(defaultPlatform, id))).eql(expected)
}
it('serializes unique ids for old file ids', () => {

View file

@ -1,4 +1,4 @@
import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime'
import { ITlPlatform, TlBinaryWriter } from '@mtcute/tl-runtime'
import { tdFileId as td } from './types.js'
import { assertNever, telegramRleEncode } from './utils.js'
@ -20,10 +20,11 @@ export type InputUniqueLocation =
*
* @param location Information about file location
*/
export function toUniqueFileId(location: Omit<td.RawFullRemoteFileLocation, '_'>): string
export function toUniqueFileId(type: td.FileType, location: InputUniqueLocation): string
export function toUniqueFileId(platform: ITlPlatform, location: Omit<td.RawFullRemoteFileLocation, '_'>): string
export function toUniqueFileId(platform: ITlPlatform, type: td.FileType, location: InputUniqueLocation): string
export function toUniqueFileId(
platform: ITlPlatform,
first: td.FileType | Omit<td.RawFullRemoteFileLocation, '_'>,
second?: InputUniqueLocation,
): string {
@ -140,7 +141,7 @@ export function toUniqueFileId(
break
}
case 'web':
writer = TlBinaryWriter.alloc(undefined, byteLengthUtf8(inputLocation.url) + 8)
writer = TlBinaryWriter.manual(platform.utf8ByteLength(inputLocation.url) + 8)
writer.int(type)
writer.string(inputLocation.url)
break
@ -153,5 +154,5 @@ export function toUniqueFileId(
assertNever(inputLocation)
}
return base64Encode(telegramRleEncode(writer.result()), true)
return platform.base64Encode(telegramRleEncode(writer.result()), true)
}

View file

@ -1,4 +1,4 @@
import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime'
import { ITlPlatform, TlBinaryWriter } from '@mtcute/tl-runtime'
import { tdFileId as td } from './types.js'
import { assertNever, telegramRleEncode } from './utils.js'
@ -11,7 +11,7 @@ const SUFFIX = new Uint8Array([td.CURRENT_VERSION, td.PERSISTENT_ID_VERSION])
*
* @param location Information about file location
*/
export function toFileId(location: Omit<td.RawFullRemoteFileLocation, '_'>): string {
export function toFileId(platform: ITlPlatform, location: Omit<td.RawFullRemoteFileLocation, '_'>): string {
const loc = location.location
let type: number = location.type
@ -25,7 +25,7 @@ export function toFileId(location: Omit<td.RawFullRemoteFileLocation, '_'>): str
//
// longest file ids are around 80 bytes, so i guess
// we are safe with allocating 100 bytes
const writer = TlBinaryWriter.alloc(undefined, loc._ === 'web' ? byteLengthUtf8(loc.url) + 32 : 100)
const writer = TlBinaryWriter.manual(loc._ === 'web' ? platform.utf8ByteLength(loc.url) + 32 : 100)
writer.int(type)
writer.int(location.dcId)
@ -108,5 +108,5 @@ export function toFileId(location: Omit<td.RawFullRemoteFileLocation, '_'>): str
withSuffix.set(result)
withSuffix.set(SUFFIX, result.length)
return base64Encode(withSuffix, true)
return platform.base64Encode(withSuffix, true)
}

View file

@ -1,32 +1,34 @@
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { defaultPlatform } from '@mtcute/test'
import { telegramRleDecode, telegramRleEncode } from './utils.js'
const p = defaultPlatform
describe('telegramRleEncode', () => {
it('should not modify input if there are no \\x00', () => {
expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('aaeeff')))).eq('aaeeff')
expect(p.hexEncode(telegramRleEncode(p.hexDecode('aaeeff')))).eq('aaeeff')
})
it('should collapse consecutive \\x00', () => {
expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa')))).eq('0004aa')
expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa000000aa')))).eq('0004aa0003aa')
expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa0000')))).eq('0004aa0002')
expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00aa00')))).eq('0001aa0001')
expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa')))).eq('0004aa')
expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa000000aa')))).eq('0004aa0003aa')
expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa0000')))).eq('0004aa0002')
expect(p.hexEncode(telegramRleEncode(p.hexDecode('00aa00')))).eq('0001aa0001')
})
})
describe('telegramRleDecode', () => {
it('should not mofify input if there are no \\x00', () => {
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('aaeeff')))).eq('aaeeff')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('aaeeff')))).eq('aaeeff')
})
it('should expand two-byte sequences starting with \\x00', () => {
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa')))).eq('00000000aa')
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0000')))).eq('00000000aa')
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0003aa')))).eq('00000000aa000000aa')
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0002')))).eq('00000000aa0000')
expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0001aa0001')))).eq('00aa00')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa')))).eq('00000000aa')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0000')))).eq('00000000aa')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0003aa')))).eq('00000000aa000000aa')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0002')))).eq('00000000aa0000')
expect(p.hexEncode(telegramRleDecode(p.hexDecode('0001aa0001')))).eq('00aa00')
})
})

View file

@ -1,9 +1,7 @@
import { connect as connectTcp } from 'net'
import { connect as connectTls, SecureContextOptions } from 'tls'
import { IntermediatePacketCodec, MtcuteError, tl, TransportState } from '@mtcute/core'
import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js'
import { base64Encode, utf8EncodeToBuffer } from '@mtcute/core/utils.js'
import { BaseTcpTransport, IntermediatePacketCodec, MtcuteError, NodePlatform, tl, TransportState } from '@mtcute/node'
/**
* An error has occurred while connecting to an HTTP(s) proxy
@ -71,6 +69,8 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport {
this._proxy = proxy
}
private _platform = new NodePlatform()
connect(dc: tl.RawDcOption): void {
if (this._state !== TransportState.Idle) {
throw new MtcuteError('Transport is not IDLE')
@ -110,7 +110,7 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport {
if (this._proxy.password) {
auth += ':' + this._proxy.password
}
headers['Proxy-Authorization'] = 'Basic ' + base64Encode(utf8EncodeToBuffer(auth))
headers['Proxy-Authorization'] = 'Basic ' + this._platform.base64Encode(this._platform.utf8Encode(auth))
}
headers['Proxy-Connection'] = 'Keep-Alive'

View file

@ -20,6 +20,6 @@
}
},
"dependencies": {
"@mtcute/core": "workspace:^"
"@mtcute/node": "workspace:^"
}
}

View file

@ -1,6 +1,6 @@
/* eslint-disable no-restricted-globals */
import { IPacketCodec, WrappedCodec } from '@mtcute/core'
import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/core/utils.js'
import { IPacketCodec, WrappedCodec } from '@mtcute/node'
import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/node/utils.js'
const MAX_TLS_PACKET_LENGTH = 2878
const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex')

View file

@ -4,6 +4,7 @@
import { connect } from 'net'
import {
BaseTcpTransport,
IntermediatePacketCodec,
IPacketCodec,
MtcuteError,
@ -13,9 +14,8 @@ import {
PaddedIntermediatePacketCodec,
tl,
TransportState,
} from '@mtcute/core'
import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js'
import { buffersEqual } from '@mtcute/core/utils.js'
} from '@mtcute/node'
import { buffersEqual } from '@mtcute/node/utils.js'
import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls.js'

View file

@ -20,6 +20,6 @@
}
},
"dependencies": {
"@mtcute/core": "workspace:^"
"@mtcute/node": "workspace:^"
}
}

View file

@ -5,12 +5,16 @@
"description": "Meta-package for Node JS",
"author": "Alina Sireneva <alina@tei.su>",
"license": "MIT",
"main": "index.ts",
"main": "src/index.ts",
"type": "module",
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package node"
},
"exports": {
".": "./src/index.ts",
"./utils.js": "./src/utils.ts"
},
"distOnlyFields": {
"exports": {
".": {
@ -25,8 +29,12 @@
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@mtcute/sqlite": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/html-parser": "workspace:^"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
}
}

View file

@ -1,13 +1,18 @@
import { createRequire } from 'module'
import { createInterface, Interface as RlInterface } from 'readline'
import { TelegramClient, TelegramClientOptions, User } from '@mtcute/core'
import { FileDownloadLocation, FileDownloadParameters, User } from '@mtcute/core'
import { TelegramClient as TelegramClientBase, TelegramClientOptions } from '@mtcute/core/client.js'
import { setPlatform } from '@mtcute/core/platform.js'
import { SqliteStorage } from '@mtcute/sqlite'
export * from '@mtcute/core'
export * from '@mtcute/html-parser'
export * from '@mtcute/markdown-parser'
export { SqliteStorage }
import { downloadToFile } from './methods/download-file.js'
import { uploadFile } from './methods/upload-file.js'
import { NodePlatform } from './platform.js'
import { NodeCryptoProvider } from './utils/crypto.js'
import { TcpTransport } from './utils/tcp.js'
export type { TelegramClientOptions }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let nativeCrypto: any
@ -21,14 +26,12 @@ try {
} catch (e) {}
/**
* Tiny wrapper over {@link TelegramClient} for usage inside Node JS.
*
* This class automatically manages native
* crypto addon and defaults to SQLite session (unlike `TelegarmClient`,
* which defaults to a JSON file on Node).
* Telegram client for use in Node.js
*/
export class NodeTelegramClient extends TelegramClient {
export class TelegramClient extends TelegramClientBase {
constructor(opts: TelegramClientOptions) {
setPlatform(new NodePlatform())
if ('client' in opts) {
super(opts)
@ -37,7 +40,8 @@ export class NodeTelegramClient extends TelegramClient {
super({
// eslint-disable-next-line
crypto: nativeCrypto ? () => new nativeCrypto() : undefined,
crypto: nativeCrypto ? new nativeCrypto() : new NodeCryptoProvider(),
transport: () => new TcpTransport(),
...opts,
storage:
typeof opts.storage === 'string' ?
@ -74,7 +78,7 @@ export class NodeTelegramClient extends TelegramClient {
return super.close()
}
start(params: Parameters<TelegramClient['start']>[0] = {}): Promise<User> {
start(params: Parameters<TelegramClientBase['start']>[0] = {}): Promise<User> {
if (!params.botToken) {
if (!params.phone) params.phone = () => this.input('phone > ')
if (!params.code) params.code = () => this.input('code > ')
@ -107,4 +111,16 @@ export class NodeTelegramClient extends TelegramClient {
.then(then)
.catch((err) => this.emitError(err))
}
downloadToFile(
filename: string,
location: FileDownloadLocation,
params?: FileDownloadParameters | undefined,
): Promise<void> {
return downloadToFile(this, filename, location, params)
}
uploadFile(params: Parameters<typeof uploadFile>[1]) {
return uploadFile(this, params)
}
}

View file

@ -0,0 +1,8 @@
export * from './client.js'
export * from './platform.js'
export * from './utils/tcp.js'
export * from './utils/crypto.js'
export * from '@mtcute/core'
export * from '@mtcute/html-parser'
export * from '@mtcute/markdown-parser'
export * from '@mtcute/sqlite'

View file

@ -0,0 +1,5 @@
/* eslint-disable import/export, simple-import-sort/exports */
export * from '@mtcute/core/methods.js'
export { downloadToFile } from './methods/download-file.js'
export { uploadFile } from './methods/upload-file.js'

View file

@ -0,0 +1,40 @@
import { createWriteStream, rmSync } from 'fs'
import { writeFile } from 'fs/promises'
import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core'
import { downloadAsIterable } from '@mtcute/core/methods.js'
/**
* Download a remote file to a local file (only for NodeJS).
* Promise will resolve once the download is complete.
*
* @param filename Local file name to which the remote file will be downloaded
* @param params File download parameters
*/
export async function downloadToFile(
client: ITelegramClient,
filename: string,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): Promise<void> {
if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) {
// early return for inline files
await writeFile(filename, location.location)
}
const output = createWriteStream(filename)
if (params?.abortSignal) {
params.abortSignal.addEventListener('abort', () => {
client.log.debug('aborting file download %s - cleaning up', filename)
output.destroy()
rmSync(filename)
})
}
for await (const chunk of downloadAsIterable(client, location, params)) {
output.write(chunk)
}
output.end()
}

Some files were not shown because too many files have changed in this diff Show more