;; SPDX-License-Identifier: EUPL-1.2 ;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs (defpackage :ukkoclot/bot/impl (:use :c2cl :iterate :ukkoclot/config :ukkoclot/log) (:import-from :anaphora :aand :acond :it) (:import-from :dex) (:import-from :ukkoclot/strings :lisp->snake-case) (:local-nicknames (:jzon :com.inuoe.jzon)) (:export :arg-encode :bot :bot-p :make-bot :do-call :bot-config :bot-db :bot-base-uri :bot-power-on :bot-username% :bot-id%)) (in-package :ukkoclot/bot/impl) (defgeneric will-arg-encode (object) (:documentation "Whether the OBJECT has any transformation done to it by arg-encode") (:method (obj) nil) (:method ((obj cons)) (or (will-arg-encode (car obj)) (will-arg-encode (cdr obj))))) (defgeneric arg-encode (object) (:documentation "Encode the OBJECT to a form that our HTTP code is happy about.") (:method (obj) obj) (:method ((obj cons)) (if (not (will-arg-encode obj)) obj (cons (arg-encode (car obj)) (arg-encode (cdr obj)))))) (defgeneric fixup-arg (value) (:documentation "Make sure Telegram & QURI & whatever like the arg") (:method (value) (error "Unsupported argument type ~S (~S)" (class-name (class-of value)) value)) (:method ((value null)) value) (:method ((value number)) value) (:method ((value string)) value) (:method ((value hash-table)) (jzon:stringify value))) (defstruct (bot (:constructor make-bot%)) (config (error "No value given for config") :read-only t) (db (error "No value given for DB") :read-only t) (base-uri (error "No value given for base-uri") :read-only t) (power-on t :type boolean) (username% nil :type (or string null)) (id% nil :type (or integer null))) (defun make-bot (config db) (let ((base-uri (concatenate 'string "https://api.telegram.org/bot" (config-bot-token config) "/"))) (make-bot% :config config :db db :base-uri base-uri))) (defun args-plist->alist (args-plist) (iter (for (old-key value) on args-plist by #'cddr) (collect (let ((new-key (string-downcase (lisp->snake-case (symbol-name old-key))))) (cons new-key value))))) (defun fixup-args (args-alist) (iter (for (name . value) in args-alist) (collecting (cons name (fixup-arg (arg-encode value)))))) (defun req (uri method content) ;; We deal with this manually (handler-bind ((dex:http-request-too-many-requests #'dex:ignore-and-continue)) (dex:request uri :method method :content content))) (defun do-call% (bot method uri mapfn args-encoded) (let ((body (req uri method args-encoded))) (let ((hash (jzon:parse body))) (acond ((gethash "ok" hash) (funcall mapfn (gethash "result" hash))) ((aand (gethash "parameters" hash) (gethash "retry_after" it)) (log-info "Should sleep for ~A seconds" it) (sleep it) (log-info "Good morning!") (do-call% bot method uri mapfn args-encoded)) (t (error "TG error ~A: ~A ~:A" (gethash "error_code" hash) (gethash "description" hash) (gethash "parameters" hash))))))) (defun do-call (bot method path mapfn args-plist) (let ((uri (concatenate 'string (bot-base-uri bot) path)) (args-encoded (fixup-args (args-plist->alist args-plist)))) (log-debug "~A .../~A ~S" method path args-encoded) (do-call% bot method uri mapfn args-encoded)))