Commit471ef335Recorded18 Apr 2026Repositorysigil-telegram
Merge branch 'feat/video-upload'
Changed
src/sigil/telegram.sgl | 2 ++
src/sigil/telegram/client.sgl | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/test-telegram.sgl | 34 +++++++++++++++++++++++++++++++++-
3 files changed, 123 insertions(+), 1 deletion(-)Diff
A merge. Shown against its first parent, so this is the effect of merging rather than the work of the branch.
src/sigil/telegram.sglmodified
@@ -60,10 +60,12 @@
60
;; Media (URL/file_id) 61
tg-send-photo 62
tg-send-document+63
tg-send-video 64
65
;; Media (file upload) 66
tg-upload-photo 67
tg-upload-document+68
tg-upload-video 69
70
;; Callbacks 71
tg-answer-callbacksrc/sigil/telegram/client.sglmodified
@@ -34,10 +34,12 @@
34
;; Media (URL/file_id) 35
tg-send-photo 36
tg-send-document+37
tg-send-video 38
39
;; Media (file upload) 40
tg-upload-photo 41
tg-upload-document+42
tg-upload-video 43
44
;; Callbacks 45
tg-answer-callback@@ -215,6 +217,41 @@
217
(params (add-param params reply_markup: reply-markup))) 218
(dict->tg-message (tg-api-call client "sendDocument" params: params)))) 219
+220
;;; Send a video by URL or file_id.+221
;;;+222
;;; The `video` parameter must be a Telegram file_id string or an+223
;;; HTTP/HTTPS URL. For uploading a local file, use `tg-upload-video`.+224
;;;+225
;;; Optional parameters mirror Telegram's `sendVideo` extras: duration+226
;;; (seconds), width/height (pixels), and supports-streaming (whether+227
;;; the file is suitable for streaming).+228
;;;+229
;;; ```scheme+230
;;; (tg-send-video client 12345 "https://example.com/clip.mp4"+231
;;; caption: "Build demo" supports-streaming: #t)+232
;;; ```+233
(define (tg-send-video client chat-id video+234
(keys: (caption #f) (parse-mode #f) (reply-markup #f)+235
(duration #f) (width #f) (height #f)+236
(supports-streaming #f)))+237
(: tg-client? (any-of integer? string?) string?+238
(caption: (maybe string?)) (parse-mode: (maybe string?))+239
(reply-markup: (maybe dict?))+240
(duration: (maybe integer?)) (width: (maybe integer?))+241
(height: (maybe integer?)) (supports-streaming: (maybe boolean?))+242
-> tg-message?)+243
(let* ((params (dict chat_id: chat-id video: video))+244
(params (add-param params caption: caption))+245
(params (add-param params parse_mode: parse-mode))+246
(params (add-param params reply_markup: reply-markup))+247
(params (add-param params duration: duration))+248
(params (add-param params width: width))+249
(params (add-param params height: height))+250
(params (if supports-streaming+251
(dict-set params supports_streaming: #t)+252
params)))+253
(dict->tg-message (tg-api-call client "sendVideo" params: params))))+254
255
;; ============================================================ 256
;; Multipart File Upload 257
;; ============================================================@@ -387,6 +424,57 @@
424
(files (list (list "document" filename mime data)))) 425
(dict->tg-message (tg-api-call/upload client "sendDocument" fields files)))) 426
+427
;;; Upload a local video file.+428
;;;+429
;;; Reads the file at `path`, auto-detects its MIME type, and sends+430
;;; it to the Telegram API via multipart/form-data upload. The bytes+431
;;; are forwarded verbatim — any audio track in the container is+432
;;; preserved (no transcoding).+433
;;;+434
;;; Optional parameters mirror `tg-send-video`: duration (seconds),+435
;;; width/height (pixels), supports-streaming.+436
;;;+437
;;; ```scheme+438
;;; (tg-upload-video client 12345 "/tmp/screencast.mp4"+439
;;; caption: "Cinder Cantata progress" supports-streaming: #t)+440
;;; ```+441
(define (tg-upload-video client chat-id path+442
(keys: (caption #f) (parse-mode #f) (reply-markup #f)+443
(duration #f) (width #f) (height #f)+444
(supports-streaming #f)))+445
(: tg-client? (any-of integer? string?) string?+446
(caption: (maybe string?)) (parse-mode: (maybe string?))+447
(reply-markup: (maybe dict?))+448
(duration: (maybe integer?)) (width: (maybe integer?))+449
(height: (maybe integer?)) (supports-streaming: (maybe boolean?))+450
-> tg-message?)+451
(let* ((data (read-file-bytes path))+452
(mime (mime-type-for-file path))+453
(filename (path-basename path))+454
(fields (let* ((f (list (cons "chat_id" (if (integer? chat-id)+455
(number->string chat-id)+456
chat-id))))+457
(f (if caption (cons (cons "caption" caption) f) f))+458
(f (if parse-mode (cons (cons "parse_mode" parse-mode) f) f))+459
(f (if reply-markup+460
(cons (cons "reply_markup" (json-encode reply-markup)) f)+461
f))+462
(f (if duration+463
(cons (cons "duration" (number->string duration)) f)+464
f))+465
(f (if width+466
(cons (cons "width" (number->string width)) f)+467
f))+468
(f (if height+469
(cons (cons "height" (number->string height)) f)+470
f))+471
(f (if supports-streaming+472
(cons (cons "supports_streaming" "true") f)+473
f)))+474
f))+475
(files (list (list "video" filename mime data))))+476
(dict->tg-message (tg-api-call/upload client "sendVideo" fields files))))+477
478
;; ============================================================ 479
;; Callback Queries 480
;; ============================================================test/test-telegram.sglmodified
@@ -277,6 +277,38 @@
277
(assert-true (bytevector? body)) 278
;; Body should end with closing boundary 279
(let ((str (utf8->string body)))−280
(assert-true (string-contains? str (string-append "--" boundary "--")))))))+280
(assert-true (string-contains? str (string-append "--" boundary "--"))))))+281
+282
(test "video file part uses video field name"+283
(let* ((data (string->utf8 "fake video bytes"))+284
(bv (encode-file-part "BOUNDARY" "video" "clip.mp4" "video/mp4" data))+285
(str (utf8->string bv)))+286
(assert-true (string-contains? str "name=\"video\"; filename=\"clip.mp4\""))+287
(assert-true (string-contains? str "Content-Type: video/mp4"))+288
(assert-true (string-contains? str "fake video bytes"))))+289
+290
(test "multipart body preserves arbitrary file bytes verbatim"+291
;; Audio tracks and binary container bytes must pass through untouched.+292
(let* ((raw (bytevector 0 1 2 255 254 253 0 13 10))+293
(result (build-multipart-body+294
(list (cons "chat_id" "1"))+295
(list (list "video" "a.mp4" "video/mp4" raw))))+296
(body (cdr result)))+297
(assert-true (bytevector? body))+298
;; Locate the raw payload inside the body bytevector+299
(let loop ((i 0) (found #f))+300
(cond+301
(found (assert-true #t))+302
((> (+ i (bytevector-length raw)) (bytevector-length body))+303
(assert-true #f))+304
((let match ((j 0))+305
(cond+306
((= j (bytevector-length raw)) #t)+307
((= (bytevector-u8-ref body (+ i j))+308
(bytevector-u8-ref raw j))+309
(match (+ j 1)))+310
(else #f)))+311
(loop i #t))+312
(else (loop (+ i 1) #f))))))) 313
314
(run-tests)