From 2e3b5eebf88a6e5a7d79073d66f33f7b328e1c34 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 10:53:26 +0200 Subject: [PATCH 01/11] convert to node project --- package.json | 3 +- src/bot.ts | 16 +-- src/composer.ts | 10 +- src/context.ts | 8 +- src/convenience/constants.ts | 2 +- src/convenience/frameworks.ts | 2 +- src/convenience/inline_query.ts | 2 +- src/convenience/input_media.ts | 2 +- src/convenience/keyboard.ts | 2 +- src/convenience/session.ts | 6 +- src/convenience/webhook.ts | 12 +-- src/core/api.ts | 4 +- src/core/client.ts | 9 +- src/core/error.ts | 2 +- src/core/payload.ts | 4 +- src/filter.ts | 31 ++---- src/mod.ts | 28 ++--- src/platform.deno.ts | 27 ----- src/platform.web.ts | 21 ---- src/types.deno.ts | 179 -------------------------------- src/types.ts | 2 +- src/types.web.ts | 133 ------------------------ 22 files changed, 67 insertions(+), 438 deletions(-) delete mode 100644 src/platform.deno.ts delete mode 100644 src/platform.web.ts delete mode 100644 src/types.deno.ts delete mode 100644 src/types.web.ts diff --git a/package.json b/package.json index 37e88ccf..b14dfc8c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "@types/debug": "^4.1.12", "@types/node": "^12.20.55", "@types/node-fetch": "2.6.2", - "deno2node": "^1.9.0" + "deno2node": "^1.9.0", + "typescript": "^5.5.0-dev.20240401" }, "files": [ "out/" diff --git a/src/bot.ts b/src/bot.ts index 8e53ce04..22c8a6a9 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -5,22 +5,22 @@ import { type Middleware, type ReactionMiddleware, run, -} from "./composer.ts"; -import { Context, type MaybeArray, type ReactionContext } from "./context.ts"; -import { Api } from "./core/api.ts"; +} from "./composer"; +import { Context, type MaybeArray, type ReactionContext } from "./context"; +import { Api } from "./core/api"; import { type ApiClientOptions, type WebhookReplyEnvelope, -} from "./core/client.ts"; -import { GrammyError, HttpError } from "./core/error.ts"; -import { type Filter, type FilterQuery, parse, preprocess } from "./filter.ts"; -import { debug as d } from "./platform.deno.ts"; +} from "./core/client"; +import { GrammyError, HttpError } from "./core/error"; +import { type Filter, type FilterQuery, parse, preprocess } from "./filter"; +import { debug as d } from "./platform.node"; import { type ReactionType, type ReactionTypeEmoji, type Update, type UserFromGetMe, -} from "./types.ts"; +} from "./types"; const debug = d("grammy:bot"); const debugWarn = d("grammy:warn"); const debugErr = d("grammy:error"); diff --git a/src/composer.ts b/src/composer.ts index 26f3da8b..c367fd68 100644 --- a/src/composer.ts +++ b/src/composer.ts @@ -10,13 +10,9 @@ import { type MaybeArray, type ReactionContext, type StringWithSuggestions, -} from "./context.ts"; -import { type Filter, type FilterQuery } from "./filter.ts"; -import { - type Chat, - type ReactionType, - type ReactionTypeEmoji, -} from "./types.ts"; +} from "./context"; +import { type Filter, type FilterQuery } from "./filter"; +import { type Chat, type ReactionType, type ReactionTypeEmoji } from "./types"; type MaybePromise = T | Promise; diff --git a/src/context.ts b/src/context.ts index 8a314dd7..1d71c3e6 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,12 +1,12 @@ // deno-lint-ignore-file camelcase -import { type Api, type Other as OtherApi } from "./core/api.ts"; -import { type Methods, type RawApi } from "./core/client.ts"; +import { type Api, type Other as OtherApi } from "./core/api"; +import { type Methods, type RawApi } from "./core/client"; import { type Filter, type FilterCore, type FilterQuery, matchFilter, -} from "./filter.ts"; +} from "./filter"; import { type Chat, type ChatPermissions, @@ -26,7 +26,7 @@ import { type Update, type User, type UserFromGetMe, -} from "./types.ts"; +} from "./types"; // === Util types export type MaybeArray = T | T[]; diff --git a/src/convenience/constants.ts b/src/convenience/constants.ts index eebac148..eb5f7e13 100644 --- a/src/convenience/constants.ts +++ b/src/convenience/constants.ts @@ -1,4 +1,4 @@ -import { DEFAULT_UPDATE_TYPES } from "../bot.ts"; +import { DEFAULT_UPDATE_TYPES } from "../bot"; const ALL_UPDATE_TYPES = [ ...DEFAULT_UPDATE_TYPES, diff --git a/src/convenience/frameworks.ts b/src/convenience/frameworks.ts index 8f9ae289..2647258f 100644 --- a/src/convenience/frameworks.ts +++ b/src/convenience/frameworks.ts @@ -1,4 +1,4 @@ -import { type Update } from "../types.ts"; +import { type Update } from "../types"; const SECRET_HEADER = "X-Telegram-Bot-Api-Secret-Token"; const SECRET_HEADER_LOWERCASE = SECRET_HEADER.toLowerCase(); diff --git a/src/convenience/inline_query.ts b/src/convenience/inline_query.ts index eb4b1027..e77996a7 100644 --- a/src/convenience/inline_query.ts +++ b/src/convenience/inline_query.ts @@ -26,7 +26,7 @@ import { type InputTextMessageContent, type InputVenueMessageContent, type LabeledPrice, -} from "../types.ts"; +} from "../types"; type InlineQueryResultOptions = Omit< T, diff --git a/src/convenience/input_media.ts b/src/convenience/input_media.ts index 6ce459b4..41f938c5 100644 --- a/src/convenience/input_media.ts +++ b/src/convenience/input_media.ts @@ -5,7 +5,7 @@ import { type InputMediaDocument, type InputMediaPhoto, type InputMediaVideo, -} from "../types.ts"; +} from "../types"; type InputMediaOptions = Omit; diff --git a/src/convenience/keyboard.ts b/src/convenience/keyboard.ts index a69c2e05..6a05259e 100644 --- a/src/convenience/keyboard.ts +++ b/src/convenience/keyboard.ts @@ -6,7 +6,7 @@ import { type KeyboardButtonRequestUsers, type LoginUrl, type SwitchInlineQueryChosenChat, -} from "../types.ts"; +} from "../types"; type KeyboardButtonSource = string | KeyboardButton; type KeyboardSource = KeyboardButtonSource[][] | Keyboard; diff --git a/src/convenience/session.ts b/src/convenience/session.ts index a01a4ebc..bf1981c8 100644 --- a/src/convenience/session.ts +++ b/src/convenience/session.ts @@ -1,6 +1,6 @@ -import { type MiddlewareFn } from "../composer.ts"; -import { type Context } from "../context.ts"; -import { debug as d } from "../platform.deno.ts"; +import { type MiddlewareFn } from "../composer"; +import { type Context } from "../context"; +import { debug as d } from "../platform.node"; const debug = d("grammy:session"); type MaybePromise = Promise | T; diff --git a/src/convenience/webhook.ts b/src/convenience/webhook.ts index c9d67af3..28e551d5 100644 --- a/src/convenience/webhook.ts +++ b/src/convenience/webhook.ts @@ -1,14 +1,14 @@ // deno-lint-ignore-file no-explicit-any -import { type Bot } from "../bot.ts"; -import { type Context } from "../context.ts"; -import { type WebhookReplyEnvelope } from "../core/client.ts"; -import { debug as d, defaultAdapter } from "../platform.deno.ts"; -import { type Update } from "../types.ts"; +import { type Bot } from "../bot"; +import { type Context } from "../context"; +import { type WebhookReplyEnvelope } from "../core/client"; +import { debug as d, defaultAdapter } from "../platform.node"; +import { type Update } from "../types"; import { adapters as nativeAdapters, type FrameworkAdapter, type SupportedFrameworks, -} from "./frameworks.ts"; +} from "./frameworks"; const debugErr = d("grammy:error"); const callbackAdapter: FrameworkAdapter = ( diff --git a/src/core/api.ts b/src/core/api.ts index f8b93693..493b8203 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -14,7 +14,7 @@ import { type MaskPosition, type PassportElementError, type ReactionType, -} from "../types.ts"; +} from "../types"; import { type ApiClientOptions, createRawApi, @@ -24,7 +24,7 @@ import { type Transformer, type TransformerConsumer, type WebhookReplyEnvelope, -} from "./client.ts"; +} from "./client"; /** * Helper type to derive remaining properties of a given API method call M, diff --git a/src/core/client.ts b/src/core/client.ts index cb680809..cba9ef1e 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,15 +1,15 @@ -import { baseFetchConfig, debug as d } from "../platform.deno.ts"; +import { baseFetchConfig, debug as d } from "../platform.node"; import { type ApiMethods as Telegram, type ApiResponse, type Opts, -} from "../types.ts"; -import { toGrammyError, toHttpError } from "./error.ts"; +} from "../types"; +import { toGrammyError, toHttpError } from "./error"; import { createFormDataPayload, createJsonPayload, requiresFormDataUpload, -} from "./payload.ts"; +} from "./payload"; const debug = d("grammy:core"); export type Methods = string & keyof R; @@ -299,6 +299,7 @@ class ApiClient { // Perform fetch call, and handle networking errors const successPromise = fetch( url instanceof URL ? url.href : url, + // @ts-ignore TODO: remove options, ).catch(toHttpError(method, opts.sensitiveLogs)); // Those are the three possible outcomes of the fetch call: diff --git a/src/core/error.ts b/src/core/error.ts index 0334f097..f8427ea7 100644 --- a/src/core/error.ts +++ b/src/core/error.ts @@ -1,4 +1,4 @@ -import { type ApiError, type ResponseParameters } from "../types.ts"; +import { type ApiError, type ResponseParameters } from "../types"; /** * This class represents errors that are thrown by grammY because the Telegram diff --git a/src/core/payload.ts b/src/core/payload.ts index db7e5ca1..b84efaa9 100644 --- a/src/core/payload.ts +++ b/src/core/payload.ts @@ -1,5 +1,5 @@ -import { itrToStream } from "../platform.deno.ts"; -import { InputFile } from "../types.ts"; +import { itrToStream } from "../platform.node"; +import { InputFile } from "../types"; // === Payload types (JSON vs. form data) /** diff --git a/src/filter.ts b/src/filter.ts index a16ea434..748a8afe 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file camelcase no-explicit-any -import { type AliasProps, type Context } from "./context.ts"; -import { type Update } from "./types.ts"; +import { type AliasProps, type Context } from "./context"; +import { type Update } from "./types"; type FilterFunction = (ctx: C) => ctx is D; @@ -122,10 +122,7 @@ function check(original: string[], preprocessed: string[][]): string[][] { function checkOne(filter: string[]): string | true { const [l1, l2, l3, ...n] = filter; if (l1 === undefined) return "Empty filter query given"; - if ( - !(l1 in UPDATE_KEYS || - l1 === "chat_boost" || l1 === "removed_chat_boost") // TODO: remove - ) { + if (!(l1 in UPDATE_KEYS)) { const permitted = Object.keys(UPDATE_KEYS); return `Invalid L1 filter '${l1}' given in '${filter.join(":")}'. \ Permitted values are: ${permitted.map((k) => `'${k}'`).join(", ")}.`; @@ -358,8 +355,8 @@ const UPDATE_KEYS = { chat_join_request: {}, message_reaction: MESSAGE_REACTION_UPDATED_KEYS, message_reaction_count: MESSAGE_REACTION_COUNT_UPDATED_KEYS, - // chat_boost: {}, - // removed_chat_boost: {}, + chat_boost: {}, + removed_chat_boost: {}, } as const; // === Build up all possible filter queries from the above validation structure @@ -407,10 +404,7 @@ type CollapseL2< : never : never); // All queries -type ComputeFilterQueryList = - | InjectShortcuts - | "chat_boost" // TODO: remove - | "removed_chat_boost"; +type ComputeFilterQueryList = InjectShortcuts; /** * Represents a filter query that can be passed to `bot.on`. There are three @@ -582,14 +576,11 @@ const L2_SHORTCUTS = { type L1Shortcuts = KeyOf; type L2Shortcuts = KeyOf; -type ExpandShortcuts = Exclude< - Q extends `${infer L1}:${infer L2}:${infer L3}` - ? `${ExpandL1}:${ExpandL2}:${L3}` - : Q extends `${infer L1}:${infer L2}` - ? `${ExpandL1}:${ExpandL2}` - : ExpandL1, - "chat_boost" | "removed_chat_boost" // TODO: remove ->; +type ExpandShortcuts = Q extends + `${infer L1}:${infer L2}:${infer L3}` + ? `${ExpandL1}:${ExpandL2}:${L3}` + : Q extends `${infer L1}:${infer L2}` ? `${ExpandL1}:${ExpandL2}` + : ExpandL1; type ExpandL1 = S extends L1Shortcuts ? typeof L1_SHORTCUTS[S][number] : S; diff --git a/src/mod.ts b/src/mod.ts index 01ce6eb7..89471269 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -5,9 +5,9 @@ export { BotError, type ErrorHandler, type PollingOptions, -} from "./bot.ts"; +} from "./bot"; -export { InputFile } from "./types.ts"; +export { InputFile } from "./types"; export { type CallbackQueryContext, @@ -18,15 +18,15 @@ export { type HearsContext, type InlineQueryContext, type ReactionContext, -} from "./context.ts"; +} from "./context"; // Convenience stuff, built-in plugins, and helpers -export * from "./convenience/constants.ts"; -export * from "./convenience/inline_query.ts"; -export * from "./convenience/input_media.ts"; -export * from "./convenience/keyboard.ts"; -export * from "./convenience/session.ts"; -export * from "./convenience/webhook.ts"; +export * from "./convenience/constants"; +export * from "./convenience/inline_query"; +export * from "./convenience/input_media"; +export * from "./convenience/keyboard"; +export * from "./convenience/session"; +export * from "./convenience/webhook"; // A little more advanced stuff export { @@ -42,12 +42,12 @@ export { type MiddlewareObj, type NextFunction, type ReactionMiddleware, -} from "./composer.ts"; +} from "./composer"; -export { type Filter, type FilterQuery, matchFilter } from "./filter.ts"; +export { type Filter, type FilterQuery, matchFilter } from "./filter"; // Internal stuff for expert users -export { Api } from "./core/api.ts"; +export { Api } from "./core/api"; export { type ApiCallFn, type ApiClientOptions, @@ -55,5 +55,5 @@ export { type TransformableApi, type Transformer, type WebhookReplyEnvelope, -} from "./core/client.ts"; -export { GrammyError, HttpError } from "./core/error.ts"; +} from "./core/client"; +export { GrammyError, HttpError } from "./core/error"; diff --git a/src/platform.deno.ts b/src/platform.deno.ts deleted file mode 100644 index b8f37474..00000000 --- a/src/platform.deno.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** Are we running on Deno or in a web browser? */ -export const isDeno = typeof Deno !== "undefined"; - -// === Export debug -import debug from "https://cdn.skypack.dev/debug@4.3.4"; -export { debug }; -const DEBUG = "DEBUG"; -if (isDeno) { - debug.useColors = () => !Deno.noColor; - const env = { name: "env", variable: DEBUG } as const; - const res = await Deno.permissions.query(env); - let namespace: string | undefined = undefined; - if (res.state === "granted") namespace = Deno.env.get(DEBUG); - if (namespace) debug.enable(namespace); - else debug.disable(); -} - -// === Export system-specific operations -// Turn an AsyncIterable into a stream -export const itrToStream = (itr: AsyncIterable) => - ReadableStream.from(itr); - -// === Base configuration for `fetch` calls -export const baseFetchConfig = (_apiRoot: string) => ({}); - -// === Default webhook adapter -export const defaultAdapter = "oak"; diff --git a/src/platform.web.ts b/src/platform.web.ts deleted file mode 100644 index 8758e4c6..00000000 --- a/src/platform.web.ts +++ /dev/null @@ -1,21 +0,0 @@ -import d from "https://cdn.skypack.dev/debug@4.3.4"; -export { d as debug }; - -// === Export system-specific operations -// Turn an AsyncIterable into a stream -export const itrToStream = (itr: AsyncIterable) => { - // do not assume ReadableStream.from to exist yet - const it = itr[Symbol.asyncIterator](); - return new ReadableStream({ - async pull(controller) { - const chunk = await it.next(); - if (chunk.done) controller.close(); - else controller.enqueue(chunk.value); - }, - }); -}; - -// === Base configuration for `fetch` calls -export const baseFetchConfig = (_apiRoot: string) => ({}); - -export const defaultAdapter = "cloudflare"; diff --git a/src/types.deno.ts b/src/types.deno.ts deleted file mode 100644 index e4393148..00000000 --- a/src/types.deno.ts +++ /dev/null @@ -1,179 +0,0 @@ -// === Needed imports -import { basename } from "https://deno.land/std@0.211.0/path/basename.ts"; -import { iterateReader } from "https://deno.land/std@0.211.0/streams/iterate_reader.ts"; -import { - type ApiMethods as ApiMethodsF, - type InputMedia as InputMediaF, - type InputMediaAnimation as InputMediaAnimationF, - type InputMediaAudio as InputMediaAudioF, - type InputMediaDocument as InputMediaDocumentF, - type InputMediaPhoto as InputMediaPhotoF, - type InputMediaVideo as InputMediaVideoF, - type InputSticker as InputStickerF, - type Opts as OptsF, -} from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; -import { debug as d, isDeno } from "./platform.deno.ts"; - -const debug = d("grammy:warn"); - -// === Export all API types -export * from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; - -/** A value, or a potentially async function supplying that value */ -type MaybeSupplier = T | (() => T | Promise); -/** Something that looks like a URL. */ -interface URLLike { - /** - * Identifier of the resource. Must be in a format that can be parsed by the - * URL constructor. - */ - url: string; -} - -// === InputFile handling and File augmenting -/** - * An `InputFile` wraps a number of different sources for [sending - * files](https://grammy.dev/guide/files.html#uploading-your-own-file). - * - * It corresponds to the `InputFile` type in the [Telegram Bot API - * Reference](https://core.telegram.org/bots/api#inputfile). - */ -export class InputFile { - private consumed = false; - private readonly fileData: ConstructorParameters[0]; - /** - * Optional name of the constructed `InputFile` instance. - * - * Check out the - * [documentation](https://grammy.dev/guide/files.html#uploading-your-own-file) - * on sending files with `InputFile`. - */ - public readonly filename?: string; - /** - * Constructs an `InputFile` that can be used in the API to send files. - * - * @param file A path to a local file or a `Buffer` or a `ReadableStream` that specifies the file data - * @param filename Optional name of the file - */ - constructor( - file: MaybeSupplier< - | string - | Blob - | Deno.FsFile - | Response - | URL - | URLLike - | Uint8Array - | ReadableStream - | Iterable - | AsyncIterable - >, - filename?: string, - ) { - this.fileData = file; - filename ??= this.guessFilename(file); - this.filename = filename; - if ( - typeof file === "string" && - (file.startsWith("http:") || file.startsWith("https:")) - ) { - debug( - `InputFile received the local file path '${file}' that looks like a URL. Is this a mistake?`, - ); - } - } - private guessFilename( - file: ConstructorParameters[0], - ): string | undefined { - if (typeof file === "string") return basename(file); - if ("url" in file) return basename(file.url); - if (!(file instanceof URL)) return undefined; - if (file.pathname !== "/") { - const filename = basename(file.pathname); - if (filename) return filename; - } - return basename(file.hostname); - } - /** - * Internal method. Do not use. - * - * Converts this instance into a binary representation that can be sent to - * the Bot API server in the request body. - */ - async toRaw(): Promise< - Uint8Array | Iterable | AsyncIterable - > { - if (this.consumed) { - throw new Error("Cannot reuse InputFile data source!"); - } - const data = this.fileData; - // Handle local files - if (typeof data === "string") { - if (!isDeno) { - throw new Error( - "Reading files by path requires a Deno environment", - ); - } - const file = await Deno.open(data); - return iterateReader(file); - } - if (data instanceof Blob) return data.stream(); - if (isDenoFile(data)) return iterateReader(data); - // Handle Response objects - if (data instanceof Response) { - if (data.body === null) throw new Error(`No response body!`); - return data.body; - } - // Handle URL and URLLike objects - if (data instanceof URL) return fetchFile(data); - if ("url" in data) return fetchFile(data.url); - // Return buffers as-is - if (data instanceof Uint8Array) return data; - // Unwrap supplier functions - if (typeof data === "function") { - return new InputFile(await data()).toRaw(); - } - // Mark streams and iterators as consumed and return them as-is - this.consumed = true; - return data; - } -} - -async function* fetchFile(url: string | URL): AsyncIterable { - const { body } = await fetch(url); - if (body === null) { - throw new Error(`Download failed, no response body from '${url}'`); - } - yield* body; -} -function isDenoFile(data: unknown): data is Deno.FsFile { - return isDeno && data instanceof Deno.FsFile; -} - -// === Export InputFile types -/** Wrapper type to bundle all methods of the Telegram API */ -export type ApiMethods = ApiMethodsF; - -/** Utility type providing the argument type for the given method name or `{}` if the method does not take any parameters */ -export type Opts = OptsF[M]; - -/** This object describes a sticker to be added to a sticker set. */ -export type InputSticker = InputStickerF; - -/** This object represents the content of a media message to be sent. It should be one of -- InputMediaAnimation -- InputMediaDocument -- InputMediaAudio -- InputMediaPhoto -- InputMediaVideo */ -export type InputMedia = InputMediaF; -/** Represents a photo to be sent. */ -export type InputMediaPhoto = InputMediaPhotoF; -/** Represents a video to be sent. */ -export type InputMediaVideo = InputMediaVideoF; -/** Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. */ -export type InputMediaAnimation = InputMediaAnimationF; -/** Represents an audio file to be treated as music to be sent. */ -export type InputMediaAudio = InputMediaAudioF; -/** Represents a general file to be sent. */ -export type InputMediaDocument = InputMediaDocumentF; diff --git a/src/types.ts b/src/types.ts index abe40e79..9bca599a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1 +1 @@ -export * from "./types.deno.ts"; +export * from "./types.node"; diff --git a/src/types.web.ts b/src/types.web.ts deleted file mode 100644 index 4a646e94..00000000 --- a/src/types.web.ts +++ /dev/null @@ -1,133 +0,0 @@ -// === Needed imports -import { basename } from "https://deno.land/std@0.211.0/path/basename.ts"; -import { - type ApiMethods as ApiMethodsF, - type InputMedia as InputMediaF, - type InputMediaAnimation as InputMediaAnimationF, - type InputMediaAudio as InputMediaAudioF, - type InputMediaDocument as InputMediaDocumentF, - type InputMediaPhoto as InputMediaPhotoF, - type InputMediaVideo as InputMediaVideoF, - type InputSticker as InputStickerF, - type Opts as OptsF, -} from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; - -// === Export all API types -export * from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; - -/** Something that looks like a URL. */ -interface URLLike { - /** - * Identifier of the resource. Must be in a format that can be parsed by the - * URL constructor. - */ - url: string; -} - -// === InputFile handling and File augmenting -/** - * An `InputFile` wraps a number of different sources for [sending - * files](https://grammy.dev/guide/files.html#uploading-your-own-file). - * - * It corresponds to the `InputFile` type in the [Telegram Bot API - * Reference](https://core.telegram.org/bots/api#inputfile). - */ -export class InputFile { - private consumed = false; - private readonly fileData: ConstructorParameters[0]; - /** - * Optional name of the constructed `InputFile` instance. - * - * Check out the - * [documentation](https://grammy.dev/guide/files.html#uploading-your-own-file) - * on sending files with `InputFile`. - */ - public readonly filename?: string; - /** - * Constructs an `InputFile` that can be used in the API to send files. - * - * @param file A URL to a file or a `Blob` or other forms of file data - * @param filename Optional name of the file - */ - constructor( - file: - | Blob - | URL - | URLLike - | Uint8Array - | ReadableStream - | Iterable - | AsyncIterable, - filename?: string, - ) { - this.fileData = file; - filename ??= this.guessFilename(file); - this.filename = filename; - } - private guessFilename( - file: ConstructorParameters[0], - ): string | undefined { - if (typeof file === "string") return basename(file); - if (typeof file !== "object") return undefined; - if ("url" in file) return basename(file.url); - if (!(file instanceof URL)) return undefined; - return basename(file.pathname) || basename(file.hostname); - } - /** - * Internal method. Do not use. - * - * Converts this instance into a binary representation that can be sent to - * the Bot API server in the request body. - */ - toRaw(): Uint8Array | Iterable | AsyncIterable { - if (this.consumed) { - throw new Error("Cannot reuse InputFile data source!"); - } - const data = this.fileData; - // Handle local files - if (data instanceof Blob) return data.stream(); - // Handle URL and URLLike objects - if (data instanceof URL) return fetchFile(data); - if ("url" in data) return fetchFile(data.url); - // Mark streams and iterators as consumed - if (!(data instanceof Uint8Array)) this.consumed = true; - // Return buffers and byte streams as-is - return data; - } -} - -async function* fetchFile(url: string | URL): AsyncIterable { - const { body } = await fetch(url); - if (body === null) { - throw new Error(`Download failed, no response body from '${url}'`); - } - yield* body; -} - -// === Export InputFile types -/** Wrapper type to bundle all methods of the Telegram API */ -export type ApiMethods = ApiMethodsF; - -/** Utility type providing the argument type for the given method name or `{}` if the method does not take any parameters */ -export type Opts = OptsF[M]; - -/** This object describes a sticker to be added to a sticker set. */ -export type InputSticker = InputStickerF; - -/** This object represents the content of a media message to be sent. It should be one of -- InputMediaAnimation -- InputMediaDocument -- InputMediaAudio -- InputMediaPhoto -- InputMediaVideo */ -export type InputMedia = InputMediaF; -/** Represents a photo to be sent. */ -export type InputMediaPhoto = InputMediaPhotoF; -/** Represents a video to be sent. */ -export type InputMediaVideo = InputMediaVideoF; -/** Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. */ -export type InputMediaAnimation = InputMediaAnimationF; -/** Represents an audio file to be treated as music to be sent. */ -export type InputMediaAudio = InputMediaAudioF; -/** Represents a general file to be sent. */ -export type InputMediaDocument = InputMediaDocumentF; From 2b332db143574ab1b6f2acb6a8b6dae347268353 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 10:53:46 +0200 Subject: [PATCH 02/11] add minimal test case --- src/bot.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bot.ts b/src/bot.ts index 22c8a6a9..402851be 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -725,3 +725,5 @@ further middleware while your bot is running, consider installing a composer \ instance on your bot, and in turn augment the composer after the fact. This way, \ you can circumvent this protection against memory leaks.`); } + +new Bot("").on("msg", (ctx) => {}); From 27077463f9fc8a8797a4286ff69bbf19cb9a6a1c Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 10:56:05 +0200 Subject: [PATCH 03/11] disable deno lsp --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 92956afa..fb84c26a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, - "deno.enable": true, + "deno.enable": false, "deno.config": "./deno.jsonc", "deno.lint": true, "[typescript]": { From dae676fd62455738d9ecb36abb3ed9a6adff1b97 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 11:43:06 +0200 Subject: [PATCH 04/11] Avoid mapped type --- src/context.ts | 2 +- src/filter.ts | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/context.ts b/src/context.ts index 1d71c3e6..92dab42f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -42,7 +42,7 @@ type Other, X extends string = never> = OtherApi< type SnakeToCamelCase = S extends `${infer L}_${infer R}` ? `${L}${Capitalize>}` : S; -export type AliasProps = { +type AliasProps = { [K in string & keyof U as SnakeToCamelCase]: U[K]; }; type RenamedUpdate = AliasProps>; diff --git a/src/filter.ts b/src/filter.ts index 748a8afe..9678b31d 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file camelcase no-explicit-any -import { type AliasProps, type Context } from "./context"; +import { type Context } from "./context"; import { type Update } from "./types"; type FilterFunction = (ctx: C) => ctx is D; @@ -512,12 +512,51 @@ type FilteredContext = // generate a structure with all aliases for a narrowed update type FilteredContextCore = & Record<"update", U> - & AliasProps> & Shortcuts; // helper type to infer shortcuts on context object based on present properties, // must be in sync with shortcut impl! interface Shortcuts { + message: [U["message"]] extends [object] ? U["message"] : undefined; + edited_message: [U["edited_message"]] extends [object] ? U["edited_message"] + : undefined; + channelPost: [U["channel_post"]] extends [object] ? U["channel_post"] + : undefined; + editedChannelPost: [U["edited_channel_post"]] extends [object] + ? U["edited_channel_post"] + : undefined; + messageReaction: [U["message_reaction"]] extends [object] + ? U["message_reaction"] + : undefined; + messageReactionCount: [U["message_reaction_count"]] extends [object] + ? U["message_reaction_count"] + : undefined; + inlineQuery: [U["inline_query"]] extends [object] ? U["inline_query"] + : undefined; + chosenInlineResult: [U["chosen_inline_result"]] extends [object] + ? U["chosen_inline_result"] + : undefined; + callbackQuery: [U["callback_query"]] extends [object] ? U["callback_query"] + : undefined; + shippingQuery: [U["shipping_query"]] extends [object] ? U["shipping_query"] + : undefined; + preCheckoutQuery: [U["pre_checkout_query"]] extends [object] + ? U["pre_checkout_query"] + : undefined; + poll: [U["poll"]] extends [object] ? U["poll"] : undefined; + pollAnswer: [U["poll_answer"]] extends [object] ? U["poll_answer"] + : undefined; + myChatMember: [U["my_chat_member"]] extends [object] ? U["my_chat_member"] + : undefined; + chatMember: [U["chat_member"]] extends [object] ? U["chat_member"] + : undefined; + chatJoinRequest: [U["chat_join_request"]] extends [object] + ? U["chat_join_request"] + : undefined; + chatBoost: [U["chat_boost"]] extends [object] ? U["chat_boost"] : undefined; + removedChatBoost: [U["removed_chat_boost"]] extends [object] + ? U["removed_chat_boost"] + : undefined; msg: [U["callback_query"]] extends [object] ? U["callback_query"]["message"] : [U["message"]] extends [object] ? U["message"] : [U["edited_message"]] extends [object] ? U["edited_message"] From db8f519c601140bb5ff8eee9682dcecac0385211 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 11:43:33 +0200 Subject: [PATCH 05/11] Revert "disable deno lsp" This reverts commit 27077463f9fc8a8797a4286ff69bbf19cb9a6a1c. --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fb84c26a..92956afa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, - "deno.enable": false, + "deno.enable": true, "deno.config": "./deno.jsonc", "deno.lint": true, "[typescript]": { From 436f709564227c4cd6f702ce56b1d494519d4e16 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 11:43:36 +0200 Subject: [PATCH 06/11] Revert "add minimal test case" This reverts commit 2b332db143574ab1b6f2acb6a8b6dae347268353. --- src/bot.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 402851be..22c8a6a9 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -725,5 +725,3 @@ further middleware while your bot is running, consider installing a composer \ instance on your bot, and in turn augment the composer after the fact. This way, \ you can circumvent this protection against memory leaks.`); } - -new Bot("").on("msg", (ctx) => {}); From 42c4b92fbf3562600b61f166223dd761f14aeb12 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 11:44:07 +0200 Subject: [PATCH 07/11] Revert "convert to node project" This reverts commit 2e3b5eebf88a6e5a7d79073d66f33f7b328e1c34. --- package.json | 3 +- src/bot.ts | 16 +-- src/composer.ts | 10 +- src/context.ts | 8 +- src/convenience/constants.ts | 2 +- src/convenience/frameworks.ts | 2 +- src/convenience/inline_query.ts | 2 +- src/convenience/input_media.ts | 2 +- src/convenience/keyboard.ts | 2 +- src/convenience/session.ts | 6 +- src/convenience/webhook.ts | 12 +-- src/core/api.ts | 4 +- src/core/client.ts | 9 +- src/core/error.ts | 2 +- src/core/payload.ts | 4 +- src/filter.ts | 31 ++++-- src/mod.ts | 28 ++--- src/platform.deno.ts | 27 +++++ src/platform.web.ts | 21 ++++ src/types.deno.ts | 179 ++++++++++++++++++++++++++++++++ src/types.ts | 2 +- src/types.web.ts | 133 ++++++++++++++++++++++++ 22 files changed, 438 insertions(+), 67 deletions(-) create mode 100644 src/platform.deno.ts create mode 100644 src/platform.web.ts create mode 100644 src/types.deno.ts create mode 100644 src/types.web.ts diff --git a/package.json b/package.json index b14dfc8c..37e88ccf 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "@types/debug": "^4.1.12", "@types/node": "^12.20.55", "@types/node-fetch": "2.6.2", - "deno2node": "^1.9.0", - "typescript": "^5.5.0-dev.20240401" + "deno2node": "^1.9.0" }, "files": [ "out/" diff --git a/src/bot.ts b/src/bot.ts index 22c8a6a9..8e53ce04 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -5,22 +5,22 @@ import { type Middleware, type ReactionMiddleware, run, -} from "./composer"; -import { Context, type MaybeArray, type ReactionContext } from "./context"; -import { Api } from "./core/api"; +} from "./composer.ts"; +import { Context, type MaybeArray, type ReactionContext } from "./context.ts"; +import { Api } from "./core/api.ts"; import { type ApiClientOptions, type WebhookReplyEnvelope, -} from "./core/client"; -import { GrammyError, HttpError } from "./core/error"; -import { type Filter, type FilterQuery, parse, preprocess } from "./filter"; -import { debug as d } from "./platform.node"; +} from "./core/client.ts"; +import { GrammyError, HttpError } from "./core/error.ts"; +import { type Filter, type FilterQuery, parse, preprocess } from "./filter.ts"; +import { debug as d } from "./platform.deno.ts"; import { type ReactionType, type ReactionTypeEmoji, type Update, type UserFromGetMe, -} from "./types"; +} from "./types.ts"; const debug = d("grammy:bot"); const debugWarn = d("grammy:warn"); const debugErr = d("grammy:error"); diff --git a/src/composer.ts b/src/composer.ts index c367fd68..26f3da8b 100644 --- a/src/composer.ts +++ b/src/composer.ts @@ -10,9 +10,13 @@ import { type MaybeArray, type ReactionContext, type StringWithSuggestions, -} from "./context"; -import { type Filter, type FilterQuery } from "./filter"; -import { type Chat, type ReactionType, type ReactionTypeEmoji } from "./types"; +} from "./context.ts"; +import { type Filter, type FilterQuery } from "./filter.ts"; +import { + type Chat, + type ReactionType, + type ReactionTypeEmoji, +} from "./types.ts"; type MaybePromise = T | Promise; diff --git a/src/context.ts b/src/context.ts index 92dab42f..4cf38ac4 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,12 +1,12 @@ // deno-lint-ignore-file camelcase -import { type Api, type Other as OtherApi } from "./core/api"; -import { type Methods, type RawApi } from "./core/client"; +import { type Api, type Other as OtherApi } from "./core/api.ts"; +import { type Methods, type RawApi } from "./core/client.ts"; import { type Filter, type FilterCore, type FilterQuery, matchFilter, -} from "./filter"; +} from "./filter.ts"; import { type Chat, type ChatPermissions, @@ -26,7 +26,7 @@ import { type Update, type User, type UserFromGetMe, -} from "./types"; +} from "./types.ts"; // === Util types export type MaybeArray = T | T[]; diff --git a/src/convenience/constants.ts b/src/convenience/constants.ts index eb5f7e13..eebac148 100644 --- a/src/convenience/constants.ts +++ b/src/convenience/constants.ts @@ -1,4 +1,4 @@ -import { DEFAULT_UPDATE_TYPES } from "../bot"; +import { DEFAULT_UPDATE_TYPES } from "../bot.ts"; const ALL_UPDATE_TYPES = [ ...DEFAULT_UPDATE_TYPES, diff --git a/src/convenience/frameworks.ts b/src/convenience/frameworks.ts index 2647258f..8f9ae289 100644 --- a/src/convenience/frameworks.ts +++ b/src/convenience/frameworks.ts @@ -1,4 +1,4 @@ -import { type Update } from "../types"; +import { type Update } from "../types.ts"; const SECRET_HEADER = "X-Telegram-Bot-Api-Secret-Token"; const SECRET_HEADER_LOWERCASE = SECRET_HEADER.toLowerCase(); diff --git a/src/convenience/inline_query.ts b/src/convenience/inline_query.ts index e77996a7..eb4b1027 100644 --- a/src/convenience/inline_query.ts +++ b/src/convenience/inline_query.ts @@ -26,7 +26,7 @@ import { type InputTextMessageContent, type InputVenueMessageContent, type LabeledPrice, -} from "../types"; +} from "../types.ts"; type InlineQueryResultOptions = Omit< T, diff --git a/src/convenience/input_media.ts b/src/convenience/input_media.ts index 41f938c5..6ce459b4 100644 --- a/src/convenience/input_media.ts +++ b/src/convenience/input_media.ts @@ -5,7 +5,7 @@ import { type InputMediaDocument, type InputMediaPhoto, type InputMediaVideo, -} from "../types"; +} from "../types.ts"; type InputMediaOptions = Omit; diff --git a/src/convenience/keyboard.ts b/src/convenience/keyboard.ts index 6a05259e..a69c2e05 100644 --- a/src/convenience/keyboard.ts +++ b/src/convenience/keyboard.ts @@ -6,7 +6,7 @@ import { type KeyboardButtonRequestUsers, type LoginUrl, type SwitchInlineQueryChosenChat, -} from "../types"; +} from "../types.ts"; type KeyboardButtonSource = string | KeyboardButton; type KeyboardSource = KeyboardButtonSource[][] | Keyboard; diff --git a/src/convenience/session.ts b/src/convenience/session.ts index bf1981c8..a01a4ebc 100644 --- a/src/convenience/session.ts +++ b/src/convenience/session.ts @@ -1,6 +1,6 @@ -import { type MiddlewareFn } from "../composer"; -import { type Context } from "../context"; -import { debug as d } from "../platform.node"; +import { type MiddlewareFn } from "../composer.ts"; +import { type Context } from "../context.ts"; +import { debug as d } from "../platform.deno.ts"; const debug = d("grammy:session"); type MaybePromise = Promise | T; diff --git a/src/convenience/webhook.ts b/src/convenience/webhook.ts index 28e551d5..c9d67af3 100644 --- a/src/convenience/webhook.ts +++ b/src/convenience/webhook.ts @@ -1,14 +1,14 @@ // deno-lint-ignore-file no-explicit-any -import { type Bot } from "../bot"; -import { type Context } from "../context"; -import { type WebhookReplyEnvelope } from "../core/client"; -import { debug as d, defaultAdapter } from "../platform.node"; -import { type Update } from "../types"; +import { type Bot } from "../bot.ts"; +import { type Context } from "../context.ts"; +import { type WebhookReplyEnvelope } from "../core/client.ts"; +import { debug as d, defaultAdapter } from "../platform.deno.ts"; +import { type Update } from "../types.ts"; import { adapters as nativeAdapters, type FrameworkAdapter, type SupportedFrameworks, -} from "./frameworks"; +} from "./frameworks.ts"; const debugErr = d("grammy:error"); const callbackAdapter: FrameworkAdapter = ( diff --git a/src/core/api.ts b/src/core/api.ts index 493b8203..f8b93693 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -14,7 +14,7 @@ import { type MaskPosition, type PassportElementError, type ReactionType, -} from "../types"; +} from "../types.ts"; import { type ApiClientOptions, createRawApi, @@ -24,7 +24,7 @@ import { type Transformer, type TransformerConsumer, type WebhookReplyEnvelope, -} from "./client"; +} from "./client.ts"; /** * Helper type to derive remaining properties of a given API method call M, diff --git a/src/core/client.ts b/src/core/client.ts index cba9ef1e..cb680809 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,15 +1,15 @@ -import { baseFetchConfig, debug as d } from "../platform.node"; +import { baseFetchConfig, debug as d } from "../platform.deno.ts"; import { type ApiMethods as Telegram, type ApiResponse, type Opts, -} from "../types"; -import { toGrammyError, toHttpError } from "./error"; +} from "../types.ts"; +import { toGrammyError, toHttpError } from "./error.ts"; import { createFormDataPayload, createJsonPayload, requiresFormDataUpload, -} from "./payload"; +} from "./payload.ts"; const debug = d("grammy:core"); export type Methods = string & keyof R; @@ -299,7 +299,6 @@ class ApiClient { // Perform fetch call, and handle networking errors const successPromise = fetch( url instanceof URL ? url.href : url, - // @ts-ignore TODO: remove options, ).catch(toHttpError(method, opts.sensitiveLogs)); // Those are the three possible outcomes of the fetch call: diff --git a/src/core/error.ts b/src/core/error.ts index f8427ea7..0334f097 100644 --- a/src/core/error.ts +++ b/src/core/error.ts @@ -1,4 +1,4 @@ -import { type ApiError, type ResponseParameters } from "../types"; +import { type ApiError, type ResponseParameters } from "../types.ts"; /** * This class represents errors that are thrown by grammY because the Telegram diff --git a/src/core/payload.ts b/src/core/payload.ts index b84efaa9..db7e5ca1 100644 --- a/src/core/payload.ts +++ b/src/core/payload.ts @@ -1,5 +1,5 @@ -import { itrToStream } from "../platform.node"; -import { InputFile } from "../types"; +import { itrToStream } from "../platform.deno.ts"; +import { InputFile } from "../types.ts"; // === Payload types (JSON vs. form data) /** diff --git a/src/filter.ts b/src/filter.ts index 9678b31d..cb8638bd 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file camelcase no-explicit-any -import { type Context } from "./context"; -import { type Update } from "./types"; +import { type Context } from "./context.ts"; +import { type Update } from "./types.ts"; type FilterFunction = (ctx: C) => ctx is D; @@ -122,7 +122,10 @@ function check(original: string[], preprocessed: string[][]): string[][] { function checkOne(filter: string[]): string | true { const [l1, l2, l3, ...n] = filter; if (l1 === undefined) return "Empty filter query given"; - if (!(l1 in UPDATE_KEYS)) { + if ( + !(l1 in UPDATE_KEYS || + l1 === "chat_boost" || l1 === "removed_chat_boost") // TODO: remove + ) { const permitted = Object.keys(UPDATE_KEYS); return `Invalid L1 filter '${l1}' given in '${filter.join(":")}'. \ Permitted values are: ${permitted.map((k) => `'${k}'`).join(", ")}.`; @@ -355,8 +358,8 @@ const UPDATE_KEYS = { chat_join_request: {}, message_reaction: MESSAGE_REACTION_UPDATED_KEYS, message_reaction_count: MESSAGE_REACTION_COUNT_UPDATED_KEYS, - chat_boost: {}, - removed_chat_boost: {}, + // chat_boost: {}, + // removed_chat_boost: {}, } as const; // === Build up all possible filter queries from the above validation structure @@ -404,7 +407,10 @@ type CollapseL2< : never : never); // All queries -type ComputeFilterQueryList = InjectShortcuts; +type ComputeFilterQueryList = + | InjectShortcuts + | "chat_boost" // TODO: remove + | "removed_chat_boost"; /** * Represents a filter query that can be passed to `bot.on`. There are three @@ -615,11 +621,14 @@ const L2_SHORTCUTS = { type L1Shortcuts = KeyOf; type L2Shortcuts = KeyOf; -type ExpandShortcuts = Q extends - `${infer L1}:${infer L2}:${infer L3}` - ? `${ExpandL1}:${ExpandL2}:${L3}` - : Q extends `${infer L1}:${infer L2}` ? `${ExpandL1}:${ExpandL2}` - : ExpandL1; +type ExpandShortcuts = Exclude< + Q extends `${infer L1}:${infer L2}:${infer L3}` + ? `${ExpandL1}:${ExpandL2}:${L3}` + : Q extends `${infer L1}:${infer L2}` + ? `${ExpandL1}:${ExpandL2}` + : ExpandL1, + "chat_boost" | "removed_chat_boost" // TODO: remove +>; type ExpandL1 = S extends L1Shortcuts ? typeof L1_SHORTCUTS[S][number] : S; diff --git a/src/mod.ts b/src/mod.ts index 89471269..01ce6eb7 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -5,9 +5,9 @@ export { BotError, type ErrorHandler, type PollingOptions, -} from "./bot"; +} from "./bot.ts"; -export { InputFile } from "./types"; +export { InputFile } from "./types.ts"; export { type CallbackQueryContext, @@ -18,15 +18,15 @@ export { type HearsContext, type InlineQueryContext, type ReactionContext, -} from "./context"; +} from "./context.ts"; // Convenience stuff, built-in plugins, and helpers -export * from "./convenience/constants"; -export * from "./convenience/inline_query"; -export * from "./convenience/input_media"; -export * from "./convenience/keyboard"; -export * from "./convenience/session"; -export * from "./convenience/webhook"; +export * from "./convenience/constants.ts"; +export * from "./convenience/inline_query.ts"; +export * from "./convenience/input_media.ts"; +export * from "./convenience/keyboard.ts"; +export * from "./convenience/session.ts"; +export * from "./convenience/webhook.ts"; // A little more advanced stuff export { @@ -42,12 +42,12 @@ export { type MiddlewareObj, type NextFunction, type ReactionMiddleware, -} from "./composer"; +} from "./composer.ts"; -export { type Filter, type FilterQuery, matchFilter } from "./filter"; +export { type Filter, type FilterQuery, matchFilter } from "./filter.ts"; // Internal stuff for expert users -export { Api } from "./core/api"; +export { Api } from "./core/api.ts"; export { type ApiCallFn, type ApiClientOptions, @@ -55,5 +55,5 @@ export { type TransformableApi, type Transformer, type WebhookReplyEnvelope, -} from "./core/client"; -export { GrammyError, HttpError } from "./core/error"; +} from "./core/client.ts"; +export { GrammyError, HttpError } from "./core/error.ts"; diff --git a/src/platform.deno.ts b/src/platform.deno.ts new file mode 100644 index 00000000..b8f37474 --- /dev/null +++ b/src/platform.deno.ts @@ -0,0 +1,27 @@ +/** Are we running on Deno or in a web browser? */ +export const isDeno = typeof Deno !== "undefined"; + +// === Export debug +import debug from "https://cdn.skypack.dev/debug@4.3.4"; +export { debug }; +const DEBUG = "DEBUG"; +if (isDeno) { + debug.useColors = () => !Deno.noColor; + const env = { name: "env", variable: DEBUG } as const; + const res = await Deno.permissions.query(env); + let namespace: string | undefined = undefined; + if (res.state === "granted") namespace = Deno.env.get(DEBUG); + if (namespace) debug.enable(namespace); + else debug.disable(); +} + +// === Export system-specific operations +// Turn an AsyncIterable into a stream +export const itrToStream = (itr: AsyncIterable) => + ReadableStream.from(itr); + +// === Base configuration for `fetch` calls +export const baseFetchConfig = (_apiRoot: string) => ({}); + +// === Default webhook adapter +export const defaultAdapter = "oak"; diff --git a/src/platform.web.ts b/src/platform.web.ts new file mode 100644 index 00000000..8758e4c6 --- /dev/null +++ b/src/platform.web.ts @@ -0,0 +1,21 @@ +import d from "https://cdn.skypack.dev/debug@4.3.4"; +export { d as debug }; + +// === Export system-specific operations +// Turn an AsyncIterable into a stream +export const itrToStream = (itr: AsyncIterable) => { + // do not assume ReadableStream.from to exist yet + const it = itr[Symbol.asyncIterator](); + return new ReadableStream({ + async pull(controller) { + const chunk = await it.next(); + if (chunk.done) controller.close(); + else controller.enqueue(chunk.value); + }, + }); +}; + +// === Base configuration for `fetch` calls +export const baseFetchConfig = (_apiRoot: string) => ({}); + +export const defaultAdapter = "cloudflare"; diff --git a/src/types.deno.ts b/src/types.deno.ts new file mode 100644 index 00000000..e4393148 --- /dev/null +++ b/src/types.deno.ts @@ -0,0 +1,179 @@ +// === Needed imports +import { basename } from "https://deno.land/std@0.211.0/path/basename.ts"; +import { iterateReader } from "https://deno.land/std@0.211.0/streams/iterate_reader.ts"; +import { + type ApiMethods as ApiMethodsF, + type InputMedia as InputMediaF, + type InputMediaAnimation as InputMediaAnimationF, + type InputMediaAudio as InputMediaAudioF, + type InputMediaDocument as InputMediaDocumentF, + type InputMediaPhoto as InputMediaPhotoF, + type InputMediaVideo as InputMediaVideoF, + type InputSticker as InputStickerF, + type Opts as OptsF, +} from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; +import { debug as d, isDeno } from "./platform.deno.ts"; + +const debug = d("grammy:warn"); + +// === Export all API types +export * from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; + +/** A value, or a potentially async function supplying that value */ +type MaybeSupplier = T | (() => T | Promise); +/** Something that looks like a URL. */ +interface URLLike { + /** + * Identifier of the resource. Must be in a format that can be parsed by the + * URL constructor. + */ + url: string; +} + +// === InputFile handling and File augmenting +/** + * An `InputFile` wraps a number of different sources for [sending + * files](https://grammy.dev/guide/files.html#uploading-your-own-file). + * + * It corresponds to the `InputFile` type in the [Telegram Bot API + * Reference](https://core.telegram.org/bots/api#inputfile). + */ +export class InputFile { + private consumed = false; + private readonly fileData: ConstructorParameters[0]; + /** + * Optional name of the constructed `InputFile` instance. + * + * Check out the + * [documentation](https://grammy.dev/guide/files.html#uploading-your-own-file) + * on sending files with `InputFile`. + */ + public readonly filename?: string; + /** + * Constructs an `InputFile` that can be used in the API to send files. + * + * @param file A path to a local file or a `Buffer` or a `ReadableStream` that specifies the file data + * @param filename Optional name of the file + */ + constructor( + file: MaybeSupplier< + | string + | Blob + | Deno.FsFile + | Response + | URL + | URLLike + | Uint8Array + | ReadableStream + | Iterable + | AsyncIterable + >, + filename?: string, + ) { + this.fileData = file; + filename ??= this.guessFilename(file); + this.filename = filename; + if ( + typeof file === "string" && + (file.startsWith("http:") || file.startsWith("https:")) + ) { + debug( + `InputFile received the local file path '${file}' that looks like a URL. Is this a mistake?`, + ); + } + } + private guessFilename( + file: ConstructorParameters[0], + ): string | undefined { + if (typeof file === "string") return basename(file); + if ("url" in file) return basename(file.url); + if (!(file instanceof URL)) return undefined; + if (file.pathname !== "/") { + const filename = basename(file.pathname); + if (filename) return filename; + } + return basename(file.hostname); + } + /** + * Internal method. Do not use. + * + * Converts this instance into a binary representation that can be sent to + * the Bot API server in the request body. + */ + async toRaw(): Promise< + Uint8Array | Iterable | AsyncIterable + > { + if (this.consumed) { + throw new Error("Cannot reuse InputFile data source!"); + } + const data = this.fileData; + // Handle local files + if (typeof data === "string") { + if (!isDeno) { + throw new Error( + "Reading files by path requires a Deno environment", + ); + } + const file = await Deno.open(data); + return iterateReader(file); + } + if (data instanceof Blob) return data.stream(); + if (isDenoFile(data)) return iterateReader(data); + // Handle Response objects + if (data instanceof Response) { + if (data.body === null) throw new Error(`No response body!`); + return data.body; + } + // Handle URL and URLLike objects + if (data instanceof URL) return fetchFile(data); + if ("url" in data) return fetchFile(data.url); + // Return buffers as-is + if (data instanceof Uint8Array) return data; + // Unwrap supplier functions + if (typeof data === "function") { + return new InputFile(await data()).toRaw(); + } + // Mark streams and iterators as consumed and return them as-is + this.consumed = true; + return data; + } +} + +async function* fetchFile(url: string | URL): AsyncIterable { + const { body } = await fetch(url); + if (body === null) { + throw new Error(`Download failed, no response body from '${url}'`); + } + yield* body; +} +function isDenoFile(data: unknown): data is Deno.FsFile { + return isDeno && data instanceof Deno.FsFile; +} + +// === Export InputFile types +/** Wrapper type to bundle all methods of the Telegram API */ +export type ApiMethods = ApiMethodsF; + +/** Utility type providing the argument type for the given method name or `{}` if the method does not take any parameters */ +export type Opts = OptsF[M]; + +/** This object describes a sticker to be added to a sticker set. */ +export type InputSticker = InputStickerF; + +/** This object represents the content of a media message to be sent. It should be one of +- InputMediaAnimation +- InputMediaDocument +- InputMediaAudio +- InputMediaPhoto +- InputMediaVideo */ +export type InputMedia = InputMediaF; +/** Represents a photo to be sent. */ +export type InputMediaPhoto = InputMediaPhotoF; +/** Represents a video to be sent. */ +export type InputMediaVideo = InputMediaVideoF; +/** Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. */ +export type InputMediaAnimation = InputMediaAnimationF; +/** Represents an audio file to be treated as music to be sent. */ +export type InputMediaAudio = InputMediaAudioF; +/** Represents a general file to be sent. */ +export type InputMediaDocument = InputMediaDocumentF; diff --git a/src/types.ts b/src/types.ts index 9bca599a..abe40e79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1 +1 @@ -export * from "./types.node"; +export * from "./types.deno.ts"; diff --git a/src/types.web.ts b/src/types.web.ts new file mode 100644 index 00000000..4a646e94 --- /dev/null +++ b/src/types.web.ts @@ -0,0 +1,133 @@ +// === Needed imports +import { basename } from "https://deno.land/std@0.211.0/path/basename.ts"; +import { + type ApiMethods as ApiMethodsF, + type InputMedia as InputMediaF, + type InputMediaAnimation as InputMediaAnimationF, + type InputMediaAudio as InputMediaAudioF, + type InputMediaDocument as InputMediaDocumentF, + type InputMediaPhoto as InputMediaPhotoF, + type InputMediaVideo as InputMediaVideoF, + type InputSticker as InputStickerF, + type Opts as OptsF, +} from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; + +// === Export all API types +export * from "https://deno.land/x/grammy_types@v3.5.2/mod.ts"; + +/** Something that looks like a URL. */ +interface URLLike { + /** + * Identifier of the resource. Must be in a format that can be parsed by the + * URL constructor. + */ + url: string; +} + +// === InputFile handling and File augmenting +/** + * An `InputFile` wraps a number of different sources for [sending + * files](https://grammy.dev/guide/files.html#uploading-your-own-file). + * + * It corresponds to the `InputFile` type in the [Telegram Bot API + * Reference](https://core.telegram.org/bots/api#inputfile). + */ +export class InputFile { + private consumed = false; + private readonly fileData: ConstructorParameters[0]; + /** + * Optional name of the constructed `InputFile` instance. + * + * Check out the + * [documentation](https://grammy.dev/guide/files.html#uploading-your-own-file) + * on sending files with `InputFile`. + */ + public readonly filename?: string; + /** + * Constructs an `InputFile` that can be used in the API to send files. + * + * @param file A URL to a file or a `Blob` or other forms of file data + * @param filename Optional name of the file + */ + constructor( + file: + | Blob + | URL + | URLLike + | Uint8Array + | ReadableStream + | Iterable + | AsyncIterable, + filename?: string, + ) { + this.fileData = file; + filename ??= this.guessFilename(file); + this.filename = filename; + } + private guessFilename( + file: ConstructorParameters[0], + ): string | undefined { + if (typeof file === "string") return basename(file); + if (typeof file !== "object") return undefined; + if ("url" in file) return basename(file.url); + if (!(file instanceof URL)) return undefined; + return basename(file.pathname) || basename(file.hostname); + } + /** + * Internal method. Do not use. + * + * Converts this instance into a binary representation that can be sent to + * the Bot API server in the request body. + */ + toRaw(): Uint8Array | Iterable | AsyncIterable { + if (this.consumed) { + throw new Error("Cannot reuse InputFile data source!"); + } + const data = this.fileData; + // Handle local files + if (data instanceof Blob) return data.stream(); + // Handle URL and URLLike objects + if (data instanceof URL) return fetchFile(data); + if ("url" in data) return fetchFile(data.url); + // Mark streams and iterators as consumed + if (!(data instanceof Uint8Array)) this.consumed = true; + // Return buffers and byte streams as-is + return data; + } +} + +async function* fetchFile(url: string | URL): AsyncIterable { + const { body } = await fetch(url); + if (body === null) { + throw new Error(`Download failed, no response body from '${url}'`); + } + yield* body; +} + +// === Export InputFile types +/** Wrapper type to bundle all methods of the Telegram API */ +export type ApiMethods = ApiMethodsF; + +/** Utility type providing the argument type for the given method name or `{}` if the method does not take any parameters */ +export type Opts = OptsF[M]; + +/** This object describes a sticker to be added to a sticker set. */ +export type InputSticker = InputStickerF; + +/** This object represents the content of a media message to be sent. It should be one of +- InputMediaAnimation +- InputMediaDocument +- InputMediaAudio +- InputMediaPhoto +- InputMediaVideo */ +export type InputMedia = InputMediaF; +/** Represents a photo to be sent. */ +export type InputMediaPhoto = InputMediaPhotoF; +/** Represents a video to be sent. */ +export type InputMediaVideo = InputMediaVideoF; +/** Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. */ +export type InputMediaAnimation = InputMediaAnimationF; +/** Represents an audio file to be treated as music to be sent. */ +export type InputMediaAudio = InputMediaAudioF; +/** Represents a general file to be sent. */ +export type InputMediaDocument = InputMediaDocumentF; From f6c7aa9a21b80bd1e89b60491984de96b28b11b8 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 19:10:11 +0200 Subject: [PATCH 08/11] test: add test for type inference --- test/filter.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/filter.test.ts b/test/filter.test.ts index c9b88659..93f42f96 100644 --- a/test/filter.test.ts +++ b/test/filter.test.ts @@ -1,4 +1,4 @@ -import { type Context, type FilterQuery, matchFilter } from "../src/mod.ts"; +import { Context, type FilterQuery, matchFilter } from "../src/mod.ts"; import { assert, assertThrows, describe, it } from "./deps.test.ts"; describe("matchFilter", () => { @@ -112,4 +112,25 @@ describe("matchFilter", () => { ); } }); + + it("should work with correct type-inference", () => { + const ctx = new Context( + // deno-lint-ignore no-explicit-any + { update: { message: { text: "" } } } as any, + // deno-lint-ignore no-explicit-any + undefined as any, + // deno-lint-ignore no-explicit-any + undefined as any, + ); + const pred = matchFilter([":text", "callback_query:data"]); + if (pred(ctx)) { + if (ctx.callbackQuery) { + const s: string = ctx.update.callback_query.data; + assert(s); + } else { + const t: string = (ctx.message ?? ctx.channelPost).text; + assert(t); + } + } + }); }); From fe9c97c9bfe84644018dde06beaccbb95c964aa4 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 19:19:34 +0200 Subject: [PATCH 09/11] actually run test --- test/filter.test.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/filter.test.ts b/test/filter.test.ts index 93f42f96..9a4036bd 100644 --- a/test/filter.test.ts +++ b/test/filter.test.ts @@ -1,5 +1,11 @@ import { Context, type FilterQuery, matchFilter } from "../src/mod.ts"; -import { assert, assertThrows, describe, it } from "./deps.test.ts"; +import { + assert, + assertEquals, + assertThrows, + describe, + it, +} from "./deps.test.ts"; describe("matchFilter", () => { it("should reject empty filters", () => { @@ -113,10 +119,11 @@ describe("matchFilter", () => { } }); - it("should work with correct type-inference", () => { + it.only("should work with correct type-inference", () => { + const text = "I <3 grammY"; const ctx = new Context( // deno-lint-ignore no-explicit-any - { update: { message: { text: "" } } } as any, + { message: { text } } as any, // deno-lint-ignore no-explicit-any undefined as any, // deno-lint-ignore no-explicit-any @@ -127,10 +134,13 @@ describe("matchFilter", () => { if (ctx.callbackQuery) { const s: string = ctx.update.callback_query.data; assert(s); + throw "never"; } else { - const t: string = (ctx.message ?? ctx.channelPost).text; - assert(t); + const t: string = (ctx.channelPost ?? ctx.message).text; + assertEquals(t, text); } + } else { + throw "never"; } }); }); From 40b129ee27043b2b6ef025e6848e550ee3664f2b Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 19:21:04 +0200 Subject: [PATCH 10/11] run all tests again --- test/filter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/filter.test.ts b/test/filter.test.ts index 9a4036bd..20b9d894 100644 --- a/test/filter.test.ts +++ b/test/filter.test.ts @@ -119,7 +119,7 @@ describe("matchFilter", () => { } }); - it.only("should work with correct type-inference", () => { + it("should work with correct type-inference", () => { const text = "I <3 grammY"; const ctx = new Context( // deno-lint-ignore no-explicit-any From 8598b02acb585c8c8703ec19442a0175e508143a Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Mon, 1 Apr 2024 20:04:02 +0200 Subject: [PATCH 11/11] remove workaround for too complex types --- src/filter.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/filter.ts b/src/filter.ts index cb8638bd..ac579bc6 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -122,10 +122,7 @@ function check(original: string[], preprocessed: string[][]): string[][] { function checkOne(filter: string[]): string | true { const [l1, l2, l3, ...n] = filter; if (l1 === undefined) return "Empty filter query given"; - if ( - !(l1 in UPDATE_KEYS || - l1 === "chat_boost" || l1 === "removed_chat_boost") // TODO: remove - ) { + if (!(l1 in UPDATE_KEYS)) { const permitted = Object.keys(UPDATE_KEYS); return `Invalid L1 filter '${l1}' given in '${filter.join(":")}'. \ Permitted values are: ${permitted.map((k) => `'${k}'`).join(", ")}.`; @@ -358,8 +355,8 @@ const UPDATE_KEYS = { chat_join_request: {}, message_reaction: MESSAGE_REACTION_UPDATED_KEYS, message_reaction_count: MESSAGE_REACTION_COUNT_UPDATED_KEYS, - // chat_boost: {}, - // removed_chat_boost: {}, + chat_boost: {}, + removed_chat_boost: {}, } as const; // === Build up all possible filter queries from the above validation structure @@ -407,10 +404,7 @@ type CollapseL2< : never : never); // All queries -type ComputeFilterQueryList = - | InjectShortcuts - | "chat_boost" // TODO: remove - | "removed_chat_boost"; +type ComputeFilterQueryList = InjectShortcuts; /** * Represents a filter query that can be passed to `bot.on`. There are three @@ -621,14 +615,11 @@ const L2_SHORTCUTS = { type L1Shortcuts = KeyOf; type L2Shortcuts = KeyOf; -type ExpandShortcuts = Exclude< - Q extends `${infer L1}:${infer L2}:${infer L3}` - ? `${ExpandL1}:${ExpandL2}:${L3}` - : Q extends `${infer L1}:${infer L2}` - ? `${ExpandL1}:${ExpandL2}` - : ExpandL1, - "chat_boost" | "removed_chat_boost" // TODO: remove ->; +type ExpandShortcuts = Q extends + `${infer L1}:${infer L2}:${infer L3}` + ? `${ExpandL1}:${ExpandL2}:${L3}` + : Q extends `${infer L1}:${infer L2}` ? `${ExpandL1}:${ExpandL2}` + : ExpandL1; type ExpandL1 = S extends L1Shortcuts ? typeof L1_SHORTCUTS[S][number] : S;