@yaebal/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/lib/api.d.ts +66 -0
  4. package/lib/api.d.ts.map +1 -0
  5. package/lib/api.js +176 -0
  6. package/lib/api.js.map +1 -0
  7. package/lib/bot.d.ts +58 -0
  8. package/lib/bot.d.ts.map +1 -0
  9. package/lib/bot.js +125 -0
  10. package/lib/bot.js.map +1 -0
  11. package/lib/composer.d.ts +97 -0
  12. package/lib/composer.d.ts.map +1 -0
  13. package/lib/composer.js +178 -0
  14. package/lib/composer.js.map +1 -0
  15. package/lib/context.d.ts +40 -0
  16. package/lib/context.d.ts.map +1 -0
  17. package/lib/context.js +78 -0
  18. package/lib/context.js.map +1 -0
  19. package/lib/core.test.d.ts +2 -0
  20. package/lib/core.test.d.ts.map +1 -0
  21. package/lib/core.test.js +87 -0
  22. package/lib/core.test.js.map +1 -0
  23. package/lib/filter.test.d.ts +2 -0
  24. package/lib/filter.test.d.ts.map +1 -0
  25. package/lib/filter.test.js +158 -0
  26. package/lib/filter.test.js.map +1 -0
  27. package/lib/format.d.ts +31 -0
  28. package/lib/format.d.ts.map +1 -0
  29. package/lib/format.js +57 -0
  30. package/lib/format.js.map +1 -0
  31. package/lib/index.d.ts +15 -0
  32. package/lib/index.d.ts.map +1 -0
  33. package/lib/index.js +14 -0
  34. package/lib/index.js.map +1 -0
  35. package/lib/media.d.ts +27 -0
  36. package/lib/media.d.ts.map +1 -0
  37. package/lib/media.js +20 -0
  38. package/lib/media.js.map +1 -0
  39. package/lib/telegram-types.d.ts +21 -0
  40. package/lib/telegram-types.d.ts.map +1 -0
  41. package/lib/telegram-types.js +2 -0
  42. package/lib/telegram-types.js.map +1 -0
  43. package/lib/webhook.d.ts +18 -0
  44. package/lib/webhook.d.ts.map +1 -0
  45. package/lib/webhook.js +82 -0
  46. package/lib/webhook.js.map +1 -0
  47. package/package.json +49 -0
  48. package/src/api.ts +276 -0
  49. package/src/bot.ts +168 -0
  50. package/src/composer.ts +280 -0
  51. package/src/context.ts +109 -0
  52. package/src/core.test.ts +108 -0
  53. package/src/filter.test.ts +202 -0
  54. package/src/format.ts +80 -0
  55. package/src/index.ts +53 -0
  56. package/src/media.ts +29 -0
  57. package/src/telegram-types.ts +25 -0
  58. package/src/webhook.ts +117 -0
