summaryrefslogtreecommitdiff
path: root/src/bot
diff options
context:
space:
mode:
Diffstat (limited to 'src/bot')
-rw-r--r--src/bot/advanced.lisp27
-rw-r--r--src/bot/impl.lisp91
-rw-r--r--src/bot/methods.lisp46
3 files changed, 113 insertions, 51 deletions
diff --git a/src/bot/advanced.lisp b/src/bot/advanced.lisp
index a6ad9ba..241b04d 100644
--- a/src/bot/advanced.lisp
+++ b/src/bot/advanced.lisp
@@ -2,7 +2,7 @@
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com> 2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/bot/advanced 3(defpackage :ukkoclot/bot/advanced
4 (:use :c2cl :ukkoclot/bot/impl :ukkoclot/bot/methods :ukkoclot/tg-types) 4 (:use :c2cl :ukkoclot/bot/impl :ukkoclot/bot/methods :ukkoclot/tg-types)
5 (:export :bot-id :bot-username :reply-message)) 5 (:export :bot-id :bot-username :reply-animation :reply-message :try-delete-message))
6(in-package :ukkoclot/bot/advanced) 6(in-package :ukkoclot/bot/advanced)
7 7
8(defun bot-id (bot) 8(defun bot-id (bot)
@@ -17,6 +17,20 @@
17 (get-me bot) 17 (get-me bot)
18 (bot-username% bot)))) 18 (bot-username% bot))))
19 19
20;; TODO: Some kind of caching for files?
21(defun reply-animation (bot msg animation &key allow-sending-without-reply text parse-mode caption-above)
22 (send-animation bot
23 :chat-id (message-chat-id msg)
24 :animation animation
25 :caption text
26 :parse-mode parse-mode
27 :show-caption-above-media caption-above
28 :reply-parameters
29 (make-reply-parameters
30 :allow-sending-without-reply allow-sending-without-reply
31 :message-id (message-id msg)
32 :chat-id (message-chat-id msg))))
33
20(defun reply-message (bot msg text &key parse-mode allow-sending-without-reply) 34(defun reply-message (bot msg text &key parse-mode allow-sending-without-reply)
21 (send-message bot 35 (send-message bot
22 :chat-id (message-chat-id msg) 36 :chat-id (message-chat-id msg)
@@ -27,3 +41,14 @@
27 :allow-sending-without-reply allow-sending-without-reply 41 :allow-sending-without-reply allow-sending-without-reply
28 :message-id (message-id msg) 42 :message-id (message-id msg)
29 :chat-id (message-chat-id msg)))) 43 :chat-id (message-chat-id msg))))
44
45(defun try-delete-message (bot msg)
46 (handler-case
47 (delete-message bot
48 :chat-id (message-chat-id msg)
49 :message-id (message-id msg))
50 (error ()
51 (handler-case
52 (reply-animation bot msg #P"blob/do-not.mp4"
53 :allow-sending-without-reply nil)
54 (error () nil)))))
diff --git a/src/bot/impl.lisp b/src/bot/impl.lisp
index b57e2d3..57a9572 100644
--- a/src/bot/impl.lisp
+++ b/src/bot/impl.lisp
@@ -8,41 +8,62 @@
8 (:local-nicknames 8 (:local-nicknames
9 (:jzon :com.inuoe.jzon)) 9 (:jzon :com.inuoe.jzon))
10 (:export 10 (:export
11 :arg-encode :bot :bot-p :make-bot :do-call 11 :bot :bot-p :make-bot :fixup-value :do-call :parse-value
12 12
13 :bot-config :bot-db :bot-base-uri :bot-power-on :bot-username% :bot-id%)) 13 :bot-config :bot-db :bot-base-uri :bot-power-on :bot-username% :bot-id%))
14(in-package :ukkoclot/bot/impl) 14(in-package :ukkoclot/bot/impl)
15 15
16(defgeneric will-arg-encode (object) 16(defgeneric parse-value (type json)
17 (:documentation "Whether the OBJECT has any transformation done to it by arg-encode") 17 (:documentation "Parse value of TYPE from the parsed JSON")
18 (:method (obj) 18 (:method (type json)
19 nil) 19 (log-error "I don't know how to parse simple type ~A!" type)
20 (:method ((obj cons)) 20 (error "I don't know how to parse simple type ~A!" type))
21 (or (will-arg-encode (car obj)) 21 (:method ((type (eql 'boolean)) json)
22 (will-arg-encode (cdr obj))))) 22 (check-type json boolean)
23 json)
24 (:method ((type (eql 'integer)) json)
25 (check-type json integer)
26 json)
27 (:method ((type (eql 'null)) json)
28 (check-type json null)
29 json)
30 (:method ((type (eql 'string)) json)
31 (check-type json string)
32 json))
23 33
24(defgeneric arg-encode (object) 34(defun try-parse-value (type json)
25 (:documentation "Encode the OBJECT to a form that our HTTP code is happy about.") 35 (handler-case (values t (parse-value type json))
26 (:method (obj) 36 (error () (values nil nil))))
27 obj)
28 (:method ((obj cons))
29 (if (not (will-arg-encode obj))
30 obj
31 (cons (arg-encode (car obj))
32 (arg-encode (cdr obj))))))
33 37
34(defgeneric fixup-arg (value) 38(defmethod parse-value ((type cons) json)
35 (:documentation "Make sure Telegram & QURI & whatever like the arg") 39 (cond ((and (eq (car type) 'array)
40 (null (cddr type)))
41 (when json
42 (let ((element-type (cadr type)))
43 (iter (for element in-vector json)
44 (collect (parse-value element-type element) result-type vector)))))
45 ((eq (car type) 'or)
46 (iter (for el-type in (cdr type))
47 (multiple-value-bind (success res) (try-parse-value el-type json)
48 (when success
49 (return res)))
50 (finally
51 (error "Failed to parse ~S as ~A!" json type))))
52 (t
53 (error "I don't know how to parse complex type ~A!" type))))
54
55(defgeneric fixup-value (value)
56 (:documentation "Fixup top-level VALUE before passing it onto telegram")
36 (:method (value) 57 (:method (value)
37 (error "Unsupported argument type ~S (~S)" (class-name (class-of value)) value)) 58 (jzon:stringify value))
38 (:method ((value null)) 59 (:method ((value null))
39 value) 60 value)
40 (:method ((value number)) 61 (:method ((value number))
41 value) 62 value)
42 (:method ((value string)) 63 (:method ((value pathname))
43 value) 64 value)
44 (:method ((value hash-table)) 65 (:method ((value string))
45 (jzon:stringify value))) 66 value))
46 67
47(defstruct (bot (:constructor make-bot%)) 68(defstruct (bot (:constructor make-bot%))
48 (config (error "No value given for config") :read-only t) 69 (config (error "No value given for config") :read-only t)
@@ -58,39 +79,35 @@
58 (config-bot-token config) "/"))) 79 (config-bot-token config) "/")))
59 (make-bot% :config config :db db :base-uri base-uri))) 80 (make-bot% :config config :db db :base-uri base-uri)))
60 81
61(defun args-plist->alist (args-plist) 82(defun fixup-args (args)
62 (iter (for (old-key value) on args-plist by #'cddr) 83 (iter (for (key . value) in args)
63 (collect 84 (collect
64 (let ((new-key (string-downcase (lisp->snake-case (symbol-name old-key))))) 85 (cons (string-downcase (lisp->snake-case (symbol-name key)))
65 (cons new-key value))))) 86 (fixup-value value)))))
66
67(defun fixup-args (args-alist)
68 (iter (for (name . value) in args-alist)
69 (collecting (cons name (fixup-arg (arg-encode value))))))
70 87
71(defun req (uri method content) 88(defun req (uri method content)
72 ;; We deal with this manually 89 ;; We deal with this manually
73 (handler-bind ((dex:http-request-too-many-requests #'dex:ignore-and-continue)) 90 (handler-bind ((dex:http-request-too-many-requests #'dex:ignore-and-continue))
74 (dex:request uri :method method :content content))) 91 (dex:request uri :method method :content content)))
75 92
76(defun do-call% (bot method uri mapfn args-encoded) 93(defun do-call% (bot method uri type args-encoded)
77 (let ((body (req uri method args-encoded))) 94 (let ((body (req uri method args-encoded)))
78 (let ((hash (jzon:parse body))) 95 (let ((hash (jzon:parse body)))
79 (acond 96 (acond
80 ((gethash "ok" hash) (funcall mapfn (gethash "result" hash))) 97 ((gethash "ok" hash) (parse-value type (gethash "result" hash)))
81 ((aand (gethash "parameters" hash) 98 ((aand (gethash "parameters" hash)
82 (gethash "retry_after" it)) 99 (gethash "retry_after" it))
83 (log-info "Should sleep for ~A seconds" it) 100 (log-info "Should sleep for ~A seconds" it)
84 (sleep it) 101 (sleep it)
85 (log-info "Good morning!") 102 (log-info "Good morning!")
86 (do-call% bot method uri mapfn args-encoded)) 103 (do-call% bot method uri type args-encoded))
87 (t (error "TG error ~A: ~A ~:A" 104 (t (error "TG error ~A: ~A ~:A"
88 (gethash "error_code" hash) 105 (gethash "error_code" hash)
89 (gethash "description" hash) 106 (gethash "description" hash)
90 (gethash "parameters" hash))))))) 107 (gethash "parameters" hash)))))))
91 108
92(defun do-call (bot method path mapfn args-plist) 109(defun do-call (bot method path type args)
93 (let ((uri (concatenate 'string (bot-base-uri bot) path)) 110 (let ((uri (concatenate 'string (bot-base-uri bot) path))
94 (args-encoded (fixup-args (args-plist->alist args-plist)))) 111 (args-encoded (fixup-args args)))
95 (log-debug "~A .../~A ~S" method path args-encoded) 112 (log-debug "~A .../~A ~S" method path args-encoded)
96 (do-call% bot method uri mapfn args-encoded))) 113 (do-call% bot method uri type args-encoded)))
diff --git a/src/bot/methods.lisp b/src/bot/methods.lisp
index 99b6411..7ccc4ad 100644
--- a/src/bot/methods.lisp
+++ b/src/bot/methods.lisp
@@ -2,22 +2,22 @@
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com> 2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/bot/methods 3(defpackage :ukkoclot/bot/methods
4 (:use :c2cl :ukkoclot/bot/impl :ukkoclot/tg-types :ukkoclot/tg-types/macros) 4 (:use :c2cl :ukkoclot/bot/impl :ukkoclot/tg-types :ukkoclot/tg-types/macros)
5 (:export :answer-callback-query :delete-message :edit-message-text :get-me :get-updates :send-message :set-my-name)) 5 (:export :answer-callback-query :delete-message :send-animation :edit-message-text :get-me :get-updates :send-message :set-my-name))
6(in-package :ukkoclot/bot/methods) 6(in-package :ukkoclot/bot/methods)
7 7
8(define-tg-method (answer-callback-query boolean "answerCallbackQuery" #'identity) 8(define-tg-method (answer-callback-query boolean "answerCallbackQuery")
9 (callback-query-id string) 9 (callback-query-id string)
10 (text (or string null) nil) 10 (text (or string null) nil)
11 (show-alert boolean nil) 11 (show-alert boolean nil)
12 (url (or string null) nil) 12 (url (or string null) nil)
13 (cache-time (or integer null) nil)) 13 (cache-time (or integer null) nil))
14 14
15(define-tg-method (delete-message boolean "deleteMessage" #'identity) 15(define-tg-method (delete-message boolean "deleteMessage")
16 (chat-id (or integer string)) 16 (chat-id (or integer string))
17 (message-id integer)) 17 (message-id integer))
18 18
19;; TODO: Add a way to simply specify :message msg :) 19;; TODO: Add a way to simply specify :message msg :)
20(define-tg-method (edit-message-text message "editMessageText" #'hash->message) 20(define-tg-method (edit-message-text message "editMessageText")
21 (business-connection-id (or string null) nil) 21 (business-connection-id (or string null) nil)
22 (chat-id (or integer string null) nil) 22 (chat-id (or integer string null) nil)
23 (message-id (or integer null) nil) 23 (message-id (or integer null) nil)
@@ -28,7 +28,7 @@
28 (link-preview-options (or link-preview-options null) nil) 28 (link-preview-options (or link-preview-options null) nil)
29 (reply-markup (or inline-keyboard-markup null) nil)) 29 (reply-markup (or inline-keyboard-markup null) nil))
30 30
31(define-tg-method (get-me% user "getMe" #'hash->user :GET)) 31(define-tg-method (get-me% user "getMe" :GET))
32 32
33(defun get-me (bot) 33(defun get-me (bot)
34 (let ((res (get-me% bot))) 34 (let ((res (get-me% bot)))
@@ -36,16 +36,39 @@
36 (setf (bot-username% bot) (user-username res)) 36 (setf (bot-username% bot) (user-username res))
37 res)) 37 res))
38 38
39(define-tg-method (get-my-name bot-name "getMyName" #'hash->bot-name :GET) 39(define-tg-method (get-my-name bot-name "getMyName" :GET)
40 (language-code (or string null) nil)) 40 (language-code (or string null) nil))
41 41
42(define-tg-method (get-updates (array update) "getUpdates" #'parse-update-array) 42(define-tg-method (get-updates (array update) "getUpdates")
43 (offset (or integer null) nil) 43 (offset (or integer null) nil)
44 (limit (or integer null) nil) 44 (limit (or integer null) nil)
45 (timeout (or integer null) nil) 45 (timeout (or integer null) nil)
46 (allowed-updates (or string null) nil)) 46 (allowed-updates (or string null) nil))
47 47
48(define-tg-method (send-message message "sendMessage" #'hash->message) 48(define-tg-method (send-animation message "sendAnimation")
49 (business-connection-id (or string null) nil)
50 (chat-id (or integer string))
51 (message-thread-id (or integer null) nil)
52 (direct-messages-topic-id (or integer null) nil)
53 (animation (or pathname string))
54 (duration (or integer null) nil)
55 (width (or integer null) nil)
56 (height (or integer null) nil)
57 (thumbnail (or pathname string null) nil)
58 (caption (or string null) nil)
59 (parse-mode (or string null) nil)
60 (caption-entities (or (array message-entity) null) nil)
61 (show-caption-above-media boolean nil)
62 (has-spoiler boolean nil)
63 (disable-notification boolean nil)
64 (protect-content boolean nil)
65 (allow-paid-broadcast boolean nil)
66 (message-effect-id (or string null) nil)
67 (suggested-post-parameters (or suggested-post-parameters null) nil)
68 (reply-parameters (or reply-parameters null) nil)
69 (reply-markup (or inline-keyboard-markup reply-keyboard-markup reply-keyboard-remove force-reply null) nil))
70
71(define-tg-method (send-message message "sendMessage")
49 (business-connection-id (or string null) nil) 72 (business-connection-id (or string null) nil)
50 (chat-id (or integer string)) 73 (chat-id (or integer string))
51 (message-thread-id (or integer null) nil) 74 (message-thread-id (or integer null) nil)
@@ -58,12 +81,9 @@
58 (protect-content (or boolean null) nil) 81 (protect-content (or boolean null) nil)
59 (message-effect-id (or string null) nil) 82 (message-effect-id (or string null) nil)
60 (reply-parameters (or reply-parameters null) nil) 83 (reply-parameters (or reply-parameters null) nil)
61 (reply-markup (or inline-keyboard-markup 84 (reply-markup (or inline-keyboard-markup reply-keyboard-markup reply-keyboard-remove force-reply null) nil))
62 ;; TODO: reply-keyboard-markup
63 ;; TODO: reply-keyboard-remove
64 force-reply null) nil))
65 85
66(define-tg-method (set-my-name% boolean "setMyName" #'identity) 86(define-tg-method (set-my-name% boolean "setMyName")
67 (name (or string null) nil) 87 (name (or string null) nil)
68 (language-code (or string null) nil)) 88 (language-code (or string null) nil))
69 89