diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 5b580c3e..1005d886 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -55,6 +55,8 @@ import { downloadAsBuffer } from './methods/files/download-buffer' import { downloadToFile } from './methods/files/download-file' import { downloadAsIterable } from './methods/files/download-iterable' import { downloadAsStream } from './methods/files/download-stream' +import { _normalizeInputFile } from './methods/files/normalize-input-file' +import { _normalizeInputMedia } from './methods/files/normalize-input-media' import { uploadFile } from './methods/files/upload-file' import { deleteMessages } from './methods/messages/delete-messages' import { editMessage } from './methods/messages/edit-message' @@ -70,7 +72,6 @@ import { searchMessages } from './methods/messages/search-messages' import { sendDice } from './methods/messages/send-dice' import { sendLocation } from './methods/messages/send-location' import { sendMedia } from './methods/messages/send-media' -import { sendPhoto } from './methods/messages/send-photo' import { sendText } from './methods/messages/send-text' import { unpinMessage } from './methods/messages/unpin-message' import { initTakeoutSession } from './methods/misc/init-takeout-session' @@ -1600,7 +1601,7 @@ export interface TelegramClient extends BaseTelegramClient { } ): Promise /** - * Send a single media. + * Send a single media (a photo or a document-based media) * * @param chatId ID of the chat, its username, phone or `"me"` or `"self"` * @param media @@ -1608,6 +1609,7 @@ export interface TelegramClient extends BaseTelegramClient { * and Bot API compatible File ID, which will be wrapped * in {@link InputMedia.auto} * @param params Additional sending parameters + * @see InputMedia */ sendMedia( chatId: InputPeerLike, @@ -1661,84 +1663,6 @@ export interface TelegramClient extends BaseTelegramClient { clearDraft?: boolean } ): Promise - /** - * Send a single photo - * - * @param chatId ID of the chat, its username, phone or `"me"` or `"self"` - * @param photo Photo contained in the message. - * @param params Additional sending parameters - */ - sendPhoto( - chatId: InputPeerLike, - photo: InputFileLike, - params?: { - /** - * Caption for the photo - */ - caption?: string - - /** - * Message to reply to. Either a message object or message ID. - */ - replyTo?: number | Message - - /** - * Parse mode to use to parse entities before sending - * the message. Defaults to current default parse mode (if any). - * - * Passing `null` will explicitly disable formatting. - */ - parseMode?: string | null - - /** - * List of formatting entities to use instead of parsing via a - * parse mode. - * - * **Note:** Passing this makes the method ignore {@link parseMode} - */ - entities?: tl.TypeMessageEntity[] - - /** - * Whether to send this message silently. - */ - silent?: boolean - - /** - * If set, the message will be scheduled to this date. - * When passing a number, a UNIX time in ms is expected. - */ - schedule?: Date | number - - /** - * For bots: inline or reply markup or an instruction - * to hide a reply keyboard or to force a reply. - */ - replyMarkup?: ReplyMarkup - - /** - * Self-Destruct timer. - * If set, the photo will self-destruct in a given number - * of seconds. - */ - ttlSeconds?: number - - /** - * Function that will be called after some part has been uploaded. - * Only used when a file that requires uploading is passed. - * - * @param uploaded Number of bytes already uploaded - * @param total Total file size - */ - progressCallback?: (uploaded: number, total: number) => void - - /** - * Whether to clear draft after sending this message. - * - * Defaults to `false` - */ - clearDraft?: boolean - } - ): Promise /** * Send a text message * @@ -2024,6 +1948,8 @@ export class TelegramClient extends BaseTelegramClient { downloadToFile = downloadToFile downloadAsIterable = downloadAsIterable downloadAsStream = downloadAsStream + protected _normalizeInputFile = _normalizeInputFile + protected _normalizeInputMedia = _normalizeInputMedia uploadFile = uploadFile deleteMessages = deleteMessages editMessage = editMessage @@ -2039,7 +1965,6 @@ export class TelegramClient extends BaseTelegramClient { sendDice = sendDice sendLocation = sendLocation sendMedia = sendMedia - sendPhoto = sendPhoto sendText = sendText unpinMessage = unpinMessage initTakeoutSession = initTakeoutSession diff --git a/packages/client/src/methods/files/normalize-input-file.ts b/packages/client/src/methods/files/normalize-input-file.ts new file mode 100644 index 00000000..48e4692f --- /dev/null +++ b/packages/client/src/methods/files/normalize-input-file.ts @@ -0,0 +1,49 @@ +import { TelegramClient } from '../../client' +import { InputFileLike, isUploadedFile, MtCuteArgumentError } from '../../types' +import { tl } from '@mtcute/tl' +import { tdFileId } from '@mtcute/file-id' + +/** + * Normalize a {@link InputFileLike} to `InputFile`, + * uploading it if needed. + * + * @internal + */ +export async function _normalizeInputFile( + this: TelegramClient, + input: InputFileLike, + params: { + progressCallback?: (uploaded: number, total: number) => void + fileName?: string + fileSize?: number + fileMime?: string + } +): Promise { + if (typeof input === 'object' && tl.isAnyInputMedia(input)) { + throw new MtCuteArgumentError( + "InputFile can't be created from an InputMedia" + ) + } else if (tdFileId.isFileIdLike(input)) { + if (typeof input === 'string' && input.match(/^file:/)) { + const uploaded = await this.uploadFile({ + file: input.substr(5), + ...params, + }) + return uploaded.inputFile + } else { + throw new MtCuteArgumentError( + "InputFile can't be created from an URL or a File ID" + ) + } + } else if (isUploadedFile(input)) { + return input.inputFile + } else if (typeof input === 'object' && tl.isAnyInputFile(input)) { + return input + } else { + const uploaded = await this.uploadFile({ + file: input, + ...params, + }) + return uploaded.inputFile + } +} diff --git a/packages/client/src/methods/files/normalize-input-media.ts b/packages/client/src/methods/files/normalize-input-media.ts new file mode 100644 index 00000000..4e5ebd49 --- /dev/null +++ b/packages/client/src/methods/files/normalize-input-media.ts @@ -0,0 +1,171 @@ +import { TelegramClient } from '../../client' +import { + InputMediaLike, + isUploadedFile, + MtCuteArgumentError, + UploadFileLike, +} from '../../types' +import { tl } from '@mtcute/tl' +import { + fileIdToInputDocument, + fileIdToInputPhoto, + parseFileId, + tdFileId, +} from '@mtcute/file-id' +import { extractFileName } from '../../utils/file-utils' + +/** + * Normalize an {@link InputMediaLike} to `InputMedia`, + * uploading the file if needed. + * + * @internal + */ +export async function _normalizeInputMedia( + this: TelegramClient, + media: InputMediaLike, + params: { + progressCallback?: (uploaded: number, total: number) => void + } +): Promise { + // my condolences to those poor souls who are going to maintain this (myself included) + + let inputFile: tl.TypeInputFile | undefined = undefined + let thumb: tl.TypeInputFile | undefined = undefined + let mime = 'application/octet-stream' + + const upload = async (file: UploadFileLike): Promise => { + const uploaded = await this.uploadFile({ + file, + progressCallback: params.progressCallback, + fileName: media.fileName, + fileMime: + media.type === 'sticker' + ? media.isAnimated + ? 'application/x-tgsticker' + : 'image/webp' + : media.fileMime, + fileSize: media.fileSize, + }) + inputFile = uploaded.inputFile + mime = uploaded.mime + } + + const input = media.file + if (tdFileId.isFileIdLike(input)) { + if (typeof input === 'string' && input.match(/^https?:\/\//)) { + return { + _: + media.type === 'photo' + ? 'inputMediaPhotoExternal' + : 'inputMediaDocumentExternal', + url: input, + } + } else if (typeof input === 'string' && input.match(/^file:/)) { + await upload(input.substr(5)) + } else { + const parsed = + typeof input === 'string' ? parseFileId(input) : input + + if (parsed.location._ === 'photo') { + return { + _: 'inputMediaPhoto', + id: fileIdToInputPhoto(parsed), + } + } else if (parsed.location._ === 'web') { + return { + _: + parsed.type === tdFileId.FileType.Photo + ? 'inputMediaPhotoExternal' + : 'inputMediaDocumentExternal', + url: parsed.location.url, + } + } else { + return { + _: 'inputMediaDocument', + id: fileIdToInputDocument(parsed), + } + } + } + } else if (typeof input === 'object' && tl.isAnyInputMedia(input)) { + return input + } else if (isUploadedFile(input)) { + inputFile = input.inputFile + mime = input.mime + } else if (typeof input === 'object' && tl.isAnyInputFile(input)) { + inputFile = input + } else { + await upload(input) + } + + if (!inputFile) throw new Error('should not happen') + + if (media.type === 'photo') { + return { + _: 'inputMediaUploadedPhoto', + file: inputFile, + ttlSeconds: media.ttlSeconds, + } + } + + if ('thumb' in media && media.thumb) { + thumb = await this._normalizeInputFile(media.thumb, {}) + } + + const attributes: tl.TypeDocumentAttribute[] = [] + + if (media.type !== 'voice') { + attributes.push({ + _: 'documentAttributeFilename', + fileName: + media.fileName || + (typeof media.file === 'string' + ? extractFileName(media.file) + : 'unnamed'), + }) + } + + if (media.type === 'video') { + attributes.push({ + _: 'documentAttributeVideo', + duration: media.duration || 0, + w: media.width || 0, + h: media.height || 0, + supportsStreaming: media.supportsStreaming, + roundMessage: media.isRound, + }) + if (media.isAnimated) + attributes.push({ _: 'documentAttributeAnimated' }) + } + + if (media.type === 'audio' || media.type === 'voice') { + attributes.push({ + _: 'documentAttributeAudio', + voice: media.type === 'voice', + duration: media.duration || 0, + title: media.type === 'audio' ? media.title : undefined, + performer: media.type === 'audio' ? media.performer : undefined, + waveform: media.type === 'voice' ? media.waveform : undefined, + }) + } + + if (media.type === 'sticker') { + attributes.push({ + _: 'documentAttributeSticker', + stickerset: { + _: 'inputStickerSetEmpty', + }, + alt: media.alt ?? '', + }) + } + + return { + _: 'inputMediaUploadedDocument', + nosoundVideo: media.type === 'video' && media.isAnimated, + forceFile: media.type === 'document', + file: inputFile, + thumb, + mimeType: mime, + attributes, + ttlSeconds: media.ttlSeconds + } +} diff --git a/packages/client/src/methods/messages/send-media.ts b/packages/client/src/methods/messages/send-media.ts index e139adfa..dfdf435e 100644 --- a/packages/client/src/methods/messages/send-media.ts +++ b/packages/client/src/methods/messages/send-media.ts @@ -3,24 +3,14 @@ import { BotKeyboard, InputMediaLike, InputPeerLike, - isUploadedFile, Message, - MtCuteArgumentError, - ReplyMarkup, UploadFileLike, + ReplyMarkup, } from '../../types' -import { tl } from '@mtcute/tl' -import { extractFileName } from '../../utils/file-utils' import { normalizeToInputPeer } from '../../utils/peer-utils' import { normalizeDate, randomUlong } from '../../utils/misc-utils' -import { - fileIdToInputDocument, - fileIdToInputPhoto, - parseFileId, - tdFileId, -} from '@mtcute/file-id' /** - * Send a single media. + * Send a single media (a photo or a document-based media) * * @param chatId ID of the chat, its username, phone or `"me"` or `"self"` * @param media @@ -28,6 +18,7 @@ import { * and Bot API compatible File ID, which will be wrapped * in {@link InputMedia.auto} * @param params Additional sending parameters + * @see InputMedia * @internal */ export async function sendMedia( @@ -92,170 +83,7 @@ export async function sendMedia( } } - if (media.type === 'photo') { - return this.sendPhoto(chatId, media.file, { - caption: media.caption, - entities: media.entities, - ...params, - }) - } - - let inputMedia: tl.TypeInputMedia | null = null - - // my condolences to those poor souls who are going to maintain this (myself included) - - let inputFile: tl.TypeInputFile | undefined = undefined - let thumb: tl.TypeInputFile | undefined = undefined - let mime = 'application/octet-stream' - - const upload = async (media: InputMediaLike, file: UploadFileLike): Promise => { - const uploaded = await this.uploadFile({ - file, - fileName: media.fileName, - progressCallback: params!.progressCallback, - fileMime: - media.type === 'sticker' - ? media.isAnimated - ? 'application/x-tgsticker' - : 'image/webp' - : media.mime, - fileSize: media.fileSize - }) - inputFile = uploaded.inputFile - mime = uploaded.mime - } - - const input = media.file - if (tdFileId.isFileIdLike(input)) { - if (typeof input === 'string' && input.match(/^https?:\/\//)) { - inputMedia = { - _: 'inputMediaDocumentExternal', - url: input, - } - } else if (typeof input === 'string' && input.match(/^file:/)) { - await upload(media, input.substr(5)) - } else { - const parsed = - typeof input === 'string' ? parseFileId(input) : input - - if (parsed.location._ === 'photo') { - inputMedia = { - _: 'inputMediaPhoto', - id: fileIdToInputPhoto(parsed), - } - } else if (parsed.location._ === 'web') { - inputMedia = { - _: - parsed.type === tdFileId.FileType.Photo - ? 'inputMediaPhotoExternal' - : 'inputMediaDocumentExternal', - url: parsed.location.url, - } - } else { - inputMedia = { - _: 'inputMediaDocument', - id: fileIdToInputDocument(parsed), - } - } - } - } else if (typeof input === 'object' && tl.isAnyInputMedia(input)) { - inputMedia = input - } else if (isUploadedFile(input)) { - inputFile = input.inputFile - mime = input.mime - } else if (typeof input === 'object' && tl.isAnyInputFile(input)) { - inputFile = input - } else { - await upload(media, input) - } - - if (!inputMedia) { - if (!inputFile) throw new Error('should not happen') - - if ('thumb' in media && media.thumb) { - const t = media.thumb - if (typeof t === 'object' && tl.isAnyInputMedia(t)) { - throw new MtCuteArgumentError("Thumbnail can't be InputMedia") - } else if (tdFileId.isFileIdLike(t)) { - if (typeof t === 'string' && t.match(/^file:/)) { - const uploaded = await this.uploadFile({ - file: t.substr(5), - }) - thumb = uploaded.inputFile - } else { - throw new MtCuteArgumentError( - "Thumbnail can't be a URL or a File ID" - ) - } - } else if (isUploadedFile(t)) { - thumb = t.inputFile - } else if (typeof t === 'object' && tl.isAnyInputFile(t)) { - thumb = t - } else { - const uploaded = await this.uploadFile({ - file: t, - }) - thumb = uploaded.inputFile - } - } - - const attributes: tl.TypeDocumentAttribute[] = [] - - if (media.type !== 'voice') { - attributes.push({ - _: 'documentAttributeFilename', - fileName: - media.fileName || - (typeof media.file === 'string' - ? extractFileName(media.file) - : 'unnamed'), - }) - } - - if (media.type === 'video') { - attributes.push({ - _: 'documentAttributeVideo', - duration: media.duration || 0, - w: media.width || 0, - h: media.height || 0, - supportsStreaming: media.supportsStreaming, - roundMessage: media.isRound, - }) - if (media.isAnimated) - attributes.push({ _: 'documentAttributeAnimated' }) - } - - if (media.type === 'audio' || media.type === 'voice') { - attributes.push({ - _: 'documentAttributeAudio', - voice: media.type === 'voice', - duration: media.duration || 0, - title: media.type === 'audio' ? media.title : undefined, - performer: media.type === 'audio' ? media.performer : undefined, - waveform: media.type === 'voice' ? media.waveform : undefined, - }) - } - - if (media.type === 'sticker') { - attributes.push({ - _: 'documentAttributeSticker', - stickerset: { - _: 'inputStickerSetEmpty', - }, - alt: media.alt ?? '', - }) - } - - inputMedia = { - _: 'inputMediaUploadedDocument', - nosoundVideo: media.type === 'video' && media.isAnimated, - forceFile: media.type === 'document', - file: inputFile, - thumb, - mimeType: mime, - attributes, - } - } + const inputMedia = await this._normalizeInputMedia(media, params) const [message, entities] = await this._parseEntities( media.caption, diff --git a/packages/client/src/methods/messages/send-photo.ts b/packages/client/src/methods/messages/send-photo.ts deleted file mode 100644 index 51948a12..00000000 --- a/packages/client/src/methods/messages/send-photo.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { - InputPeerLike, - InputFileLike, - Message, - BotKeyboard, - ReplyMarkup, - isUploadedFile, - UploadFileLike, -} from '../../types' -import { tl } from '@mtcute/tl' -import { TelegramClient } from '../../client' -import { normalizeToInputPeer } from '../../utils/peer-utils' -import { normalizeDate, randomUlong } from '../../utils/misc-utils' -import { fileIdToInputPhoto, tdFileId } from '@mtcute/file-id' - -/** - * Send a single photo - * - * @param chatId ID of the chat, its username, phone or `"me"` or `"self"` - * @param photo Photo contained in the message. - * @param params Additional sending parameters - * @internal - */ -export async function sendPhoto( - this: TelegramClient, - chatId: InputPeerLike, - photo: InputFileLike, - params?: { - /** - * Caption for the photo - */ - caption?: string - - /** - * Message to reply to. Either a message object or message ID. - */ - replyTo?: number | Message - - /** - * Parse mode to use to parse entities before sending - * the message. Defaults to current default parse mode (if any). - * - * Passing `null` will explicitly disable formatting. - */ - parseMode?: string | null - - /** - * List of formatting entities to use instead of parsing via a - * parse mode. - * - * **Note:** Passing this makes the method ignore {@link parseMode} - */ - entities?: tl.TypeMessageEntity[] - - /** - * Whether to send this message silently. - */ - silent?: boolean - - /** - * If set, the message will be scheduled to this date. - * When passing a number, a UNIX time in ms is expected. - */ - schedule?: Date | number - - /** - * For bots: inline or reply markup or an instruction - * to hide a reply keyboard or to force a reply. - */ - replyMarkup?: ReplyMarkup - - /** - * Self-Destruct timer. - * If set, the photo will self-destruct in a given number - * of seconds. - */ - ttlSeconds?: number - - /** - * Function that will be called after some part has been uploaded. - * Only used when a file that requires uploading is passed. - * - * @param uploaded Number of bytes already uploaded - * @param total Total file size - */ - progressCallback?: (uploaded: number, total: number) => void - - /** - * Whether to clear draft after sending this message. - * - * Defaults to `false` - */ - clearDraft?: boolean - - /** - * File size. Only used when uploading from streams without - * known length. - */ - fileSize?: number - } -): Promise { - if (!params) params = {} - - let media: tl.TypeInputMedia - - const upload = async (photo: UploadFileLike) => { - const uploaded = await this.uploadFile({ - file: photo, - progressCallback: params!.progressCallback, - fileSize: params!.fileSize, - }) - media = { - _: 'inputMediaUploadedPhoto', - file: uploaded.inputFile, - ttlSeconds: params!.ttlSeconds, - } - } - - if (tdFileId.isFileIdLike(photo)) { - if (typeof photo === 'string' && photo.match(/^https?:\/\//)) { - media = { - _: 'inputMediaPhotoExternal', - url: photo, - ttlSeconds: params.ttlSeconds, - } - } else if (typeof photo === 'string' && photo.match(/^file:/)) { - await upload(photo.substr(5)) - } else { - const input = fileIdToInputPhoto(photo) - media = { - _: 'inputMediaPhoto', - id: input, - } - } - } else if (typeof photo === 'object' && tl.isAnyInputMedia(photo)) { - media = photo - } else if (isUploadedFile(photo)) { - media = { - _: 'inputMediaUploadedPhoto', - file: photo.inputFile, - ttlSeconds: params.ttlSeconds, - } - } else if (typeof photo === 'object' && tl.isAnyInputFile(photo)) { - media = { - _: 'inputMediaUploadedPhoto', - file: photo, - ttlSeconds: params.ttlSeconds, - } - } else { - await upload(photo) - } - - const [message, entities] = await this._parseEntities( - params.caption, - params.parseMode, - params.entities - ) - - const peer = normalizeToInputPeer(await this.resolvePeer(chatId)) - const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup) - - const res = await this.call({ - _: 'messages.sendMedia', - media: media!, - peer, - silent: params.silent, - replyToMsgId: params.replyTo - ? typeof params.replyTo === 'number' - ? params.replyTo - : params.replyTo.id - : undefined, - randomId: randomUlong(), - scheduleDate: normalizeDate(params.schedule), - replyMarkup, - message, - entities, - clearDraft: params.clearDraft, - }) - - return this._findMessageInUpdate(res) -} diff --git a/packages/client/src/types/media/input-media.ts b/packages/client/src/types/media/input-media.ts index da919f5a..8091df54 100644 --- a/packages/client/src/types/media/input-media.ts +++ b/packages/client/src/types/media/input-media.ts @@ -20,26 +20,39 @@ interface BaseInputMedia { /** * Override file name for the file. + * + * Only applicable to newly uploaded files. */ fileName?: string /** * Override MIME type for the file + * + * Only applicable to newly uploaded files. */ - mime?: string + fileMime?: string /** * Override file size for the file + * + * Only applicable to newly uploaded files. */ fileSize?: number + + /** + * TTL for the media in seconds. + * + * Only applicable to some media types + */ + ttlSeconds?: number } /** * Automatically detect media type based on file contents. * - * Only works for files that are internally documents, i.e. - * *does not* infer photos, so use {@link InputMediaPhoto} instead - * (except for File IDs, from which photos *are* inferred) + * Photo type is only inferred for reused files, + * newly uploaded photos with `auto` will be + * uploaded as a document */ export interface InputMediaAuto extends BaseInputMedia { type: 'auto' @@ -57,21 +70,29 @@ export interface InputMediaAudio extends BaseInputMedia { * The thumbnail should be in JPEG format and less than 200 KB in size. * A thumbnail's width and height should not exceed 320 pixels. * Thumbnails can't be reused and can be only uploaded as a new file. + * + * Only applicable to newly uploaded files. */ thumb?: InputFileLike /** * Duration of the audio in seconds + * + * Only applicable to newly uploaded files. */ duration?: number /** * Performer of the audio + * + * Only applicable to newly uploaded files. */ performer?: string /** * Title of the audio + * + * Only applicable to newly uploaded files. */ title?: string } @@ -84,11 +105,15 @@ export interface InputMediaVoice extends BaseInputMedia { /** * Duration of the voice message in seconds + * + * Only applicable to newly uploaded files. */ duration?: number /** * Waveform of the voice message + * + * Only applicable to newly uploaded files. */ waveform?: Buffer } @@ -105,6 +130,8 @@ export interface InputMediaDocument extends BaseInputMedia { * The thumbnail should be in JPEG format and less than 200 KB in size. * A thumbnail's width and height should not exceed 320 pixels. * Thumbnails can't be reused and can be only uploaded as a new file. + * + * Only applicable to newly uploaded files. */ thumb?: InputFileLike } @@ -132,11 +159,16 @@ export interface InputMediaSticker extends BaseInputMedia { * format, which is Lottie JSON compressed using GZip * * Defaults to `false` + * + * Only applicable to newly uploaded files. */ isAnimated?: boolean /** * An emoji representing this sticker + * + * Only applicable to newly uploaded files, + * for some reason doesn't work with animated stickers. */ alt?: string } @@ -153,36 +185,50 @@ export interface InputMediaVideo extends BaseInputMedia { * The thumbnail should be in JPEG format and less than 200 KB in size. * A thumbnail's width and height should not exceed 320 pixels. * Thumbnails can't be reused and can be only uploaded as a new file. + * + * Only applicable to newly uploaded files. */ thumb?: InputFileLike /** * Width of the video in pixels + * + * Only applicable to newly uploaded files. */ width?: number /** * Height of the video in pixels + * + * Only applicable to newly uploaded files. */ height?: number /** * Duration of the video in seconds + * + * Only applicable to newly uploaded files. */ duration?: number /** * Whether the video is suitable for streaming + * + * Only applicable to newly uploaded files. */ supportsStreaming?: boolean /** * Whether this video is an animated GIF + * + * Only applicable to newly uploaded files. */ isAnimated?: boolean /** * Whether this video is a round message (aka video note) + * + * Only applicable to newly uploaded files. */ isRound?: boolean } @@ -311,8 +357,9 @@ export namespace InputMedia { * Create a document to be sent, which subtype * is inferred automatically by file contents. * - * Only infers photos from the File ID, otherwise - * photos will be sent as documents. + * Photo type is only inferred for reused files, + * newly uploaded photos with `auto` will be + * uploaded as a document */ export function auto( file: InputFileLike,