@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.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/dist/journeys/customer-life.json +21 -21
  3. package/dist/journeys/customer-life.outline.md +17 -17
  4. package/dist/journeys/customer-life.outline.verdict.json +17 -52
  5. package/dist/journeys/customer-life.verdict.json +24 -98
  6. package/dist/journeys/decisions.json +49 -13
  7. package/dist/journeys/demand.json +0 -91
  8. package/dist/journeys/first-use.json +21 -0
  9. package/dist/journeys/unreachable.json +1 -1
  10. package/dist/journeys/vendor-examples.json +14 -6
  11. package/dist/spec/SOURCE.md +5 -6
  12. package/dist/spec/patches.json +20 -20
  13. package/dist/spec/sources.json +15 -0
  14. package/dist/src/cli.js +1 -1
  15. package/dist/src/engine/media.d.ts +6 -12
  16. package/dist/src/engine/media.js +28 -66
  17. package/dist/src/engine/text.d.ts +0 -7
  18. package/dist/src/engine/text.js +2 -2
  19. package/dist/src/index.js +2 -1
  20. package/dist/src/manifest.js +7 -6
  21. package/dist/src/semantics/available-methods.js +50 -40
  22. package/dist/src/semantics/doors.d.ts +0 -1
  23. package/dist/src/semantics/doors.js +82 -5
  24. package/dist/src/semantics/shared.d.ts +10 -6
  25. package/dist/src/semantics/shared.js +29 -8
  26. package/dist/src/server.js +2 -19
  27. package/generated/pack-facts.json +10 -0
  28. package/journeys/customer-life.json +21 -21
  29. package/journeys/customer-life.outline.md +17 -17
  30. package/journeys/customer-life.outline.verdict.json +17 -52
  31. package/journeys/customer-life.verdict.json +24 -98
  32. package/journeys/decisions.json +49 -13
  33. package/journeys/demand.json +0 -91
  34. package/journeys/first-use.json +21 -0
  35. package/journeys/unreachable.json +1 -1
  36. package/journeys/vendor-examples.json +14 -6
  37. package/package.json +12 -9
  38. package/spec/SOURCE.md +5 -6
  39. package/spec/patches.json +20 -20
  40. package/spec/sources.json +15 -0
  41. package/src/cli.ts +1 -1
  42. package/src/engine/media.ts +21 -58
  43. package/src/engine/text.ts +2 -2
  44. package/src/index.ts +2 -1
  45. package/src/manifest.ts +7 -6
  46. package/src/semantics/available-methods.ts +55 -41
  47. package/src/semantics/doors.ts +74 -8
  48. package/src/semantics/shared.ts +27 -8
  49. package/src/server.ts +2 -11
  50. package/dist/generated/pack-facts.json +0 -53
  51. package/dist/spec/page-to-openapi.ts +0 -258
  52. 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 as Postiz calls it sends reply_to_message_id (sendMessage).",
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 as Postiz calls it sends reply_to_message_id (sendMessage).",
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 as Postiz calls it sends reply_to_message_id (sendMessage).",
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 as Postiz calls it sends reply_to_message_id (sendPhoto).",
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 as Postiz calls it sends reply_to_message_id (sendPhoto).",
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 as Postiz calls it sends reply_to_message_id (sendPhoto).",
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 as Postiz calls it sends reply_to_message_id (sendVideo).",
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 as Postiz calls it sends reply_to_message_id (sendVideo).",
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 as Postiz calls it sends reply_to_message_id (sendVideo).",
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 as Postiz calls it sends reply_to_message_id (sendDocument).",
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 as Postiz calls it sends reply_to_message_id (sendDocument).",
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 as Postiz calls it sends reply_to_message_id (sendDocument).",
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 as Postiz calls it sends reply_to_message_id (sendMediaGroup).",
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 as Postiz calls it sends reply_to_message_id (sendMediaGroup).",
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 as Postiz calls it sends reply_to_message_id (sendMediaGroup).",
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 as Postiz calls it sends reply_to_message_id (sendMessage).",
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 as Postiz calls it sends reply_to_message_id (sendPhoto).",
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 as Postiz calls it sends reply_to_message_id (sendVideo).",
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 as Postiz calls it sends reply_to_message_id (sendDocument).",
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 as Postiz calls it sends reply_to_message_id (sendMediaGroup).",
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 World's managed Postgres, which the runtime binds a pack declaring managedDatabase to
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;
@@ -1,67 +1,30 @@
1
- // What Telegram reads from an uploaded file's own bytes: an image's width and height (a PhotoSize), and a video's width,
2
- // height and duration (a Video), each from the format's header as that format defines it. Pure computation over bytes.
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
- export type ImageSize = { width: number; height: number; mime: string };
5
- export type VideoInfo = { width: number; height: number; duration: number };
6
-
7
- const u16be = (b: Uint8Array, i: number): number => (b[i]! << 8) | b[i + 1]!;
8
- const u32be = (b: Uint8Array, i: number): number => ((b[i]! << 24) >>> 0) + (b[i + 1]! << 16) + (b[i + 2]! << 8) + b[i + 3]!;
9
- const u16le = (b: Uint8Array, i: number): number => b[i]! | (b[i + 1]! << 8);
10
- const ascii = (b: Uint8Array, i: number, n: number): string => String.fromCharCode(...b.subarray(i, i + n));
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
- /** How far a JPEG's marker at `i` reaches: a marker with no length (SOI, TEM, RSTn) is its two bytes, any other its
29
- * length after them. */
30
- const jpegStep = (b: Uint8Array, i: number): number => {
31
- const marker = b[i + 1]!;
32
- return marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7) ? 2 : 2 + u16be(b, i + 2);
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
- const walk = (start: number, end: number): void => {
43
- let i = start;
44
- while (i + 8 <= end) {
45
- let size = u32be(b, i);
46
- const type = ascii(b, i + 4, 4);
47
- let header = 8;
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
  }
@@ -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
- export function sortEntities(entities: Entity[]): Entity[] {
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
- export function findEntities(text: string, skipBotCommands: boolean): Entity[] {
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 (architecture, "The real-system adapters").
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
- chat: { idPrefix: '', ...states.chat, refresh: { get: 'getChat', known: true, query: { chat_id: '{id}' } } },
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 and Postiz call them.",
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) and node-telegram-bot-api (Postiz)',
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
- /** A medium's fields on its message, from an upload or a file sent again: a photo's one size, a video's size and
54
- * length, a document's name and type. */
55
- async function mediaFields(ctx: HandlerContext, botId: number, kind: 'photo' | 'video' | 'document', upload: Upload, given: { width?: unknown; height?: unknown; duration?: unknown }): Promise<Row | { refusal: Response }> {
56
- const operation = ctx.call.operation.id;
57
- let file: Row;
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
- file = upload.file;
68
- bytes = await fileBytes(ctx, file);
70
+ bytes = await fileBytes(ctx, upload.file);
69
71
  }
70
- else {
71
- bytes = upload.bytes;
72
- // source: https://core.telegram.org/bots/api#sendphoto "The photo must be at most 10 MB in size."
73
- // 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."
74
- // 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."
75
- // Where the documentation stops: the words for a file over its limit are the server's for a file too big
76
- // source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Bad Request: file is too big"
77
- if (bytes.byteLength > (kind === 'photo' ? 10 : 50) * 1024 * 1024) return { refusal: refuse(ctx, 400, 'Bad Request: file is too big') };
78
- if (kind === 'photo' && !imageSize(bytes)) {
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
- const ids = { file_id: file.file_id, file_unique_id: file.file_unique_id };
92
- if (kind === 'photo') {
93
- const size = imageSize(bytes) ?? { width: 0, height: 0 };
94
- // Where the documentation stops: the page gives a photo's sizes as "Available sizes of the photo" and names none;
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 { video: { ...ids, width: num(given.width, info?.width ?? 0), height: num(given.height, info?.height ?? 0), duration: num(given.duration, info?.duration ?? 0), ...(name ? { file_name: name } : {}), ...(mime ? { mime_type: mime } : {}), file_size: bytes.byteLength } };
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 { document: { ...ids, ...(name ? { file_name: name } : {}), ...(mime ? { mime_type: mime } : {}), file_size: bytes.byteLength } };
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: 'photo' | 'video' | 'document'): Promise<Response> {
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 mediaFields(ctx, botId, kind, upload, { width: param(ctx, 'width'), height: param(ctx, 'height'), duration: param(ctx, 'duration') });
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 row = await storeBotMessage(ctx, botId, access.chat, { ...media, ...caption }, reply.key, ctx.call.operation.id);
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<{ kind: 'photo' | 'video' | 'document'; upload: Upload; caption: Row; item: Row }> = [];
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
- prepared.push({ kind: item.type as 'photo' | 'video' | 'document', upload, caption, item });
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.kind, p.upload, { width: p.item.width, height: p.item.height, duration: p.item.duration });
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(ctx, access.chat));
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').find((r) => r.deleted !== true && r.file_path === path);
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);