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.
- package/LICENSE +22 -0
- package/README.md +1126 -0
- package/filters.d.ts +1 -0
- package/filters.js +1 -0
- package/format.d.ts +1 -0
- package/format.js +1 -0
- package/future.d.ts +1 -0
- package/future.js +1 -0
- package/lib/button.js +101 -0
- package/lib/cli.mjs +105 -0
- package/lib/composer.js +581 -0
- package/lib/context.js +1300 -0
- package/lib/core/helpers/ai-stream.js +69 -0
- package/lib/core/helpers/args.js +58 -0
- package/lib/core/helpers/check.js +56 -0
- package/lib/core/helpers/compact.js +17 -0
- package/lib/core/helpers/deunionize.js +13 -0
- package/lib/core/helpers/formatting.js +89 -0
- package/lib/core/helpers/input-box.js +87 -0
- package/lib/core/helpers/rich-sanitizer.js +31 -0
- package/lib/core/helpers/smart-queue.js +70 -0
- package/lib/core/helpers/util.js +50 -0
- package/lib/core/network/client.js +318 -0
- package/lib/core/network/error.js +21 -0
- package/lib/core/network/multipart-stream.js +61 -0
- package/lib/core/network/polling.js +86 -0
- package/lib/core/network/webhook.js +54 -0
- package/lib/core/types/rich-message.js +287 -0
- package/lib/core/types/typegram.js +18 -0
- package/lib/filters.js +69 -0
- package/lib/format.js +38 -0
- package/lib/future.js +166 -0
- package/lib/index.js +61 -0
- package/lib/input.js +61 -0
- package/lib/markup.js +111 -0
- package/lib/middleware.js +2 -0
- package/lib/paginated.js +76 -0
- package/lib/reactions.js +81 -0
- package/lib/router.js +45 -0
- package/lib/scenes/base.js +39 -0
- package/lib/scenes/context.js +99 -0
- package/lib/scenes/form-scene.js +23 -0
- package/lib/scenes/index.js +21 -0
- package/lib/scenes/stage.js +49 -0
- package/lib/scenes/wizard/context.js +30 -0
- package/lib/scenes/wizard/index.js +45 -0
- package/lib/scenes.js +17 -0
- package/lib/session.js +163 -0
- package/lib/telegraf.js +274 -0
- package/lib/telegram-types.js +6 -0
- package/lib/telegram.js +1286 -0
- package/lib/types.js +2 -0
- package/lib/utils.js +5 -0
- package/markup.d.ts +1 -0
- package/markup.js +1 -0
- package/package.json +141 -0
- package/scenes.d.ts +1 -0
- package/scenes.js +1 -0
- package/session.d.ts +1 -0
- package/session.js +1 -0
- package/src/button.ts +182 -0
- package/src/composer.ts +1008 -0
- package/src/context.ts +1778 -0
- package/src/core/helpers/ai-stream.ts +96 -0
- package/src/core/helpers/args.ts +63 -0
- package/src/core/helpers/check.ts +71 -0
- package/src/core/helpers/compact.ts +18 -0
- package/src/core/helpers/deunionize.ts +26 -0
- package/src/core/helpers/formatting.ts +119 -0
- package/src/core/helpers/input-box.ts +86 -0
- package/src/core/helpers/rich-sanitizer.ts +36 -0
- package/src/core/helpers/smart-queue.ts +89 -0
- package/src/core/helpers/util.ts +96 -0
- package/src/core/network/client.ts +396 -0
- package/src/core/network/error.ts +29 -0
- package/src/core/network/multipart-stream.ts +45 -0
- package/src/core/network/polling.ts +94 -0
- package/src/core/network/webhook.ts +58 -0
- package/src/core/types/rich-message.ts +375 -0
- package/src/core/types/typegram.ts +44 -0
- package/src/filters.js +69 -0
- package/src/filters.ts +109 -0
- package/src/format.js +38 -0
- package/src/format.ts +110 -0
- package/src/future.js +147 -0
- package/src/future.ts +231 -0
- package/src/index.ts +27 -0
- package/src/input.ts +59 -0
- package/src/markup.js +93 -0
- package/src/markup.ts +142 -0
- package/src/middleware.ts +24 -0
- package/src/paginated.ts +75 -0
- package/src/reactions.ts +118 -0
- package/src/router.ts +55 -0
- package/src/scenes/base.ts +52 -0
- package/src/scenes/context.ts +136 -0
- package/src/scenes/form-scene.ts +34 -0
- package/src/scenes/index.ts +21 -0
- package/src/scenes/stage.ts +71 -0
- package/src/scenes/wizard/context.ts +58 -0
- package/src/scenes/wizard/index.ts +63 -0
- package/src/scenes.js +17 -0
- package/src/scenes.ts +1 -0
- package/src/session.js +175 -0
- package/src/session.ts +204 -0
- package/src/telegraf.ts +385 -0
- package/src/telegram-types.ts +256 -0
- package/src/telegram.ts +1700 -0
- package/src/types.js +2 -0
- package/src/types.ts +2 -0
- package/src/utils.js +5 -0
- package/src/utils.ts +1 -0
- package/types.d.ts +1 -0
- package/types.js +1 -0
- package/typings/button.d.ts +35 -0
- package/typings/button.d.ts.map +1 -0
- package/typings/composer.d.ts +226 -0
- package/typings/composer.d.ts.map +1 -0
- package/typings/context.d.ts +711 -0
- package/typings/context.d.ts.map +1 -0
- package/typings/core/helpers/ai-stream.d.ts +33 -0
- package/typings/core/helpers/args.d.ts +10 -0
- package/typings/core/helpers/args.d.ts.map +1 -0
- package/typings/core/helpers/check.d.ts +55 -0
- package/typings/core/helpers/check.d.ts.map +1 -0
- package/typings/core/helpers/compact.d.ts +3 -0
- package/typings/core/helpers/compact.d.ts.map +1 -0
- package/typings/core/helpers/deunionize.d.ts +17 -0
- package/typings/core/helpers/deunionize.d.ts.map +1 -0
- package/typings/core/helpers/formatting.d.ts +29 -0
- package/typings/core/helpers/formatting.d.ts.map +1 -0
- package/typings/core/helpers/input-box.d.ts +32 -0
- package/typings/core/helpers/rich-sanitizer.d.ts +5 -0
- package/typings/core/helpers/smart-queue.d.ts +18 -0
- package/typings/core/helpers/util.d.ts +26 -0
- package/typings/core/helpers/util.d.ts.map +1 -0
- package/typings/core/network/client.d.ts +54 -0
- package/typings/core/network/client.d.ts.map +1 -0
- package/typings/core/network/error.d.ts +15 -0
- package/typings/core/network/error.d.ts.map +1 -0
- package/typings/core/network/multipart-stream.d.ts +19 -0
- package/typings/core/network/multipart-stream.d.ts.map +1 -0
- package/typings/core/network/polling.d.ts +15 -0
- package/typings/core/network/polling.d.ts.map +1 -0
- package/typings/core/network/webhook.d.ts +6 -0
- package/typings/core/network/webhook.d.ts.map +1 -0
- package/typings/core/types/rich-message.d.ts +143 -0
- package/typings/core/types/rich-message.d.ts.map +1 -0
- package/typings/core/types/typegram.d.ts +36 -0
- package/typings/core/types/typegram.d.ts.map +1 -0
- package/typings/filters.d.ts +17 -0
- package/typings/filters.d.ts.map +1 -0
- package/typings/format.d.ts +21 -0
- package/typings/format.d.ts.map +1 -0
- package/typings/future.d.ts +11 -0
- package/typings/future.d.ts.map +1 -0
- package/typings/index.d.ts +21 -0
- package/typings/index.d.ts.map +1 -0
- package/typings/input.d.ts +58 -0
- package/typings/input.d.ts.map +1 -0
- package/typings/markup.d.ts +26 -0
- package/typings/markup.d.ts.map +1 -0
- package/typings/middleware.d.ts +7 -0
- package/typings/middleware.d.ts.map +1 -0
- package/typings/paginated.d.ts +20 -0
- package/typings/reactions.d.ts +31 -0
- package/typings/reactions.d.ts.map +1 -0
- package/typings/router.d.ts +20 -0
- package/typings/router.d.ts.map +1 -0
- package/typings/scenes/base.d.ts +21 -0
- package/typings/scenes/base.d.ts.map +1 -0
- package/typings/scenes/context.d.ts +35 -0
- package/typings/scenes/context.d.ts.map +1 -0
- package/typings/scenes/form-scene.d.ts +17 -0
- package/typings/scenes/index.d.ts +10 -0
- package/typings/scenes/index.d.ts.map +1 -0
- package/typings/scenes/stage.d.ts +23 -0
- package/typings/scenes/stage.d.ts.map +1 -0
- package/typings/scenes/wizard/context.d.ts +28 -0
- package/typings/scenes/wizard/context.d.ts.map +1 -0
- package/typings/scenes/wizard/index.d.ts +15 -0
- package/typings/scenes/wizard/index.d.ts.map +1 -0
- package/typings/scenes.d.ts +1 -0
- package/typings/scenes.d.ts.map +1 -0
- package/typings/session.d.ts +54 -0
- package/typings/session.d.ts.map +1 -0
- package/typings/telegraf.d.ts +118 -0
- package/typings/telegraf.d.ts.map +1 -0
- package/typings/telegram-types.d.ts +140 -0
- package/typings/telegram-types.d.ts.map +1 -0
- package/typings/telegram.d.ts +697 -0
- package/typings/telegram.d.ts.map +1 -0
- package/typings/types.d.ts +2 -0
- package/typings/types.d.ts.map +1 -0
- package/typings/utils.d.ts +1 -0
- package/typings/utils.d.ts.map +1 -0
- package/utils.d.ts +1 -0
- 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
|
+
}
|