teledzik 1.0.0

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 (198) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1126 -0
  3. package/filters.d.ts +1 -0
  4. package/filters.js +1 -0
  5. package/format.d.ts +1 -0
  6. package/format.js +1 -0
  7. package/future.d.ts +1 -0
  8. package/future.js +1 -0
  9. package/lib/button.js +101 -0
  10. package/lib/cli.mjs +105 -0
  11. package/lib/composer.js +581 -0
  12. package/lib/context.js +1300 -0
  13. package/lib/core/helpers/ai-stream.js +69 -0
  14. package/lib/core/helpers/args.js +58 -0
  15. package/lib/core/helpers/check.js +56 -0
  16. package/lib/core/helpers/compact.js +17 -0
  17. package/lib/core/helpers/deunionize.js +13 -0
  18. package/lib/core/helpers/formatting.js +89 -0
  19. package/lib/core/helpers/input-box.js +87 -0
  20. package/lib/core/helpers/rich-sanitizer.js +31 -0
  21. package/lib/core/helpers/smart-queue.js +70 -0
  22. package/lib/core/helpers/util.js +50 -0
  23. package/lib/core/network/client.js +318 -0
  24. package/lib/core/network/error.js +21 -0
  25. package/lib/core/network/multipart-stream.js +61 -0
  26. package/lib/core/network/polling.js +86 -0
  27. package/lib/core/network/webhook.js +54 -0
  28. package/lib/core/types/rich-message.js +287 -0
  29. package/lib/core/types/typegram.js +18 -0
  30. package/lib/filters.js +69 -0
  31. package/lib/format.js +38 -0
  32. package/lib/future.js +166 -0
  33. package/lib/index.js +61 -0
  34. package/lib/input.js +61 -0
  35. package/lib/markup.js +111 -0
  36. package/lib/middleware.js +2 -0
  37. package/lib/paginated.js +76 -0
  38. package/lib/reactions.js +81 -0
  39. package/lib/router.js +45 -0
  40. package/lib/scenes/base.js +39 -0
  41. package/lib/scenes/context.js +99 -0
  42. package/lib/scenes/form-scene.js +23 -0
  43. package/lib/scenes/index.js +21 -0
  44. package/lib/scenes/stage.js +49 -0
  45. package/lib/scenes/wizard/context.js +30 -0
  46. package/lib/scenes/wizard/index.js +45 -0
  47. package/lib/scenes.js +17 -0
  48. package/lib/session.js +163 -0
  49. package/lib/telegraf.js +274 -0
  50. package/lib/telegram-types.js +6 -0
  51. package/lib/telegram.js +1286 -0
  52. package/lib/types.js +2 -0
  53. package/lib/utils.js +5 -0
  54. package/markup.d.ts +1 -0
  55. package/markup.js +1 -0
  56. package/package.json +141 -0
  57. package/scenes.d.ts +1 -0
  58. package/scenes.js +1 -0
  59. package/session.d.ts +1 -0
  60. package/session.js +1 -0
  61. package/src/button.ts +182 -0
  62. package/src/composer.ts +1008 -0
  63. package/src/context.ts +1778 -0
  64. package/src/core/helpers/ai-stream.ts +96 -0
  65. package/src/core/helpers/args.ts +63 -0
  66. package/src/core/helpers/check.ts +71 -0
  67. package/src/core/helpers/compact.ts +18 -0
  68. package/src/core/helpers/deunionize.ts +26 -0
  69. package/src/core/helpers/formatting.ts +119 -0
  70. package/src/core/helpers/input-box.ts +86 -0
  71. package/src/core/helpers/rich-sanitizer.ts +36 -0
  72. package/src/core/helpers/smart-queue.ts +89 -0
  73. package/src/core/helpers/util.ts +96 -0
  74. package/src/core/network/client.ts +396 -0
  75. package/src/core/network/error.ts +29 -0
  76. package/src/core/network/multipart-stream.ts +45 -0
  77. package/src/core/network/polling.ts +94 -0
  78. package/src/core/network/webhook.ts +58 -0
  79. package/src/core/types/rich-message.ts +375 -0
  80. package/src/core/types/typegram.ts +44 -0
  81. package/src/filters.js +69 -0
  82. package/src/filters.ts +109 -0
  83. package/src/format.js +38 -0
  84. package/src/format.ts +110 -0
  85. package/src/future.js +147 -0
  86. package/src/future.ts +231 -0
  87. package/src/index.ts +27 -0
  88. package/src/input.ts +59 -0
  89. package/src/markup.js +93 -0
  90. package/src/markup.ts +142 -0
  91. package/src/middleware.ts +24 -0
  92. package/src/paginated.ts +75 -0
  93. package/src/reactions.ts +118 -0
  94. package/src/router.ts +55 -0
  95. package/src/scenes/base.ts +52 -0
  96. package/src/scenes/context.ts +136 -0
  97. package/src/scenes/form-scene.ts +34 -0
  98. package/src/scenes/index.ts +21 -0
  99. package/src/scenes/stage.ts +71 -0
  100. package/src/scenes/wizard/context.ts +58 -0
  101. package/src/scenes/wizard/index.ts +63 -0
  102. package/src/scenes.js +17 -0
  103. package/src/scenes.ts +1 -0
  104. package/src/session.js +175 -0
  105. package/src/session.ts +204 -0
  106. package/src/telegraf.ts +385 -0
  107. package/src/telegram-types.ts +256 -0
  108. package/src/telegram.ts +1700 -0
  109. package/src/types.js +2 -0
  110. package/src/types.ts +2 -0
  111. package/src/utils.js +5 -0
  112. package/src/utils.ts +1 -0
  113. package/types.d.ts +1 -0
  114. package/types.js +1 -0
  115. package/typings/button.d.ts +35 -0
  116. package/typings/button.d.ts.map +1 -0
  117. package/typings/composer.d.ts +226 -0
  118. package/typings/composer.d.ts.map +1 -0
  119. package/typings/context.d.ts +711 -0
  120. package/typings/context.d.ts.map +1 -0
  121. package/typings/core/helpers/ai-stream.d.ts +33 -0
  122. package/typings/core/helpers/args.d.ts +10 -0
  123. package/typings/core/helpers/args.d.ts.map +1 -0
  124. package/typings/core/helpers/check.d.ts +55 -0
  125. package/typings/core/helpers/check.d.ts.map +1 -0
  126. package/typings/core/helpers/compact.d.ts +3 -0
  127. package/typings/core/helpers/compact.d.ts.map +1 -0
  128. package/typings/core/helpers/deunionize.d.ts +17 -0
  129. package/typings/core/helpers/deunionize.d.ts.map +1 -0
  130. package/typings/core/helpers/formatting.d.ts +29 -0
  131. package/typings/core/helpers/formatting.d.ts.map +1 -0
  132. package/typings/core/helpers/input-box.d.ts +32 -0
  133. package/typings/core/helpers/rich-sanitizer.d.ts +5 -0
  134. package/typings/core/helpers/smart-queue.d.ts +18 -0
  135. package/typings/core/helpers/util.d.ts +26 -0
  136. package/typings/core/helpers/util.d.ts.map +1 -0
  137. package/typings/core/network/client.d.ts +54 -0
  138. package/typings/core/network/client.d.ts.map +1 -0
  139. package/typings/core/network/error.d.ts +15 -0
  140. package/typings/core/network/error.d.ts.map +1 -0
  141. package/typings/core/network/multipart-stream.d.ts +19 -0
  142. package/typings/core/network/multipart-stream.d.ts.map +1 -0
  143. package/typings/core/network/polling.d.ts +15 -0
  144. package/typings/core/network/polling.d.ts.map +1 -0
  145. package/typings/core/network/webhook.d.ts +6 -0
  146. package/typings/core/network/webhook.d.ts.map +1 -0
  147. package/typings/core/types/rich-message.d.ts +143 -0
  148. package/typings/core/types/rich-message.d.ts.map +1 -0
  149. package/typings/core/types/typegram.d.ts +36 -0
  150. package/typings/core/types/typegram.d.ts.map +1 -0
  151. package/typings/filters.d.ts +17 -0
  152. package/typings/filters.d.ts.map +1 -0
  153. package/typings/format.d.ts +21 -0
  154. package/typings/format.d.ts.map +1 -0
  155. package/typings/future.d.ts +11 -0
  156. package/typings/future.d.ts.map +1 -0
  157. package/typings/index.d.ts +21 -0
  158. package/typings/index.d.ts.map +1 -0
  159. package/typings/input.d.ts +58 -0
  160. package/typings/input.d.ts.map +1 -0
  161. package/typings/markup.d.ts +26 -0
  162. package/typings/markup.d.ts.map +1 -0
  163. package/typings/middleware.d.ts +7 -0
  164. package/typings/middleware.d.ts.map +1 -0
  165. package/typings/paginated.d.ts +20 -0
  166. package/typings/reactions.d.ts +31 -0
  167. package/typings/reactions.d.ts.map +1 -0
  168. package/typings/router.d.ts +20 -0
  169. package/typings/router.d.ts.map +1 -0
  170. package/typings/scenes/base.d.ts +21 -0
  171. package/typings/scenes/base.d.ts.map +1 -0
  172. package/typings/scenes/context.d.ts +35 -0
  173. package/typings/scenes/context.d.ts.map +1 -0
  174. package/typings/scenes/form-scene.d.ts +17 -0
  175. package/typings/scenes/index.d.ts +10 -0
  176. package/typings/scenes/index.d.ts.map +1 -0
  177. package/typings/scenes/stage.d.ts +23 -0
  178. package/typings/scenes/stage.d.ts.map +1 -0
  179. package/typings/scenes/wizard/context.d.ts +28 -0
  180. package/typings/scenes/wizard/context.d.ts.map +1 -0
  181. package/typings/scenes/wizard/index.d.ts +15 -0
  182. package/typings/scenes/wizard/index.d.ts.map +1 -0
  183. package/typings/scenes.d.ts +1 -0
  184. package/typings/scenes.d.ts.map +1 -0
  185. package/typings/session.d.ts +54 -0
  186. package/typings/session.d.ts.map +1 -0
  187. package/typings/telegraf.d.ts +118 -0
  188. package/typings/telegraf.d.ts.map +1 -0
  189. package/typings/telegram-types.d.ts +140 -0
  190. package/typings/telegram-types.d.ts.map +1 -0
  191. package/typings/telegram.d.ts +697 -0
  192. package/typings/telegram.d.ts.map +1 -0
  193. package/typings/types.d.ts +2 -0
  194. package/typings/types.d.ts.map +1 -0
  195. package/typings/utils.d.ts +1 -0
  196. package/typings/utils.d.ts.map +1 -0
  197. package/utils.d.ts +1 -0
  198. package/utils.js +1 -0