package/lib/format.js ADDED
@@ -0,0 +1,57 @@
1
+ /** a piece of text that carries its own entities, used inside `format`. */
2
+ export class Stringable {
3
+ text;
4
+ entities;
5
+ constructor(text, entities = []) {
6
+ this.text = text;
7
+ this.entities = entities;
8
+ }
9
+ }
10
+ function isFormatResult(value) {
11
+ return (typeof value === "object" &&
12
+ value !== null &&
13
+ "text" in value &&
14
+ "entities" in value &&
15
+ Array.isArray(value.entities));
16
+ }
17
+ /** stitches the literal parts and interpolations into one `{ text, entities }`. */
18
+ export function format(strings, ...subs) {
19
+ let text = "";
20
+ const entities = [];
21
+ for (let i = 0; i < strings.length; i++) {
22
+ text += strings[i] ?? "";
23
+ if (i < subs.length) {
24
+ const sub = subs[i];
25
+ const offset = text.length;
26
+ if (isFormatResult(sub)) {
27
+ text += sub.text;
28
+ for (const entity of sub.entities) {
29
+ entities.push({ ...entity, offset: entity.offset + offset });
30
+ }
31
+ }
32
+ else {
33
+ text += String(sub);
34
+ }
35
+ }
36
+ }
37
+ return { text, entities };
38
+ }
39
+ function wrap(type, extra = {}) {
40
+ return (value) => new Stringable(value, [{ type, offset: 0, length: value.length, ...extra }]);
41
+ }
42
+ export const bold = wrap("bold");
43
+ export const italic = wrap("italic");
44
+ export const underline = wrap("underline");
45
+ export const strikethrough = wrap("strikethrough");
46
+ export const spoiler = wrap("spoiler");
47
+ export const code = wrap("code");
48
+ export const pre = wrap("pre");
49
+ export function link(text, url) {
50
+ return new Stringable(text, [{ type: "text_link", offset: 0, length: text.length, url }]);
51
+ }
52
+ export function mention(text, user) {
53
+ return new Stringable(text, [
54
+ { type: "text_mention", offset: 0, length: text.length, user: user },
55
+ ]);
56
+ }
57
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAWA,2EAA2E;AAC3E,MAAM,OAAO,UAAU;IAEZ;IACA;IAFV,YACU,IAAY,EACZ,WAA4B,EAAE;QAD9B,SAAI,GAAJ,IAAI,CAAQ;QACZ,aAAQ,GAAR,QAAQ,CAAsB;IACrC,CAAC;CACJ;AAID,SAAS,cAAc,CAAC,KAAc;IACrC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,OAAO,CAAE,KAAsB,CAAC,QAAQ,CAAC,CAC/C,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,MAAM,CAAC,OAA6B,EAAE,GAAG,IAAkB;IAC1E,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzB,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAe,CAAC;YAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAE3B,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC;gBAEjB,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;oBACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;gBAC9D,CAAC;YACF,CAAC;iBAAM,CAAC;gBACP,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,IAAI,CAAC,IAAY,EAAE,QAAgC,EAAE;IAC7D,OAAO,CAAC,KAAa,EAAc,EAAE,CACpC,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;AACnD,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;AACvC,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;AAE/B,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW;IAC7C,OAAO,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAoB;IACzD,OAAO,IAAI,UAAU,CAAC,IAAI,EAAE;QAC3B,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAa,EAAE;KAC7E,CAAC,CAAC;AACJ,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @yaebal/core — core of Yet Another tElegram Bot Api Library.
3
+ *
4
+ * public surface: the `Bot`, the standalone `Composer`, the `Context`,
5
+ * the entity-based `format` helpers, the low-level `Api`, and the telegram types.
6
+ */
7
+ export { Bot, type BotOptions } from "./bot.js";
8
+ export { Composer, compose, matchQuery, type Filter, type FilterQuery, type Filtered, type Middleware, type NextFn, type Plugin, } from "./composer.js";
9
+ export { Context, type ContextOptions } from "./context.js";
10
+ export { media, isMediaSource, type MediaSource } from "./media.js";
11
+ export { webhookCallback, nodeWebhookCallback, type UpdateSink, type WebhookOptions, } from "./webhook.js";
12
+ export { createApi, TelegramError, type Api, type ApiOptions, type FileReader, type BeforeHook, type AfterHook, type ErrorHook, type ErrorAction, } from "./api.js";
13
+ export { format, Stringable, bold, italic, underline, strikethrough, spoiler, code, pre, link, mention, type FormatResult, } from "./format.js";
14
+ export type * from "./telegram-types.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EACN,QAAQ,EACR,OAAO,EACP,UAAU,EACV,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,MAAM,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EACN,eAAe,EACf,mBAAmB,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,SAAS,EACT,aAAa,EACb,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,WAAW,GAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,MAAM,EACN,UAAU,EACV,IAAI,EACJ,MAAM,EACN,SAAS,EACT,aAAa,EACb,OAAO,EACP,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,OAAO,EACP,KAAK,YAAY,GACjB,MAAM,aAAa,CAAC;AACrB,mBAAmB,qBAAqB,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @yaebal/core — core of Yet Another tElegram Bot Api Library.
3
+ *
4
+ * public surface: the `Bot`, the standalone `Composer`, the `Context`,
5
+ * the entity-based `format` helpers, the low-level `Api`, and the telegram types.
6
+ */
7
+ export { Bot } from "./bot.js";
8
+ export { Composer, compose, matchQuery, } from "./composer.js";
9
+ export { Context } from "./context.js";
10
+ export { media, isMediaSource } from "./media.js";
11
+ export { webhookCallback, nodeWebhookCallback, } from "./webhook.js";
12
+ export { createApi, TelegramError, } from "./api.js";
13
+ export { format, Stringable, bold, italic, underline, strikethrough, spoiler, code, pre, link, mention, } from "./format.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAmB,MAAM,UAAU,CAAC;AAChD,OAAO,EACN,QAAQ,EACR,OAAO,EACP,UAAU,GAOV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAuB,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,aAAa,EAAoB,MAAM,YAAY,CAAC;AACpE,OAAO,EACN,eAAe,EACf,mBAAmB,GAGnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,SAAS,EACT,aAAa,GAQb,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,MAAM,EACN,UAAU,EACV,IAAI,EACJ,MAAM,EACN,SAAS,EACT,aAAa,EACb,OAAO,EACP,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,OAAO,GAEP,MAAM,aAAa,CAAC"}
package/lib/media.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ declare const MEDIA: unique symbol;
2
+ export type MediaSource = {
3
+ readonly [MEDIA]: true;
4
+ } & ({
5
+ kind: "path";
6
+ path: string;
7
+ } | {
8
+ kind: "url";
9
+ url: string;
10
+ } | {
11
+ kind: "buffer";
12
+ buffer: Uint8Array;
13
+ filename?: string;
14
+ } | {
15
+ kind: "fileId";
16
+ fileId: string;
17
+ });
18
+ /** build a {@link MediaSource}. `media.path("./a.jpg")`, `media.url(...)`, etc. */
19
+ export declare const media: {
20
+ path: (path: string) => MediaSource;
21
+ url: (url: string) => MediaSource;
22
+ buffer: (buffer: Uint8Array, filename?: string) => MediaSource;
23
+ fileId: (fileId: string) => MediaSource;
24
+ };
25
+ export declare function isMediaSource(x: unknown): x is MediaSource;
26
+ export {};
27
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,KAAK,EAAE,OAAO,MAAmC,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAA;CAAE,GAAG,CACpD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CACpC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,KAAK;iBACJ,MAAM,KAAG,WAAW;eACtB,MAAM,KAAG,WAAW;qBACd,UAAU,aAAa,MAAM,KAAG,WAAW;qBAM3C,MAAM,KAAG,WAAW;CACrC,CAAC;AAEF,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAE1D"}
package/lib/media.js ADDED
@@ -0,0 +1,20 @@
1
+ // media abstraction (the puregram idea): a uniform way to point at a file —
2
+ // local path, URL, in-memory buffer, or an already-uploaded telegram file_id.
3
+ // the api layer turns each into the right wire form (multipart vs plain string).
4
+ const MEDIA = Symbol.for("yaebal.media");
5
+ /** build a {@link MediaSource}. `media.path("./a.jpg")`, `media.url(...)`, etc. */
6
+ export const media = {
7
+ path: (path) => ({ [MEDIA]: true, kind: "path", path }),
8
+ url: (url) => ({ [MEDIA]: true, kind: "url", url }),
9
+ buffer: (buffer, filename) => ({
10
+ [MEDIA]: true,
11
+ kind: "buffer",
12
+ filename,
13
+ buffer,
14
+ }),
15
+ fileId: (fileId) => ({ [MEDIA]: true, kind: "fileId", fileId }),
16
+ };
17
+ export function isMediaSource(x) {
18
+ return typeof x === "object" && x !== null && MEDIA in x;
19
+ }
20
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,8EAA8E;AAC9E,iFAAiF;AAEjF,MAAM,KAAK,GAAkB,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AASxD,mFAAmF;AACnF,MAAM,CAAC,MAAM,KAAK,GAAG;IACpB,IAAI,EAAE,CAAC,IAAY,EAAe,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC5E,GAAG,EAAE,CAAC,GAAW,EAAe,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IACxE,MAAM,EAAE,CAAC,MAAkB,EAAE,QAAiB,EAAe,EAAE,CAAC,CAAC;QAChE,CAAC,KAAK,CAAC,EAAE,IAAI;QACb,IAAI,EAAE,QAAQ;QACd,QAAQ;QACR,MAAM;KACN,CAAC;IACF,MAAM,EAAE,CAAC,MAAc,EAAe,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpF,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,CAAU;IACvC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Telegram Bot API types — re-exported from the code-generated `@yaebal/types`,
3
+ * the single source of truth (generated from the official schema on every API
4
+ * release). core only keeps the API-response envelope and the `UpdateName`
5
+ * helper, which aren't part of the object schema.
6
+ */
7
+ import type { Update } from "@yaebal/types";
8
+ export type { User, Chat, MessageEntity, Message, CallbackQuery, Update } from "@yaebal/types";
9
+ /** the keys of `Update` that carry a payload (everything except `update_id`). */
10
+ export type UpdateName = Exclude<keyof Update, "update_id" | symbol | number>;
11
+ export interface ApiResponseOk<T> {
12
+ ok: true;
13
+ result: T;
14
+ }
15
+ export interface ApiResponseError {
16
+ ok: false;
17
+ error_code: number;
18
+ description: string;
19
+ }
20
+ export type ApiResponse<T> = ApiResponseOk<T> | ApiResponseError;
21
+ //# sourceMappingURL=telegram-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram-types.d.ts","sourceRoot":"","sources":["../src/telegram-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE/F,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC;AAE9E,MAAM,WAAW,aAAa,CAAC,CAAC;IAC/B,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,CAAC,CAAC;CACV;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,KAAK,CAAC;IACV,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=telegram-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram-types.js","sourceRoot":"","sources":["../src/telegram-types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Update } from "./telegram-types.js";
3
+ /** anything that can take a single update — i.e. a `Bot`. */
4
+ export interface UpdateSink {
5
+ handleUpdate(update: Update): Promise<void>;
6
+ }
7
+ export interface WebhookOptions {
8
+ /** if set, require telegram's `X-Telegram-Bot-Api-Secret-Token` header to match. */
9
+ secretToken?: string;
10
+ }
11
+ /**
12
+ * a fetch-style webhook handler — `(Request) => Response`. works on bun, deno,
13
+ * cloudflare workers, and any runtime with the fetch globals.
14
+ */
15
+ export declare function webhookCallback(bot: UpdateSink, options?: WebhookOptions): (request: Request) => Promise<Response>;
16
+ /** a node `http` webhook handler — `http.createServer(nodeWebhookCallback(bot))`. */
17
+ export declare function nodeWebhookCallback(bot: UpdateSink, options?: WebhookOptions): (req: IncomingMessage, res: ServerResponse) => void;
18
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAiBlD,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,cAAc;IAC9B,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,GAAG,EAAE,UAAU,EACf,OAAO,GAAE,cAAmB,GAC1B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAyBzC;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAClC,GAAG,EAAE,UAAU,EACf,OAAO,GAAE,cAAmB,GAC1B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAkDrD"}
package/lib/webhook.js ADDED
@@ -0,0 +1,82 @@
1
+ /** telegram updates are tiny; reject anything wildly larger to avoid memory abuse. */
2
+ const MAX_BODY = 1 << 20; // 1 MiB
3
+ /**
4
+ * constant-time string compare so the secret-token check can't be timed.
5
+ * pure js (no node:crypto / Buffer) so it runs on node, bun, deno and edge/web.
6
+ */
7
+ function safeEqual(a, b) {
8
+ if (a.length !== b.length)
9
+ return false;
10
+ let diff = 0;
11
+ for (let i = 0; i < a.length; i++)
12
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
13
+ return diff === 0;
14
+ }
15
+ /**
16
+ * a fetch-style webhook handler — `(Request) => Response`. works on bun, deno,
17
+ * cloudflare workers, and any runtime with the fetch globals.
18
+ */
19
+ export function webhookCallback(bot, options = {}) {
20
+ return async (request) => {
21
+ if (request.method !== "POST")
22
+ return new Response("method not allowed", { status: 405 });
23
+ if (options.secretToken &&
24
+ !safeEqual(request.headers.get("x-telegram-bot-api-secret-token") ?? "", options.secretToken)) {
25
+ return new Response("unauthorized", { status: 401 });
26
+ }
27
+ if (Number(request.headers.get("content-length") ?? 0) > MAX_BODY) {
28
+ return new Response("payload too large", { status: 413 });
29
+ }
30
+ let update;
31
+ try {
32
+ update = (await request.json());
33
+ }
34
+ catch {
35
+ return new Response("bad request", { status: 400 });
36
+ }
37
+ await bot.handleUpdate(update);
38
+ return new Response("ok");
39
+ };
40
+ }
41
+ /** a node `http` webhook handler — `http.createServer(nodeWebhookCallback(bot))`. */
42
+ export function nodeWebhookCallback(bot, options = {}) {
43
+ return (req, res) => {
44
+ if (req.method !== "POST") {
45
+ res.statusCode = 405;
46
+ res.end();
47
+ return;
48
+ }
49
+ const got = req.headers["x-telegram-bot-api-secret-token"];
50
+ if (options.secretToken &&
51
+ !safeEqual(typeof got === "string" ? got : "", options.secretToken)) {
52
+ res.statusCode = 401;
53
+ res.end();
54
+ return;
55
+ }
56
+ const chunks = [];
57
+ let size = 0;
58
+ req.on("data", (c) => {
59
+ size += c.length;
60
+ if (size > MAX_BODY) {
61
+ res.statusCode = 413;
62
+ res.end();
63
+ req.destroy();
64
+ return;
65
+ }
66
+ chunks.push(c);
67
+ });
68
+ req.on("end", async () => {
69
+ try {
70
+ const update = JSON.parse(Buffer.concat(chunks).toString("utf8"));
71
+ await bot.handleUpdate(update);
72
+ res.statusCode = 200;
73
+ res.end("ok");
74
+ }
75
+ catch {
76
+ res.statusCode = 400;
77
+ res.end();
78
+ }
79
+ });
80
+ };
81
+ }
82
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@yaebal/core",
3
+ "version": "0.0.1",
4
+ "description": "yaebal core — type-safe, chainable Telegram Bot API framework.",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib",
16
+ "src"
17
+ ],
18
+ "dependencies": {
19
+ "@yaebal/types": "0.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "latest"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "telegram",
29
+ "telegram-bot",
30
+ "bot-api",
31
+ "framework",
32
+ "typescript",
33
+ "yaebal"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/neverlane/yaebal",
39
+ "directory": "packages/core"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "node --test lib"
48
+ }
49
+ }
package/src/api.ts ADDED
@@ -0,0 +1,276 @@
1
+ import { type MediaSource, isMediaSource } from "./media.js";
2
+ import type { ApiResponse, Message, Update, User } from "./telegram-types.js";
3
+
4
+ /**
5
+ * resolve a local `media.path()` into bytes. runtime-specific (node:fs, Bun.file,
6
+ * Deno.readFile), so it's injected rather than imported — that keeps `@yaebal/core`
7
+ * free of any `node:` import and loadable on edge/web. the `yaebal` package wires an
8
+ * auto-detecting default; absent ⇒ `media.path()` throws (e.g. on Cloudflare Workers).
9
+ */
10
+ export type FileReader = (path: string) => Promise<Uint8Array>;
11
+
12
+ /** thrown when telegram replies with `ok: false`. */
13
+ export class TelegramError extends Error {
14
+ readonly method: string;
15
+ readonly code: number;
16
+
17
+ constructor(method: string, code: number, description: string) {
18
+ super(`[${method}] ${code}: ${description}`);
19
+
20
+ this.name = "TelegramError";
21
+ this.method = method;
22
+ this.code = code;
23
+ }
24
+ }
25
+
26
+ /** inspect/rewrite params before a request. return new params to replace them. */
27
+ export type BeforeHook = (
28
+ method: string,
29
+ params: Record<string, unknown> | undefined,
30
+ ) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
31
+
32
+ /** inspect/rewrite the result after a successful request. return a value to replace it. */
33
+ export type AfterHook = (method: string, result: unknown) => unknown | Promise<unknown>;
34
+
35
+ /** what an error hook can ask the client to do. */
36
+ export interface ErrorAction {
37
+ /** re-run the same call. */
38
+ retry?: boolean;
39
+ /** wait this many ms before retrying. */
40
+ delayMs?: number;
41
+ }
42
+
43
+ /** runs when a request throws. `attempt` is the (1-based) attempt that just failed. */
44
+ export type ErrorHook = (
45
+ method: string,
46
+ error: unknown,
47
+ attempt: number,
48
+ ) => ErrorAction | undefined | Promise<ErrorAction | undefined>;
49
+
50
+ /**
51
+ * the API client. known methods are typed; everything else goes through `call`
52
+ * (the puregram passthrough — a new Bot API method works before its types ship).
53
+ * `before` / `after` / `onError` are the extension points plugins hang off of.
54
+ */
55
+ export interface Api {
56
+ call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
57
+ getMe(): Promise<User>;
58
+ getUpdates(params?: Record<string, unknown>): Promise<Update[]>;
59
+ sendMessage(params: Record<string, unknown>): Promise<Message>;
60
+ answerCallbackQuery(params: Record<string, unknown>): Promise<boolean>;
61
+ /** build the download URL for a `file_path` from `getFile`. contains the bot token — don't log it. */
62
+ fileUrl(filePath: string): string;
63
+ /** register a hook that runs before every request; may rewrite params. */
64
+ before(hook: BeforeHook): Api;
65
+ /** register a hook that runs after every successful request; may rewrite the result. */
66
+ after(hook: AfterHook): Api;
67
+ /** register a hook that runs when a request throws; may request a retry. */
68
+ onError(hook: ErrorHook): Api;
69
+ }
70
+
71
+ export interface ApiOptions {
72
+ apiRoot?: string;
73
+ /** resolve `media.path()` to bytes. injected per runtime; absent ⇒ path media throws (e.g. edge). */
74
+ readFile?: FileReader;
75
+ }
76
+
77
+ interface EncodedRequest {
78
+ body: string | FormData | undefined;
79
+ contentType?: string;
80
+ }
81
+
82
+ function containsMedia(v: unknown): boolean {
83
+ if (isMediaSource(v)) return true;
84
+ if (Array.isArray(v)) return v.some(containsMedia);
85
+ if (v !== null && typeof v === "object") return Object.values(v).some(containsMedia);
86
+
87
+ return false;
88
+ }
89
+
90
+ /** basename without node:path — keeps core free of node: imports. */
91
+ const baseName = (p: string): string => p.split(/[\\/]/).pop() || "file";
92
+ const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => {
93
+ const copy = new Uint8Array(bytes.byteLength);
94
+ copy.set(bytes);
95
+
96
+ return copy.buffer;
97
+ };
98
+
99
+ async function mediaToBlob(
100
+ m: MediaSource,
101
+ readFile?: FileReader,
102
+ ): Promise<{ blob: Blob; filename: string }> {
103
+ if (m.kind === "path") {
104
+ if (!readFile)
105
+ throw new Error(
106
+ "media.path() needs a filesystem — use the `yaebal` package (auto-detects node/bun/deno), " +
107
+ "pass `readFile` to the Bot, or send media.buffer()/url() instead (e.g. on edge).",
108
+ );
109
+
110
+ const bytes = await readFile(m.path);
111
+ return { blob: new Blob([toArrayBuffer(bytes)]), filename: baseName(m.path) };
112
+ }
113
+
114
+ if (m.kind === "buffer") {
115
+ return { blob: new Blob([toArrayBuffer(m.buffer)]), filename: m.filename ?? "file" };
116
+ }
117
+
118
+ throw new Error(`mediaToBlob: ${m.kind} is not an uploadable source`);
119
+ }
120
+
121
+ /**
122
+ * encode request params. Plain params (and `url`/`fileId` media) go as JSON;
123
+ * if any `path`/`buffer` media is present the whole request becomes multipart,
124
+ * with each upload attached via `attach://`. exported for testing.
125
+ */
126
+ export async function encodeRequest(
127
+ params: Record<string, unknown> | undefined,
128
+ readFile?: FileReader,
129
+ ): Promise<EncodedRequest> {
130
+ if (!params) return { body: undefined, contentType: "application/json" };
131
+
132
+ // ponytail: media is only handled at the top level. media nested inside arrays
133
+ // or objects (e.g. sendMediaGroup's `media[]`) would serialize to garbage, so
134
+ // fail loud until that method is actually supported.
135
+ if (Object.values(params).some((v) => !isMediaSource(v) && containsMedia(v))) {
136
+ throw new Error(
137
+ "encodeRequest: nested MediaSource (e.g. inside sendMediaGroup) is not supported yet — pass media as a top-level param",
138
+ );
139
+ }
140
+
141
+ const needsUpload = Object.values(params).some(
142
+ (v) => isMediaSource(v) && (v.kind === "path" || v.kind === "buffer"),
143
+ );
144
+
145
+ if (!needsUpload) {
146
+ const inlined: Record<string, unknown> = {};
147
+
148
+ for (const [k, v] of Object.entries(params)) {
149
+ inlined[k] = isMediaSource(v)
150
+ ? v.kind === "url"
151
+ ? v.url
152
+ : (v as { fileId: string }).fileId
153
+ : v;
154
+ }
155
+
156
+ return { body: JSON.stringify(inlined), contentType: "application/json" };
157
+ }
158
+
159
+ const form = new FormData();
160
+ let n = 0;
161
+
162
+ for (const [k, v] of Object.entries(params)) {
163
+ if (v === undefined || v === null) continue;
164
+
165
+ if (isMediaSource(v)) {
166
+ if (v.kind === "fileId") form.set(k, v.fileId);
167
+ else if (v.kind === "url") form.set(k, v.url);
168
+ else {
169
+ const field = `_file${n++}`;
170
+ const { blob, filename } = await mediaToBlob(v, readFile);
171
+
172
+ form.set(field, blob, filename);
173
+ form.set(k, `attach://${field}`);
174
+ }
175
+ } else if (typeof v === "string") {
176
+ form.set(k, v);
177
+ } else {
178
+ form.set(k, JSON.stringify(v));
179
+ }
180
+ }
181
+
182
+ return { body: form };
183
+ }
184
+
185
+ /** builds a callable API client with before/after/error hooks. */
186
+ export function createApi(token: string, options: ApiOptions = {}): Api {
187
+ const apiRoot = options.apiRoot ?? "https://api.telegram.org";
188
+ const beforeHooks: BeforeHook[] = [];
189
+ const afterHooks: AfterHook[] = [];
190
+ const errorHooks: ErrorHook[] = [];
191
+
192
+ const rawCall = async <T>(method: string, params?: Record<string, unknown>): Promise<T> => {
193
+ const { body, contentType } = await encodeRequest(params, options.readFile);
194
+ const res = await fetch(`${apiRoot}/bot${token}/${method}`, {
195
+ method: "POST",
196
+ // for multipart, let fetch set the content-type (with its boundary).
197
+ headers: contentType ? { "content-type": contentType } : undefined,
198
+ body,
199
+ });
200
+
201
+ const data = (await res.json()) as ApiResponse<T>;
202
+ if (!data.ok) throw new TelegramError(method, data.error_code, data.description);
203
+
204
+ return data.result;
205
+ };
206
+
207
+ const call = async <T = unknown>(
208
+ method: string,
209
+ params?: Record<string, unknown>,
210
+ ): Promise<T> => {
211
+ let p = params;
212
+ for (const hook of beforeHooks) {
213
+ const next = await hook(method, p);
214
+ if (next !== undefined) p = next;
215
+ }
216
+
217
+ // ponytail: retry loop is bounded by the error hooks themselves (e.g. again caps
218
+ // attempts). with no hook requesting a retry it throws on the first failure.
219
+ for (let attempt = 1; ; attempt++) {
220
+ try {
221
+ let result = await rawCall<T>(method, p);
222
+
223
+ for (const hook of afterHooks) {
224
+ const next = await hook(method, result);
225
+ if (next !== undefined) result = next as Awaited<T>;
226
+ }
227
+
228
+ return result;
229
+ } catch (error) {
230
+ let retry: ErrorAction | undefined;
231
+
232
+ for (const hook of errorHooks) {
233
+ const action = await hook(method, error, attempt);
234
+ if (action?.retry) {
235
+ retry = action;
236
+ break;
237
+ }
238
+ }
239
+
240
+ if (!retry) throw error;
241
+ if (retry.delayMs) await new Promise((r) => setTimeout(r, retry.delayMs));
242
+ }
243
+ }
244
+ };
245
+
246
+ const registrar: Record<string, unknown> = {
247
+ call,
248
+ fileUrl: (filePath: string) => `${apiRoot}/file/bot${token}/${filePath}`,
249
+ before(hook: BeforeHook) {
250
+ beforeHooks.push(hook);
251
+ return api;
252
+ },
253
+ after(hook: AfterHook) {
254
+ afterHooks.push(hook);
255
+ return api;
256
+ },
257
+ onError(hook: ErrorHook) {
258
+ errorHooks.push(hook);
259
+ return api;
260
+ },
261
+ };
262
+
263
+ const api = new Proxy(registrar, {
264
+ get(obj, prop: string) {
265
+ if (prop in obj) return obj[prop];
266
+
267
+ // lazily materialise `api.<method>(params)` → call(method, params).
268
+ const method = (params?: Record<string, unknown>) => call(prop, params);
269
+
270
+ obj[prop] = method;
271
+ return method;
272
+ },
273
+ }) as unknown as Api;
274
+
275
+ return api;
276
+ }