@volter/twin-telegram 1.0.0 → 1.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.
- package/README.md +2 -2
- package/dist/journeys/customer-life.json +21 -21
- package/dist/journeys/customer-life.outline.md +17 -17
- package/dist/journeys/customer-life.outline.verdict.json +17 -52
- package/dist/journeys/customer-life.verdict.json +24 -98
- package/dist/journeys/decisions.json +49 -13
- package/dist/journeys/demand.json +0 -91
- package/dist/journeys/first-use.json +21 -0
- package/dist/journeys/unreachable.json +1 -1
- package/dist/journeys/vendor-examples.json +14 -6
- package/dist/spec/SOURCE.md +5 -6
- package/dist/spec/patches.json +20 -20
- package/dist/spec/sources.json +15 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/engine/media.d.ts +6 -12
- package/dist/src/engine/media.js +28 -66
- package/dist/src/engine/text.d.ts +0 -7
- package/dist/src/engine/text.js +2 -2
- package/dist/src/index.js +2 -1
- package/dist/src/manifest.js +7 -6
- package/dist/src/semantics/available-methods.js +50 -40
- package/dist/src/semantics/doors.d.ts +0 -1
- package/dist/src/semantics/doors.js +82 -5
- package/dist/src/semantics/shared.d.ts +10 -6
- package/dist/src/semantics/shared.js +29 -8
- package/dist/src/server.js +2 -19
- package/generated/pack-facts.json +10 -0
- package/journeys/customer-life.json +21 -21
- package/journeys/customer-life.outline.md +17 -17
- package/journeys/customer-life.outline.verdict.json +17 -52
- package/journeys/customer-life.verdict.json +24 -98
- package/journeys/decisions.json +49 -13
- package/journeys/demand.json +0 -91
- package/journeys/first-use.json +21 -0
- package/journeys/unreachable.json +1 -1
- package/journeys/vendor-examples.json +14 -6
- package/package.json +12 -9
- package/spec/SOURCE.md +5 -6
- package/spec/patches.json +20 -20
- package/spec/sources.json +15 -0
- package/src/cli.ts +1 -1
- package/src/engine/media.ts +21 -58
- package/src/engine/text.ts +2 -2
- package/src/index.ts +2 -1
- package/src/manifest.ts +7 -6
- package/src/semantics/available-methods.ts +55 -41
- package/src/semantics/doors.ts +74 -8
- package/src/semantics/shared.ts +27 -8
- package/src/server.ts +2 -11
- package/dist/generated/pack-facts.json +0 -53
- package/dist/spec/page-to-openapi.ts +0 -258
- package/spec/page-to-openapi.ts +0 -258
package/spec/patches.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "integer",
|
|
8
8
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
9
9
|
},
|
|
10
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
10
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMessage).",
|
|
11
11
|
"source": {
|
|
12
12
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
13
13
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "integer",
|
|
21
21
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
22
22
|
},
|
|
23
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
23
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMessage).",
|
|
24
24
|
"source": {
|
|
25
25
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
26
26
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"type": "integer",
|
|
34
34
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
35
35
|
},
|
|
36
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
36
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMessage).",
|
|
37
37
|
"source": {
|
|
38
38
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
39
39
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"type": "integer",
|
|
47
47
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
48
48
|
},
|
|
49
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
49
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendPhoto).",
|
|
50
50
|
"source": {
|
|
51
51
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
52
52
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"type": "integer",
|
|
60
60
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
61
61
|
},
|
|
62
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
62
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendPhoto).",
|
|
63
63
|
"source": {
|
|
64
64
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
65
65
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"type": "integer",
|
|
73
73
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
74
74
|
},
|
|
75
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
75
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendPhoto).",
|
|
76
76
|
"source": {
|
|
77
77
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
78
78
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"type": "integer",
|
|
86
86
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
87
87
|
},
|
|
88
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
88
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendVideo).",
|
|
89
89
|
"source": {
|
|
90
90
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
91
91
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"type": "integer",
|
|
99
99
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
100
100
|
},
|
|
101
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
101
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendVideo).",
|
|
102
102
|
"source": {
|
|
103
103
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
104
104
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"type": "integer",
|
|
112
112
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
113
113
|
},
|
|
114
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
114
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendVideo).",
|
|
115
115
|
"source": {
|
|
116
116
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
117
117
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"type": "integer",
|
|
125
125
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
126
126
|
},
|
|
127
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
127
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendDocument).",
|
|
128
128
|
"source": {
|
|
129
129
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
130
130
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"type": "integer",
|
|
138
138
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
139
139
|
},
|
|
140
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
140
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendDocument).",
|
|
141
141
|
"source": {
|
|
142
142
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
143
143
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"type": "integer",
|
|
151
151
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
152
152
|
},
|
|
153
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
153
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendDocument).",
|
|
154
154
|
"source": {
|
|
155
155
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
156
156
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"type": "integer",
|
|
164
164
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
165
165
|
},
|
|
166
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
166
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMediaGroup).",
|
|
167
167
|
"source": {
|
|
168
168
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
169
169
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"type": "integer",
|
|
177
177
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
178
178
|
},
|
|
179
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
179
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMediaGroup).",
|
|
180
180
|
"source": {
|
|
181
181
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
182
182
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"type": "integer",
|
|
190
190
|
"description": "If the message is a reply, ID of the original message (read when reply_parameters is absent)"
|
|
191
191
|
},
|
|
192
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0
|
|
192
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMediaGroup).",
|
|
193
193
|
"source": {
|
|
194
194
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
195
195
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"type": "integer"
|
|
208
208
|
}
|
|
209
209
|
},
|
|
210
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0
|
|
210
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMessage).",
|
|
211
211
|
"source": {
|
|
212
212
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
213
213
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"type": "integer"
|
|
226
226
|
}
|
|
227
227
|
},
|
|
228
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0
|
|
228
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendPhoto).",
|
|
229
229
|
"source": {
|
|
230
230
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
231
231
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
"type": "integer"
|
|
244
244
|
}
|
|
245
245
|
},
|
|
246
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0
|
|
246
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendVideo).",
|
|
247
247
|
"source": {
|
|
248
248
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
249
249
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"type": "integer"
|
|
262
262
|
}
|
|
263
263
|
},
|
|
264
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0
|
|
264
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendDocument).",
|
|
265
265
|
"source": {
|
|
266
266
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
267
267
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
"type": "integer"
|
|
280
280
|
}
|
|
281
281
|
},
|
|
282
|
-
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0
|
|
282
|
+
"why": "The page lists only reply_parameters, but the Bot API server still reads reply_to_message_id (and allow_sending_without_reply) when a request carries no reply_parameters, in the query string as in the body, and node-telegram-bot-api 0.66.0 sends reply_to_message_id (sendMediaGroup).",
|
|
283
283
|
"source": {
|
|
284
284
|
"url": "https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp",
|
|
285
285
|
"quote": "result.reply_to_message_id = get_message_id(query, \"reply_to_message_id\");"
|
package/spec/sources.json
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
+
"https://core.telegram.org/api/files": {
|
|
3
|
+
"status": 200,
|
|
4
|
+
"checked": "2026-10-15",
|
|
5
|
+
"quotes": {
|
|
6
|
+
"s - Server-side resized image, bounded by 100x100 pixels.": true,
|
|
7
|
+
"w - Server-side resized image, bounded by 2560x2560 pixels.": true
|
|
8
|
+
}
|
|
9
|
+
},
|
|
2
10
|
"https://core.telegram.org/bots/api#accent-colors": {
|
|
3
11
|
"status": 200,
|
|
4
12
|
"checked": "2026-10-02",
|
|
@@ -355,6 +363,13 @@
|
|
|
355
363
|
"Wrong file identifier/HTTP URL specified": true
|
|
356
364
|
}
|
|
357
365
|
},
|
|
366
|
+
"https://raw.githubusercontent.com/tdlib/td/42e6a5259551178d1dab54a22ad96d14bd906e20/td/telegram/PhotoSize.cpp": {
|
|
367
|
+
"status": 200,
|
|
368
|
+
"checked": "2026-10-15",
|
|
369
|
+
"quotes": {
|
|
370
|
+
"get_photo_sizes_object": true
|
|
371
|
+
}
|
|
372
|
+
},
|
|
358
373
|
"https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp": {
|
|
359
374
|
"status": 200,
|
|
360
375
|
"checked": "2026-10-02",
|
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ const [cmd, ...rest] = process.argv.slice(2);
|
|
|
8
8
|
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
9
9
|
const root = optionValue(rest, '--root') || undefined;
|
|
10
10
|
const readOnly = hasFlag(rest, '--read-only');
|
|
11
|
-
// the
|
|
11
|
+
// the selected twin's private Postgres, which the runtime binds a pack declaring managedDatabase to
|
|
12
12
|
const database = optionValue(rest, '--database') || undefined;
|
|
13
13
|
// the World's scenario file, for a model vendor's turns
|
|
14
14
|
const scenarioPath = optionValue(rest, '--scenario') || undefined;
|
package/src/engine/media.ts
CHANGED
|
@@ -1,67 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// Telegram's format acceptance and presentation policy over the shared file-header facts.
|
|
2
|
+
import { readImageHeader, readMp4Header } from '@volter/world-core';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** An image's size from its header: PNG (IHDR), GIF (logical screen), JPEG (the first start-of-frame); undefined for
|
|
13
|
-
* anything else (a photo Telegram takes is a .jpg, .jpeg, .gif, .png, .tif or .bmp, MessagesManager.cpp's
|
|
14
|
-
* PHOTO_EXT_INVALID; the twin reads the first three). */
|
|
15
|
-
export function imageSize(b: Uint8Array): ImageSize | undefined {
|
|
16
|
-
if (b.length >= 24 && b[0] === 0x89 && ascii(b, 1, 3) === 'PNG' && ascii(b, 12, 4) === 'IHDR') return { width: u32be(b, 16), height: u32be(b, 20), mime: 'image/png' };
|
|
17
|
-
if (b.length >= 10 && ascii(b, 0, 3) === 'GIF') return { width: u16le(b, 6), height: u16le(b, 8), mime: 'image/gif' };
|
|
18
|
-
// a JPEG's markers are walked to its first frame header; a JPEG with none, like any other file, has no size
|
|
19
|
-
const jpeg = b.length >= 4 && b[0] === 0xff && b[1] === 0xd8;
|
|
20
|
-
for (let i = 2; jpeg && i + 9 < b.length; i += b[i] !== 0xff ? 1 : jpegStep(b, i)) {
|
|
21
|
-
const marker = b[i + 1]!;
|
|
22
|
-
// SOF0-SOF15 but DHT (C4), JPG (C8) and DAC (CC) carry the frame's height and width
|
|
23
|
-
if (b[i] === 0xff && marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) return { height: u16be(b, i + 5), width: u16be(b, i + 7), mime: 'image/jpeg' };
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
4
|
+
/** Telegram reads PNG's IHDR, GIF's logical screen and JPEG's first frame; other formats have no photo size. */
|
|
5
|
+
export function imageSize(bytes: Uint8Array): { width: number; height: number } | undefined {
|
|
6
|
+
const image = readImageHeader(bytes);
|
|
7
|
+
if (!image || image.type === 'image/webp') return undefined;
|
|
8
|
+
if (image.type === 'image/png' && String.fromCharCode(...bytes.subarray(12, 16)) !== 'IHDR') return undefined;
|
|
9
|
+
if (image.type === 'image/jpeg' && (!image.jpeg || image.jpeg.frameEnd >= bytes.length)) return undefined;
|
|
10
|
+
return { width: image.width, height: image.height };
|
|
26
11
|
}
|
|
27
12
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** An MP4's (ISO base media) duration in whole seconds from its movie header (mvhd), and the first track with a size
|
|
36
|
-
* from its track header (tkhd: 16.16 fixed-point width and height); undefined for a file that is not one. */
|
|
37
|
-
export function mp4Info(b: Uint8Array): VideoInfo | undefined {
|
|
38
|
-
if (b.length < 12 || ascii(b, 4, 4) !== 'ftyp') return undefined;
|
|
13
|
+
/** Whole-second duration from the last usable movie header, and the first track whose floored width is nonzero. */
|
|
14
|
+
export function mp4Info(bytes: Uint8Array): { width: number; height: number; duration: number } | undefined {
|
|
15
|
+
if (bytes.length < 12) return undefined;
|
|
16
|
+
const file = readMp4Header(bytes);
|
|
17
|
+
if (!file.fileType) return undefined;
|
|
39
18
|
let duration: number | undefined;
|
|
40
19
|
let width = 0;
|
|
41
20
|
let height = 0;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (size === 1 && i + 16 <= end) { size = u32be(b, i + 8) * 2 ** 32 + u32be(b, i + 12); header = 16; }
|
|
49
|
-
if (size === 0) size = end - i;
|
|
50
|
-
if (size < header || i + size > end) return;
|
|
51
|
-
if (type === 'moov' || type === 'trak') walk(i + header, i + size);
|
|
52
|
-
else if (type === 'mvhd') {
|
|
53
|
-
const v = b[i + header]!;
|
|
54
|
-
const timescale = v === 1 ? u32be(b, i + header + 20) : u32be(b, i + header + 12);
|
|
55
|
-
const units = v === 1 ? u32be(b, i + header + 24) * 2 ** 32 + u32be(b, i + header + 28) : u32be(b, i + header + 16);
|
|
56
|
-
if (timescale) duration = Math.round(units / timescale);
|
|
57
|
-
} else if (type === 'tkhd' && !width) {
|
|
58
|
-
const v = b[i + header]!;
|
|
59
|
-
const at = i + header + (v === 1 ? 88 : 76);
|
|
60
|
-
if (at + 8 <= i + size) { width = u32be(b, at) >>> 16; height = u32be(b, at + 4) >>> 16; }
|
|
61
|
-
}
|
|
62
|
-
i += size;
|
|
21
|
+
for (const movie of file.movies) {
|
|
22
|
+
for (const header of movie.headers) {
|
|
23
|
+
if (header.timescale) duration = Math.round(header.duration / header.timescale);
|
|
24
|
+
}
|
|
25
|
+
for (const track of movie.tracks) {
|
|
26
|
+
if (!width) { width = Math.floor(track.width); height = Math.floor(track.height); }
|
|
63
27
|
}
|
|
64
|
-
}
|
|
65
|
-
walk(0, b.length);
|
|
28
|
+
}
|
|
66
29
|
return duration === undefined ? undefined : { width, height, duration };
|
|
67
30
|
}
|
package/src/engine/text.ts
CHANGED
|
@@ -351,7 +351,7 @@ export function parseMarkdown(input: string): Formatted | { error: string } {
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
/** Entities in TDLib's order: by offset, the longer first. */
|
|
354
|
-
|
|
354
|
+
function sortEntities(entities: Entity[]): Entity[] {
|
|
355
355
|
return [...entities].sort((a, b) => a.offset - b.offset || b.length - a.length);
|
|
356
356
|
}
|
|
357
357
|
|
|
@@ -362,7 +362,7 @@ const isWord = (ch: string | undefined): boolean => ch !== undefined && WORD.tes
|
|
|
362
362
|
* inside a word), bot commands (/command, 1-64, optionally @bot), hashtags (#tag with a letter), cashtags ($ABC),
|
|
363
363
|
* links (http, https, or www.) and e-mail addresses. Where the twin stops: TDLib's own link matcher also takes bare
|
|
364
364
|
* domains by their top-level domain, and finds bank card numbers; the twin does not. */
|
|
365
|
-
|
|
365
|
+
function findEntities(text: string, skipBotCommands: boolean): Entity[] {
|
|
366
366
|
const found: Entity[] = [];
|
|
367
367
|
const scan = (re: RegExp, type: string, ok: (m: RegExpExecArray) => boolean = () => true): void => {
|
|
368
368
|
for (const m of text.matchAll(re)) {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @volter/twin-telegram — the telegram twin (Protocol 3). A fixed file (create-pack): the descriptor is the manifest's, and the
|
|
2
|
-
// kernel derives the vendor-backed half from the generated surface
|
|
2
|
+
// kernel derives the vendor-backed half from the generated surface, unless the manifest declares none (vendorBacked.none;
|
|
3
|
+
// architecture, "The real-system adapters").
|
|
3
4
|
import { packOf, registerPack, type DerivedSurface } from '@volter/world-core';
|
|
4
5
|
import { manifest } from './manifest.ts';
|
|
5
6
|
import surface from './generated/surface.gen.json' with { type: 'json' };
|
package/src/manifest.ts
CHANGED
|
@@ -46,11 +46,12 @@ export const manifest: DerivedManifest = {
|
|
|
46
46
|
// a Telegram account, a person's or a bot's, stored as the Bot API answers a User; its id is an integer the vendor
|
|
47
47
|
// assigns, kept as the subject's id and as its own `id`
|
|
48
48
|
// source: https://core.telegram.org/bots/api#user "Unique identifier for this user or bot."
|
|
49
|
-
user: { idPrefix: '58104400', ids: '{prefix}{n}', refresh: { none: 'the Bot API reads no user by id and lists none: getMe answers the bot itself, and a person is seen only in the messages and updates that name them' } },
|
|
49
|
+
user: { idPrefix: '58104400', ids: '{prefix}{n}', unique: [{ fields: ['_username'], refusal: { status: 409, message: 'This username is taken.' } }], refresh: { none: 'the Bot API reads no user by id and lists none: getMe answers the bot itself, and a person is seen only in the messages and updates that name them' } },
|
|
50
50
|
// a chat as getChat answers it (ChatFullInfo); a private chat's id is its person's, a group's and a channel's are
|
|
51
51
|
// the vendor's negative ids (minted by the doors that make them)
|
|
52
52
|
// source: https://core.telegram.org/bots/api#chat "Unique identifier for this chat."
|
|
53
|
-
|
|
53
|
+
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Bad Request: chat not found"
|
|
54
|
+
chat: { idPrefix: '', ...states.chat, refresh: { get: 'getChat', known: true, query: { chat_id: '{id}' }, gone: { status: 400, says: 'chat not found' } } },
|
|
54
55
|
// one subject per member of a chat, `<chat>:<user>`, stored as getChatMember answers it
|
|
55
56
|
chat_member: { idPrefix: '', ...states.chat_member, refresh: { none: 'the Bot API lists only administrators (getChatAdministrators), and getChatMember reads one member by a user id the World learns only from updates' } },
|
|
56
57
|
// a message, `<chat>:<message_id>`; its message_id counts per chat
|
|
@@ -63,7 +64,7 @@ export const manifest: DerivedManifest = {
|
|
|
63
64
|
// source: https://core.telegram.org/bots/api#file "Unique identifier for this file, which is supposed to be the same over time and for different bots."
|
|
64
65
|
file: { idPrefix: 'AgAD', ids: '{prefix}{letters:12}', refresh: { none: 'getFile reads a file by a file_id the World learns only from messages, and its answer is the download path, not the file' } },
|
|
65
66
|
// a bot's own keeping: its token's SHA-256 and its owner, its updates' counter and the allowed_updates it last asked for
|
|
66
|
-
_bot: { idPrefix: '' },
|
|
67
|
+
_bot: { idPrefix: '', unique: [{ fields: ['id'], refusal: { status: 409, message: 'This bot already has credentials.' } }] },
|
|
67
68
|
// the ids the vendor assigns to a basic group and to a channel or supergroup, and a file's download path
|
|
68
69
|
_group: { idPrefix: '-49870000', ids: '{prefix}{n}' },
|
|
69
70
|
_channel: { idPrefix: '-10019876540', ids: '{prefix}{n}' },
|
|
@@ -98,15 +99,15 @@ export const manifest: DerivedManifest = {
|
|
|
98
99
|
bin: 'world-telegram',
|
|
99
100
|
resources: ['user', 'chat', 'chat_member', 'message', 'update', 'file'],
|
|
100
101
|
specSource: 'the Telegram Bot API page (https://core.telegram.org/bots/api, Bot API 10.3), written as OpenAPI in spec/ (provenance in spec/SOURCE.md)',
|
|
101
|
-
description: "Telegram Bot API twin — a bot's updates by long polling, its messages (text, HTML, MarkdownV2, photos, videos, documents, albums), chats, members and files, as Volter Harness's Chat SDK channel
|
|
102
|
+
description: "Telegram Bot API twin — a bot's updates by long polling, its messages (text, HTML, MarkdownV2, photos, videos, documents, albums), chats, members and files, as Volter Harness's Chat SDK channel calls them.",
|
|
102
103
|
adoption: { sdks: ['@chat-adapter/telegram', 'node-telegram-bot-api', 'telegraf'], envStems: ['TELEGRAM'] },
|
|
103
104
|
// the libraries above name Telegram in an application; none is Telegram's own
|
|
104
105
|
// source: https://core.telegram.org/bots/samples "these projects are not maintained by Telegram"
|
|
105
|
-
clientCasesNone: 'Telegram publishes no client library of its own (its samples page lists libraries others maintain): the demand applications call the Bot API through the Chat SDK adapter (Volter Harness)
|
|
106
|
+
clientCasesNone: 'Telegram publishes no client library of its own (its samples page lists libraries others maintain): the demand applications call the Bot API through the Chat SDK adapter (Volter Harness)',
|
|
106
107
|
hosts: [{ host: 'api.telegram.org' }],
|
|
107
108
|
// the Chat SDK's adapter reads TELEGRAM_API_BASE_URL as its API's base URL (@chat-adapter/telegram 4.39.0, dist/index.js:1039)
|
|
108
109
|
endpointEnv: { name: 'TELEGRAM_API_BASE_URL', note: "the Chat SDK's Telegram adapter reads it as its API base (@chat-adapter/telegram 4.39.0, dist/index.js:1039); node-telegram-bot-api takes a baseApiUrl option only, and reaches the twin by its host" },
|
|
109
|
-
credentialDoor: { path: '/_twin/bots', body: {}, fill: { TELEGRAM_BOT_TOKEN: 'token', TELEGRAM_TOKEN: 'token', TELEGRAM_BOT_USERNAME: 'username', TELEGRAM_BOT_NAME: 'username' } },
|
|
110
|
+
credentialDoor: { account: { field: 'account', tenancy: 'Telegram person', source: 'https://core.telegram.org/bots/features#botfather' }, deployment: { field: 'deployment', object: 'Bot', source: 'https://core.telegram.org/bots/features#botfather' }, path: '/_twin/bots', body: {}, fill: { TELEGRAM_BOT_TOKEN: 'token', TELEGRAM_TOKEN: 'token', TELEGRAM_BOT_USERNAME: 'username', TELEGRAM_BOT_NAME: 'username' } },
|
|
110
111
|
// the bot token rides in the path, after /bot (the API's) or /file/bot (a file's download)
|
|
111
112
|
// source: https://core.telegram.org/bots/api#making-requests "https://api.telegram.org/bot<token>/METHOD_NAME"
|
|
112
113
|
auth: { in: 'path', pattern: '/(?:file/)?bot([^/]+)/' },
|
|
@@ -4,7 +4,7 @@ import type { HandlerContext } from '@volter/world-core';
|
|
|
4
4
|
import { imageSize, mp4Info } from '../engine/media.ts';
|
|
5
5
|
import {
|
|
6
6
|
answer, botOf, chatFor, chatFullView, fileBytes, fileById, formatted, jsonParam, memberKey, memberView, messageView, param,
|
|
7
|
-
refuse, replyTarget, storeBotMessage, storeFile, textFields, uploadOf, type Row, type Upload,
|
|
7
|
+
refuse, replyTarget, storeBotMessage, storeFile, photoThumbnails, textFields, uploadOf, type Row, type Upload,
|
|
8
8
|
} from './shared.ts';
|
|
9
9
|
|
|
10
10
|
// source: https://core.telegram.org/bots/api#sendmessage "Text of the message to be sent, 1-4096 characters after entities parsing"
|
|
@@ -50,11 +50,14 @@ function captionOf(ctx: HandlerContext, caption: unknown, parseMode: unknown, en
|
|
|
50
50
|
return textFields(text, 'caption');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
type MediaKind = 'photo' | 'video' | 'document';
|
|
54
|
+
type PreparedMedia = { upload: Upload; bytes: Uint8Array } & (
|
|
55
|
+
{ kind: 'photo'; size: { width: number; height: number } } |
|
|
56
|
+
{ kind: 'video' | 'document'; fields: Row }
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/** Read and validate a medium without creating files, thumbnails or messages. */
|
|
60
|
+
async function prepareMedia(ctx: HandlerContext, kind: MediaKind, upload: Upload, given: { width?: unknown; height?: unknown; duration?: unknown }): Promise<PreparedMedia | { refusal: Response }> {
|
|
58
61
|
let bytes: Uint8Array;
|
|
59
62
|
if ('file' in upload) {
|
|
60
63
|
// a file sent again keeps its type: a photo is not sent as a document, nor a video as a photo
|
|
@@ -64,52 +67,59 @@ async function mediaFields(ctx: HandlerContext, botId: number, kind: 'photo' | '
|
|
|
64
67
|
const real = types[String(upload.file._kind)] ?? 'Document';
|
|
65
68
|
const wanted = kind === 'photo' ? 'Photo' : kind === 'video' ? 'Video' : 'Document';
|
|
66
69
|
if (real !== wanted) return { refusal: refuse(ctx, 400, `Bad Request: can't use file of type ${real} as ${wanted}`) };
|
|
67
|
-
|
|
68
|
-
bytes = await fileBytes(ctx, file);
|
|
70
|
+
bytes = await fileBytes(ctx, upload.file);
|
|
69
71
|
}
|
|
70
|
-
else
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
else bytes = upload.bytes;
|
|
73
|
+
// source: https://core.telegram.org/bots/api#sendphoto "The photo must be at most 10 MB in size."
|
|
74
|
+
// source: https://core.telegram.org/bots/api#sendvideo "Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future."
|
|
75
|
+
// source: https://core.telegram.org/bots/api#senddocument "Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future."
|
|
76
|
+
// Where the documentation stops: the words for a file over its limit are the server's for a file too big
|
|
77
|
+
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Bad Request: file is too big"
|
|
78
|
+
if (!('file' in upload) && bytes.byteLength > (kind === 'photo' ? 10 : 50) * 1024 * 1024) return { refusal: refuse(ctx, 400, 'Bad Request: file is too big') };
|
|
79
|
+
const size = kind === 'photo' ? imageSize(bytes) : undefined;
|
|
80
|
+
if (!('file' in upload)) {
|
|
81
|
+
if (kind === 'photo' && !size) {
|
|
79
82
|
// an upload that is no image Telegram can read: Telegram's own error for it, which the server relays as it is (its
|
|
80
83
|
// second character an underscore or a capital)
|
|
81
84
|
// source: https://core.telegram.org/method/messages.sendMedia "Failure while processing image."
|
|
82
85
|
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "(error_message[1] == '_' || ('A' <= error_message[1] && error_message[1] <= 'Z'))) {"
|
|
83
86
|
return { refusal: refuse(ctx, 400, 'Bad Request: IMAGE_PROCESS_FAILED') };
|
|
84
87
|
}
|
|
85
|
-
const size = kind === 'photo' ? imageSize(bytes)! : undefined;
|
|
86
88
|
// source: https://core.telegram.org/bots/api#sendphoto "The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20."
|
|
87
89
|
// (Telegram's own error for it, relayed as it is: an MTProto error's second character is a capital)
|
|
88
90
|
if (size && (size.width + size.height > 10000 || Math.max(size.width, size.height) > 20 * Math.max(1, Math.min(size.width, size.height)))) return { refusal: refuse(ctx, 400, 'Bad Request: PHOTO_INVALID_DIMENSIONS') };
|
|
89
|
-
file = await storeFile(ctx, bytes, { kind: kind === 'photo' ? 'photos' : kind === 'video' ? 'videos' : 'documents', bot: botId, ...(upload.mime ? { mime: upload.mime } : {}), ...(upload.name ? { name: upload.name } : {}) }, operation);
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// the twin keeps the one it was given
|
|
96
|
-
// source: https://core.telegram.org/bots/api#message "Optional. Message is a photo, available sizes of the photo"
|
|
97
|
-
return { photo: [{ ...ids, file_size: bytes.byteLength, width: size.width, height: size.height }] };
|
|
98
|
-
}
|
|
99
|
-
const name = typeof file._name === 'string' ? file._name : undefined;
|
|
100
|
-
const mime = typeof file._mime === 'string' ? file._mime : undefined;
|
|
92
|
+
if (kind === 'photo') return { kind, upload, bytes, size: size ?? { width: 0, height: 0 } };
|
|
93
|
+
const name = 'file' in upload ? upload.file._name : upload.name;
|
|
94
|
+
const mime = 'file' in upload ? upload.file._mime : upload.mime;
|
|
95
|
+
const fields = { ...(typeof name === 'string' && name ? { file_name: name } : {}), ...(typeof mime === 'string' && mime ? { mime_type: mime } : {}), file_size: bytes.byteLength };
|
|
101
96
|
if (kind === 'video') {
|
|
102
97
|
const info = mp4Info(bytes);
|
|
103
98
|
// the request's own width, height and duration, else the file's
|
|
104
99
|
// source: https://core.telegram.org/bots/api#sendvideo "Duration of sent video in seconds"
|
|
105
100
|
const num = (v: unknown, fallback: number): number => (typeof v === 'number' ? v : typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : fallback);
|
|
106
|
-
return {
|
|
101
|
+
return { kind, upload, bytes, fields: { width: num(given.width, info?.width ?? 0), height: num(given.height, info?.height ?? 0), duration: num(given.duration, info?.duration ?? 0), ...fields } };
|
|
107
102
|
}
|
|
108
|
-
return {
|
|
103
|
+
return { kind, upload, bytes, fields };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Persist a validated medium and assemble its message fields. */
|
|
107
|
+
async function mediaFields(ctx: HandlerContext, botId: number, media: PreparedMedia): Promise<Row> {
|
|
108
|
+
const operation = ctx.call.operation.id;
|
|
109
|
+
const upload = media.upload;
|
|
110
|
+
const file = 'file' in upload ? upload.file : await storeFile(ctx, media.bytes, { kind: media.kind === 'photo' ? 'photos' : media.kind === 'video' ? 'videos' : 'documents', bot: botId, ...(upload.mime ? { mime: upload.mime } : {}), ...(upload.name ? { name: upload.name } : {}) }, operation);
|
|
111
|
+
const ids = { file_id: file.file_id, file_unique_id: file.file_unique_id };
|
|
112
|
+
if (media.kind === 'photo') {
|
|
113
|
+
// its smaller sizes first, then the photo as it was given
|
|
114
|
+
// source: https://core.telegram.org/bots/api#message "Optional. Message is a photo, available sizes of the photo"
|
|
115
|
+
const thumbnails = await photoThumbnails(ctx, file, media.size.width, media.size.height, operation);
|
|
116
|
+
return { photo: [...thumbnails, { ...ids, file_size: media.bytes.byteLength, ...media.size }] };
|
|
117
|
+
}
|
|
118
|
+
return { [media.kind]: { ...ids, ...media.fields } };
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/** One medium sent to a chat with its caption. */
|
|
112
|
-
async function sendMedium(ctx: HandlerContext, kind:
|
|
122
|
+
async function sendMedium(ctx: HandlerContext, kind: MediaKind): Promise<Response> {
|
|
113
123
|
const { id: botId } = botOf(ctx);
|
|
114
124
|
const access = chatFor(ctx, botId, param(ctx, 'chat_id'), 'write');
|
|
115
125
|
if ('refusal' in access) return access.refusal;
|
|
@@ -119,9 +129,10 @@ async function sendMedium(ctx: HandlerContext, kind: 'photo' | 'video' | 'docume
|
|
|
119
129
|
if ('refusal' in caption) return caption.refusal as Response;
|
|
120
130
|
const reply = replyTarget(ctx, access.chat, botId);
|
|
121
131
|
if ('refusal' in reply) return reply.refusal;
|
|
122
|
-
const media = await
|
|
132
|
+
const media = await prepareMedia(ctx, kind, upload, { width: param(ctx, 'width'), height: param(ctx, 'height'), duration: param(ctx, 'duration') });
|
|
123
133
|
if ('refusal' in media) return media.refusal as Response;
|
|
124
|
-
const
|
|
134
|
+
const fields = await mediaFields(ctx, botId, media);
|
|
135
|
+
const row = await storeBotMessage(ctx, botId, access.chat, { ...fields, ...caption }, reply.key, ctx.call.operation.id);
|
|
125
136
|
return answer(messageView(ctx, row));
|
|
126
137
|
}
|
|
127
138
|
|
|
@@ -164,21 +175,22 @@ export async function sendMediaGroup(ctx: HandlerContext): Promise<Response> {
|
|
|
164
175
|
const reply = replyTarget(ctx, access.chat, botId);
|
|
165
176
|
if ('refusal' in reply) return reply.refusal;
|
|
166
177
|
// every part checked before any is sent
|
|
167
|
-
const prepared: Array<{
|
|
178
|
+
const prepared: Array<{ media: PreparedMedia; caption: Row }> = [];
|
|
168
179
|
for (const item of items) {
|
|
169
180
|
const upload = uploadOf(ctx, item.media, botId);
|
|
170
181
|
if ('refusal' in upload) return upload.refusal;
|
|
171
182
|
const caption = captionOf(ctx, item.caption, item.parse_mode, item.caption_entities);
|
|
172
183
|
if ('refusal' in caption) return caption.refusal as Response;
|
|
173
|
-
|
|
184
|
+
const medium = await prepareMedia(ctx, item.type as MediaKind, upload, { width: item.width, height: item.height, duration: item.duration });
|
|
185
|
+
if ('refusal' in medium) return medium.refusal;
|
|
186
|
+
prepared.push({ media: medium, caption });
|
|
174
187
|
}
|
|
175
188
|
// the album's one media_group_id, the World's for it
|
|
176
189
|
// source: https://core.telegram.org/bots/api#message "Optional. The unique identifier inside this chat of a media message group this message belongs to"
|
|
177
190
|
const group = await ctx.issue('_media_group');
|
|
178
191
|
const sent: Row[] = [];
|
|
179
192
|
for (const p of prepared) {
|
|
180
|
-
const fields = await mediaFields(ctx, botId, p.
|
|
181
|
-
if ('refusal' in fields) return fields.refusal as Response;
|
|
193
|
+
const fields = await mediaFields(ctx, botId, p.media);
|
|
182
194
|
const row = await storeBotMessage(ctx, botId, access.chat, { media_group_id: group, ...fields, ...p.caption }, reply.key, ctx.call.operation.id);
|
|
183
195
|
sent.push(messageView(ctx, row));
|
|
184
196
|
}
|
|
@@ -205,7 +217,7 @@ export async function getChat(ctx: HandlerContext): Promise<Response> {
|
|
|
205
217
|
const { id: botId } = botOf(ctx);
|
|
206
218
|
const access = chatFor(ctx, botId, param(ctx, 'chat_id'), 'read');
|
|
207
219
|
if ('refusal' in access) return access.refusal;
|
|
208
|
-
return answer(chatFullView(
|
|
220
|
+
return answer(chatFullView(access.chat));
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
// source: https://core.telegram.org/bots/api#getchatmember "Use this method to get information about a member of a chat."
|
|
@@ -227,6 +239,8 @@ export async function getFile(ctx: HandlerContext): Promise<Response> {
|
|
|
227
239
|
const file = fileById(ctx, param(ctx, 'file_id'), botId);
|
|
228
240
|
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "invalid file_id"
|
|
229
241
|
if (!file) return refuse(ctx, 400, 'Bad Request: invalid file_id');
|
|
242
|
+
// a thumbnail's pixels are not rendered by the twin
|
|
243
|
+
if (file._thumbnail_of !== undefined) return ctx.gap();
|
|
230
244
|
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Bad Request: file is too big"
|
|
231
245
|
if (Number(file.file_size ?? 0) > 20 * 1024 * 1024) return refuse(ctx, 400, 'Bad Request: file is too big');
|
|
232
246
|
let path = typeof file.file_path === 'string' ? file.file_path : undefined;
|
|
@@ -243,9 +257,9 @@ export async function getFile(ctx: HandlerContext): Promise<Response> {
|
|
|
243
257
|
|
|
244
258
|
// source: https://core.telegram.org/bots/api#getfile "The file can then be downloaded via the link https://api.telegram.org/file/bot<token>/<file_path> , where <file_path> is taken from the response."
|
|
245
259
|
export async function downloadFile(ctx: HandlerContext): Promise<Response> {
|
|
246
|
-
botOf(ctx);
|
|
260
|
+
const { id: botId } = botOf(ctx);
|
|
247
261
|
const path = String(ctx.call.params.file_path ?? '');
|
|
248
|
-
const file = ctx.rowsRaw('file').
|
|
262
|
+
const file = ctx.rowsRaw('file').filter((r) => r.file_path === path).map((r) => fileById(ctx, r.file_id, botId)).find((r) => r?.file_path === path);
|
|
249
263
|
// source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/HttpConnection.cpp "return send_http_error(404,"
|
|
250
264
|
if (!file) return refuse(ctx, 404, 'Not Found');
|
|
251
265
|
const bytes = await fileBytes(ctx, file);
|