@@ -0,0 +1,96 @@
1
+ import { Context } from '../../context'
2
+ import { InputRichMessage } from '../types/rich-message'
3
+
4
+ export interface AIStreamOptions {
5
+ /** Minimum interval in ms between Telegram message edits (default: 800ms) */
6
+ minUpdateIntervalMs?: number
7
+ /** Use draft (sendRichMessageDraft) if true (default: false) */
8
+ useDraft?: boolean
9
+ /** Draft ID for sendRichMessageDraft */
10
+ draftId?: number
11
+ }
12
+
13
+ /**
14
+ * AI Live Stream Adapter for Telegram Bots.
15
+ * Throttles live token stream edits to prevent Telegram rate limit bans.
16
+ */
17
+ export class AIStreamAdapter {
18
+ private buffer = ''
19
+ private lastUpdate = 0
20
+ private timer: ReturnType<typeof setTimeout> | null = null
21
+ private readonly minInterval: number
22
+ private readonly ctx: Context<any>
23
+ private readonly options: AIStreamOptions
24
+ private sentMessageId?: number
25
+
26
+ constructor(ctx: Context<any>, options: AIStreamOptions = {}) {
27
+ this.ctx = ctx
28
+ this.options = options
29
+ this.minInterval = options.minUpdateIntervalMs ?? 800
30
+ }
31
+
32
+ /**
33
+ * Append new token chunk from LLM stream.
34
+ */
35
+ async push(chunk: string): Promise<void> {
36
+ this.buffer += chunk
37
+ await this.scheduleUpdate()
38
+ }
39
+
40
+ /**
41
+ * Finalize the stream and send the full final content.
42
+ */
43
+ async end(finalContent?: string): Promise<any> {
44
+ if (this.timer) {
45
+ clearTimeout(this.timer)
46
+ this.timer = null
47
+ }
48
+
49
+ if (finalContent !== undefined) {
50
+ this.buffer = finalContent
51
+ }
52
+
53
+ return this.flush(true)
54
+ }
55
+
56
+ private async scheduleUpdate(): Promise<void> {
57
+ const now = Date.now()
58
+ const elapsed = now - this.lastUpdate
59
+
60
+ if (elapsed >= this.minInterval) {
61
+ await this.flush()
62
+ } else if (!this.timer) {
63
+ this.timer = setTimeout(async () => {
64
+ this.timer = null
65
+ await this.flush()
66
+ }, this.minInterval - elapsed)
67
+ }
68
+ }
69
+
70
+ private async flush(isFinal = false): Promise<any> {
71
+ if (!this.buffer) return
72
+
73
+ this.lastUpdate = Date.now()
74
+
75
+ if (this.options.useDraft && this.options.draftId) {
76
+ const richMsg: InputRichMessage = { html: `<p>${this.buffer}${isFinal ? '' : ' ▍'}</p>` }
77
+ return this.ctx.sendRichMessageDraft(this.options.draftId, richMsg)
78
+ }
79
+
80
+ const text = `${this.buffer}${isFinal ? '' : ' ▍'}`
81
+
82
+ if (!this.sentMessageId) {
83
+ const sent = await this.ctx.reply(text, { parse_mode: 'HTML' })
84
+ this.sentMessageId = sent.message_id
85
+ return sent
86
+ } else {
87
+ return this.ctx.telegram.editMessageText(
88
+ this.ctx.chat?.id,
89
+ this.sentMessageId,
90
+ undefined,
91
+ text,
92
+ { parse_mode: 'HTML' }
93
+ )
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,63 @@
1
+ interface Entity {
2
+ /** Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag), “cashtag” ($USD), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers) */
3
+ type: string
4
+ /** Offset in UTF-16 code units to the start of the entity */
5
+ offset: number
6
+ /** Length of the entity in UTF-16 code units */
7
+ length: number
8
+ }
9
+
10
+ const SINGLE_QUOTE = "'"
11
+ const DOUBLE_QUOTE = '"'
12
+
13
+ export function argsParser(
14
+ str: string,
15
+ entities: Entity[] = [],
16
+ entityOffset = 0
17
+ ) {
18
+ const mentions: { [offset: string]: number } = {}
19
+ for (const entity of entities) // extract all text_mentions into an { offset: length } map
20
+ if (entity.type === 'text_mention' || entity.type === 'text_link')
21
+ mentions[entity.offset - entityOffset] = entity.length
22
+
23
+ const args: string[] = []
24
+ let done = 0
25
+ let inside: `'` | `"` | undefined = undefined
26
+ let buf = ''
27
+
28
+ function flush(to: number) {
29
+ if (done !== to) args.push(buf + str.slice(done, to)), (inside = undefined)
30
+ buf = ''
31
+ done = to + 1
32
+ }
33
+
34
+ for (let i = 0; i < str.length; i++) {
35
+ const char = str[i]
36
+ // quick lookup length of mention starting at i
37
+ const mention = mentions[i]
38
+ if (mention) {
39
+ // if we're inside a quote, eagerly flush existing state
40
+ flush(i)
41
+ // this also consumes current index, so decrement
42
+ done--
43
+ // fast forward to end of mention
44
+ i += mention
45
+ flush(i)
46
+ } else if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE)
47
+ if (inside)
48
+ if (inside === char) flush(i)
49
+ else continue
50
+ else flush(i), (inside = char)
51
+ else if (char === ' ')
52
+ if (inside) continue
53
+ else flush(i)
54
+ else if (char === '\n') flush(i)
55
+ else if (char === '\\')
56
+ (buf += str.slice(done, i)), (done = ++i) // skip parsing the next char
57
+ else continue
58
+ }
59
+
60
+ if (done < str.length) flush(str.length)
61
+
62
+ return args
63
+ }
@@ -0,0 +1,71 @@
1
+ interface Mapping {
2
+ string: string
3
+ number: number
4
+ bigint: bigint
5
+ boolean: boolean
6
+ symbol: symbol
7
+ undefined: undefined
8
+ object: Record<string, unknown>
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ function: (...props: any[]) => any
11
+ }
12
+
13
+ /**
14
+ * Checks if a given object has a property with a given name.
15
+ *
16
+ * Example invocation:
17
+ * ```js
18
+ * let obj = { 'foo': 'bar', 'baz': () => {} }
19
+ * hasProp(obj, 'foo') // true
20
+ * hasProp(obj, 'baz') // true
21
+ * hasProp(obj, 'abc') // false
22
+ * ```
23
+ *
24
+ * @param obj An object to test
25
+ * @param prop The name of the property
26
+ */
27
+ export function hasProp<O extends object, K extends PropertyKey>(
28
+ obj: O | undefined,
29
+ prop: K
30
+ ): obj is O & Record<K, unknown> {
31
+ return obj !== undefined && prop in obj
32
+ }
33
+ /**
34
+ * Checks if a given object has a property with a given name.
35
+ * Furthermore performs a `typeof` check on the property if it exists.
36
+ *
37
+ * Example invocation:
38
+ * ```js
39
+ * let obj = { 'foo': 'bar', 'baz': () => {} }
40
+ * hasPropType(obj, 'foo', 'string') // true
41
+ * hasPropType(obj, 'baz', 'function') // true
42
+ * hasPropType(obj, 'abc', 'number') // false
43
+ * ```
44
+ *
45
+ * @param obj An object to test
46
+ * @param prop The name of the property
47
+ * @param type The type the property is expected to have
48
+ */
49
+ export function hasPropType<
50
+ O extends object,
51
+ K extends PropertyKey,
52
+ T extends keyof Mapping,
53
+ V extends Mapping[T],
54
+ >(obj: O | undefined, prop: K, type: T): obj is O & Record<K, V> {
55
+ return hasProp(obj, prop) && type === typeof obj[prop]
56
+ }
57
+
58
+ /**
59
+ * Checks if the supplied array has two dimensions or not.
60
+ *
61
+ * Example invocations:
62
+ * is2D([]) // false
63
+ * is2D([[]]) // true
64
+ * is2D([[], []]) // true
65
+ * is2D([42]) // false
66
+ *
67
+ * @param arr an array with one or two dimensions
68
+ */
69
+ export function is2D<E>(arr: E[] | E[][]): arr is E[][] {
70
+ return Array.isArray(arr[0])
71
+ }
@@ -0,0 +1,18 @@
1
+ export function compactOptions<T extends { [key: string]: unknown }>(
2
+ options?: T
3
+ ): T | undefined {
4
+ if (!options) {
5
+ return options
6
+ }
7
+
8
+ const compacted: Partial<T> = {}
9
+ for (const key in options)
10
+ if (
11
+ // todo(mkr): replace with Object.hasOwn in v5 (Node 16+)
12
+ Object.prototype.hasOwnProperty.call(options, key) &&
13
+ options[key] !== undefined
14
+ )
15
+ compacted[key] = options[key]
16
+
17
+ return compacted as T | undefined
18
+ }
@@ -0,0 +1,26 @@
1
+ export type PropOr<
2
+ T extends object | undefined,
3
+ P extends string | symbol | number,
4
+ D = undefined,
5
+ > = T extends Partial<Record<P, unknown>> ? T[P] : D
6
+
7
+ export type UnionKeys<T> = T extends unknown ? keyof T : never
8
+
9
+ type AddOptionalKeys<K extends PropertyKey> = { readonly [P in K]?: never }
10
+
11
+ /**
12
+ * @see https://millsp.github.io/ts-toolbelt/modules/union_strict.html
13
+ */
14
+ export type Deunionize<B extends object | undefined, T = B> = T extends object
15
+ ? T & AddOptionalKeys<Exclude<UnionKeys<B>, keyof T>>
16
+ : T
17
+
18
+ /**
19
+ * Expose properties from all union variants.
20
+ * @deprectated
21
+ * @see https://github.com/telegraf/telegraf/issues/1388#issuecomment-791573609
22
+ * @see https://millsp.github.io/ts-toolbelt/modules/union_strict.html
23
+ */
24
+ export function deunionize<T extends object | undefined>(t: T) {
25
+ return t as Deunionize<T>
26
+ }
@@ -0,0 +1,119 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
2
+ import { MessageEntity, User } from '@telegraf/types'
3
+ import { Any, zip } from './util'
4
+
5
+ export type Nestable<Kind extends string> =
6
+ | string
7
+ | number
8
+ | boolean
9
+ | FmtString<Kind>
10
+ export type MaybeNestableList<Kind extends string> =
11
+ | Nestable<Kind>
12
+ | readonly Nestable<Kind>[]
13
+
14
+ export interface FmtString<Brand extends string> {
15
+ text: string
16
+ entities?: MessageEntity[]
17
+ parse_mode?: undefined
18
+ __to_nest: Brand
19
+ }
20
+
21
+ export class FmtString<Brand extends string = string>
22
+ implements FmtString<Brand>
23
+ {
24
+ constructor(
25
+ public text: string,
26
+ entities?: MessageEntity[]
27
+ ) {
28
+ if (entities) {
29
+ this.entities = entities
30
+ // force parse_mode to undefined if entities are present
31
+ this.parse_mode = undefined
32
+ }
33
+ }
34
+ static normalise(content: Nestable<string>) {
35
+ if (content instanceof FmtString) return content
36
+ return new FmtString(String(content))
37
+ }
38
+ }
39
+
40
+ const isArray: <T>(xs: T | readonly T[]) => xs is readonly T[] = Array.isArray
41
+
42
+ /** Given a base FmtString and something to append to it, mutates the base */
43
+ const _add = (base: FmtString, next: FmtString | Any) => {
44
+ const len = base.text.length
45
+ if (next instanceof FmtString) {
46
+ base.text = `${base.text}${next.text}`
47
+ // next.entities could be undefined and condition will fail
48
+ for (let i = 0; i < (next.entities?.length || 0); i++) {
49
+ // because of the above condition, next.entities[i] cannot be undefined
50
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
51
+ const entity = next.entities![i]!
52
+ // base.entities is ensured by caller
53
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54
+ base.entities!.push({ ...entity, offset: entity.offset + len })
55
+ }
56
+ } else base.text = `${base.text}${next}`
57
+ }
58
+
59
+ /**
60
+ * Given an `Iterable<FmtString | string | Any>` and a separator, flattens the list into a single FmtString.
61
+ * Analogous to Array#join -> string, but for FmtString
62
+ */
63
+ export const join = (
64
+ fragments: Iterable<FmtString | string | Any>,
65
+ separator?: string | FmtString
66
+ ) => {
67
+ const result = new FmtString('')
68
+ // ensure entities array so loop doesn't need to check
69
+ result.entities = []
70
+
71
+ const iter = fragments[Symbol.iterator]()
72
+
73
+ let curr = iter.next()
74
+ while (!curr.done) {
75
+ _add(result, curr.value)
76
+ curr = iter.next()
77
+ if (separator && !curr.done) _add(result, separator)
78
+ }
79
+
80
+ // set parse_mode: undefined if entities are present
81
+ if (result.entities.length) result.parse_mode = undefined
82
+ // remove entities array if not relevant
83
+ else delete result.entities
84
+
85
+ return result
86
+ }
87
+
88
+ /** Internal constructor for all fmt helpers */
89
+ export function createFmt(kind?: MessageEntity['type'], opts?: object) {
90
+ return function fmt(
91
+ parts: MaybeNestableList<string>,
92
+ ...items: Nestable<string>[]
93
+ ) {
94
+ parts = isArray(parts) ? parts : [parts]
95
+ const result = join(zip(parts, items))
96
+ if (kind) {
97
+ result.entities ??= []
98
+ result.entities.unshift({
99
+ type: kind,
100
+ offset: 0,
101
+ length: result.text.length,
102
+ ...opts,
103
+ } as MessageEntity)
104
+ result.parse_mode = undefined
105
+ }
106
+ return result
107
+ }
108
+ }
109
+
110
+ export const linkOrMention = (
111
+ content: Nestable<string>,
112
+ data:
113
+ | { type: 'text_link'; url: string }
114
+ | { type: 'text_mention'; user: User }
115
+ ) => {
116
+ const { text, entities = [] } = FmtString.normalise(content)
117
+ entities.unshift(Object.assign(data, { offset: 0, length: text.length }))
118
+ return new FmtString(text, entities)
119
+ }
@@ -0,0 +1,86 @@
1
+ import * as Markup from '../../markup'
2
+
3
+ export interface InputBoxFieldOptions {
4
+ placeholder?: string
5
+ validate?: (value: string) => boolean | string
6
+ type?: 'text' | 'number'
7
+ }
8
+
9
+ export interface InputBoxField extends InputBoxFieldOptions {
10
+ key: string
11
+ label: string
12
+ }
13
+
14
+ /**
15
+ * Interactive In-Chat Form Box Card for Telegram Messages.
16
+ * Render boxed form cards with dynamic real-time field updates.
17
+ */
18
+ export class InputBox {
19
+ readonly id: string
20
+ private title = '📋 FORM INPUT'
21
+ private fields: InputBoxField[] = []
22
+ private completionHandler?: (data: Record<string, string>) => any
23
+
24
+ constructor(id: string) {
25
+ this.id = id
26
+ }
27
+
28
+ setTitle(title: string): this {
29
+ this.title = title
30
+ return this
31
+ }
32
+
33
+ addField(key: string, label: string, options: InputBoxFieldOptions = {}): this {
34
+ this.fields.push({ key, label, ...options })
35
+ return this
36
+ }
37
+
38
+ onComplete(handler: (data: Record<string, string>) => any): this {
39
+ this.completionHandler = handler
40
+ return this
41
+ }
42
+
43
+ /**
44
+ * Render formatted HTML box card text.
45
+ */
46
+ renderText(data: Record<string, string> = {}): string {
47
+ const lines: string[] = []
48
+ lines.push(`<b>┌─ ${this.title.toUpperCase()} ─┐</b>`)
49
+
50
+ this.fields.forEach((field, idx) => {
51
+ const val = data[field.key] ? `<b>${data[field.key]}</b>` : `<i>[ ${field.placeholder ?? 'Belum diisi'} ]</i>`
52
+ lines.push(`│ ${idx + 1}. <b>${field.label}</b>: ${val}`)
53
+ })
54
+
55
+ const isComplete = this.fields.every((f) => Boolean(data[f.key]))
56
+ lines.push(`<b>└${'─'.repeat(30)}┘</b>`)
57
+ lines.push(isComplete ? '<b>Status:</b> ✅ <i>Lengkap (Siap Dikirim)</i>' : '<b>Status:</b> ⌛ <i>Menunggu Pengisian...</i>')
58
+
59
+ return lines.join('\n')
60
+ }
61
+
62
+ /**
63
+ * Render inline keyboard with field edit buttons and submit action.
64
+ */
65
+ renderKeyboard(data: Record<string, string> = {}) {
66
+ const buttons: any[] = []
67
+
68
+ // Field buttons
69
+ this.fields.forEach((field) => {
70
+ const filled = Boolean(data[field.key])
71
+ const icon = filled ? '✅' : '✏️'
72
+ buttons.push([Markup.button.callback(`${icon} ${field.label}`, `inputbox:${this.id}:edit:${field.key}`)])
73
+ })
74
+
75
+ // Action row
76
+ const isComplete = this.fields.every((f) => Boolean(data[f.key]))
77
+ if (isComplete) {
78
+ buttons.push([
79
+ Markup.button.callback('🚀 Submit Form', `inputbox:${this.id}:submit`),
80
+ Markup.button.callback('🔄 Reset', `inputbox:${this.id}:reset`),
81
+ ])
82
+ }
83
+
84
+ return Markup.inlineKeyboard(buttons)
85
+ }
86
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Converts Rich HTML strings containing tags like <h1>, <h2>, <p>, <hr>
3
+ * into standard Telegram HTML compatible with parse_mode: 'HTML' (editMessageText/sendMessage).
4
+ */
5
+ export function sanitizeRichHtml(html: string): string {
6
+ if (!html) return html
7
+
8
+ let result = html
9
+
10
+ // Convert H1: <h1>Text</h1> -> <b>TEXT</b>
11
+ result = result.replace(/<h1[^>]*>(.*?)<\/h1>/gis, (_, content) => `<b>${content.trim().toUpperCase()}</b>\n\n`)
12
+
13
+ // Convert H2..H6: <h2>Text</h2> -> <b>Text</b>
14
+ result = result.replace(/<h[2-6][^>]*>(.*?)<\/h[2-6]>/gis, (_, content) => `<b>${content.trim()}</b>\n\n`)
15
+
16
+ // Convert Paragraphs: <p>Text</p> -> Text\n\n
17
+ result = result.replace(/<p[^>]*>(.*?)<\/p>/gis, (_, content) => `${content.trim()}\n\n`)
18
+
19
+ // Convert Footer: <footer>Text</footer> -> <i>Text</i>
20
+ result = result.replace(/<footer[^>]*>(.*?)<\/footer>/gis, (_, content) => `<i>${content.trim()}</i>\n`)
21
+
22
+ // Convert Horizontal Rules: <hr/> or <hr> -> ──────────
23
+ result = result.replace(/<hr\s*\/?>/gi, '──────────\n')
24
+
25
+ // Convert Marks: <mark>Text</mark> -> <b>[Text]</b>
26
+ result = result.replace(/<mark[^>]*>(.*?)<\/mark>/gis, (_, content) => `<b>[${content.trim()}]</b>`)
27
+
28
+ // Strip unsupported sub/sup tags but keep text
29
+ result = result.replace(/<sub[^>]*>(.*?)<\/sub>/gis, '$1')
30
+ result = result.replace(/<sup[^>]*>(.*?)<\/sup>/gis, '$1')
31
+
32
+ // Trim extra trailing newlines (> 2 consecutive newlines)
33
+ result = result.replace(/\n{3,}/g, '\n\n')
34
+
35
+ return result.trim()
36
+ }
@@ -0,0 +1,89 @@
1
+ export interface QueueOptions {
2
+ minDelayMs?: number
3
+ maxRetries?: number
4
+ }
5
+
6
+ interface QueuedItem<T> {
7
+ task: () => Promise<T>
8
+ resolve: (val: T | PromiseLike<T>) => void
9
+ reject: (err: any) => void
10
+ retries: number
11
+ }
12
+
13
+ /**
14
+ * Smart Queue & Rate-Limit Manager for Telegram Bot API calls.
15
+ * Prevents 429 Too Many Requests by enforcing min delay between calls per target key (e.g. chatId).
16
+ */
17
+ export class SmartQueue {
18
+ private static instance: SmartQueue
19
+ private readonly queues = new Map<string | number, QueuedItem<any>[]>()
20
+ private readonly processing = new Set<string | number>()
21
+ private minDelayMs: number
22
+
23
+ constructor(options: QueueOptions = {}) {
24
+ this.minDelayMs = options.minDelayMs ?? 50
25
+ }
26
+
27
+ static getInstance(): SmartQueue {
28
+ if (!SmartQueue.instance) {
29
+ SmartQueue.instance = new SmartQueue()
30
+ }
31
+ return SmartQueue.instance
32
+ }
33
+
34
+ enqueue<T>(key: string | number, task: () => Promise<T>, options?: QueueOptions): Promise<T> {
35
+ return new Promise((resolve, reject) => {
36
+ if (!this.queues.has(key)) {
37
+ this.queues.set(key, [])
38
+ }
39
+
40
+ this.queues.get(key)!.push({
41
+ task,
42
+ resolve,
43
+ reject,
44
+ retries: 0,
45
+ })
46
+
47
+ this.processQueue(key, options)
48
+ })
49
+ }
50
+
51
+ private async processQueue(key: string | number, options?: QueueOptions) {
52
+ if (this.processing.has(key)) return
53
+ this.processing.add(key)
54
+
55
+ const queue = this.queues.get(key)
56
+ const maxRetries = options?.maxRetries ?? 3
57
+ const minDelay = options?.minDelayMs ?? this.minDelayMs
58
+
59
+ while (queue && queue.length > 0) {
60
+ const item = queue[0]
61
+ try {
62
+ const result = await item.task()
63
+ queue.shift()
64
+ item.resolve(result)
65
+ } catch (err: any) {
66
+ // Handle Telegram 429 Too Many Requests (retry_after)
67
+ const retryAfter = err?.parameters?.retry_after ?? err?.response?.parameters?.retry_after
68
+ if (retryAfter && item.retries < maxRetries) {
69
+ item.retries++
70
+ const waitTime = (retryAfter + 0.5) * 1000
71
+ await new Promise((res) => setTimeout(res, waitTime))
72
+ continue
73
+ }
74
+
75
+ queue.shift()
76
+ item.reject(err)
77
+ }
78
+
79
+ if (queue.length > 0) {
80
+ await new Promise((res) => setTimeout(res, minDelay))
81
+ }
82
+ }
83
+
84
+ this.processing.delete(key)
85
+ if (queue && queue.length === 0) {
86
+ this.queues.delete(key)
87
+ }
88
+ }
89
+ }