diff options
Diffstat (limited to 'src/main.lisp')
| -rw-r--r-- | src/main.lisp | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/src/main.lisp b/src/main.lisp new file mode 100644 index 0000000..af88fe6 --- /dev/null +++ b/src/main.lisp | |||
| @@ -0,0 +1,354 @@ | |||
| 1 | ;; SPDX-License-Identifier: EUPL-1.2 | ||
| 2 | ;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com> | ||
| 3 | (defpackage :ukkoclot/main | ||
| 4 | (:nicknames :ukkoclot) | ||
| 5 | (:use :c2cl :ukkoclot/bot :ukkoclot/config :ukkoclot/inline-bots :ukkoclot/log :ukkoclot/tg-types) | ||
| 6 | (:import-from :anaphora :aand :awhen :it) | ||
| 7 | (:import-from :ukkoclot/bot :make-bot :bot-power-on) | ||
| 8 | (:import-from :ukkoclot/db :with-db) | ||
| 9 | (:import-from :ukkoclot/strings :escape-xml :is-tg-whitespace-str :starts-with :starts-with-ignore-case) | ||
| 10 | (:local-nicknames | ||
| 11 | (:jzon :com.inuoe.jzon)) | ||
| 12 | (:export :main)) | ||
| 13 | (in-package :ukkoclot/main) | ||
| 14 | |||
| 15 | (defvar *in-prod* t) | ||
| 16 | |||
| 17 | (defmacro reporty ((evt) &body body) | ||
| 18 | `(if *in-prod* | ||
| 19 | (handler-case (progn ,@body) | ||
| 20 | (error (err) (report-error bot ,evt err))) | ||
| 21 | (progn ,@body))) | ||
| 22 | |||
| 23 | (defun main () | ||
| 24 | (unwind-protect | ||
| 25 | (let ((config (config-load #P"config.default.lisp"))) | ||
| 26 | (config-merge config #P"config.lisp") | ||
| 27 | (log-info "Starting up ~A" (config-bot-name config)) | ||
| 28 | (with-db (db (config-db-path config)) | ||
| 29 | (let ((bot (make-bot config db))) | ||
| 30 | ;; TODO: Catch fatal errors & report them | ||
| 31 | (wrapped-main bot config)))) | ||
| 32 | (log-info "We're done!"))) | ||
| 33 | |||
| 34 | (defun wrapped-main (bot config) | ||
| 35 | (send-message bot :chat-id (config-dev-group config) :text "Initializing...") | ||
| 36 | (set-my-name bot :name (config-bot-name config)) | ||
| 37 | (let ((gup-offset 0)) | ||
| 38 | (loop while (bot-power-on bot) do | ||
| 39 | (let ((updates (get-updates bot :timeout 60 :offset gup-offset))) | ||
| 40 | (loop for update across updates do | ||
| 41 | (unwind-protect | ||
| 42 | (progn | ||
| 43 | (awhen (update-message update) | ||
| 44 | (reporty (it) | ||
| 45 | (on-message bot it))) | ||
| 46 | (awhen (update-callback-query update) | ||
| 47 | (reporty (it) | ||
| 48 | (on-callback-query bot it)))) | ||
| 49 | (setf gup-offset (1+ (update-update-id update))))))) | ||
| 50 | ;; One last getUpdates to make sure offset is stored on server | ||
| 51 | (get-updates bot :timeout 0 :limit 1 :offset gup-offset)) | ||
| 52 | (send-message bot :chat-id (config-dev-group config) :text "Shutting down...")) | ||
| 53 | |||
| 54 | (defun on-callback-query (bot cb) | ||
| 55 | (let ((data (callback-query-data cb))) | ||
| 56 | (cond ((and data | ||
| 57 | (starts-with data "bbl:") | ||
| 58 | (= (user-id (callback-query-from cb)) | ||
| 59 | (config-owner (bot-config bot)))) | ||
| 60 | (let ((bot-id (read-from-string data t nil :start 4))) | ||
| 61 | (blacklist-inline-bot bot bot-id)) | ||
| 62 | (awhen (callback-query-message cb) | ||
| 63 | (delete-message bot | ||
| 64 | :chat-id (message-chat-id it) | ||
| 65 | :message-id (message-id it))) | ||
| 66 | (answer-callback-query bot | ||
| 67 | :callback-query-id (callback-query-id cb) | ||
| 68 | :text "OK")) | ||
| 69 | ((and data | ||
| 70 | (starts-with data "bwl:") | ||
| 71 | (= (user-id (callback-query-from cb)) | ||
| 72 | (config-owner (bot-config bot)))) | ||
| 73 | (let ((bot-id (read-from-string data t nil :start 4))) | ||
| 74 | (whitelist-inline-bot bot bot-id)) | ||
| 75 | (awhen (callback-query-message cb) | ||
| 76 | (delete-message bot | ||
| 77 | :chat-id (message-chat-id it) | ||
| 78 | :message-id (message-id it))) | ||
| 79 | (answer-callback-query bot | ||
| 80 | :callback-query-id (callback-query-id cb) | ||
| 81 | :text "OK")) | ||
| 82 | (t | ||
| 83 | (log-info "Unrecognised callback query data: ~A" data) | ||
| 84 | (answer-callback-query bot | ||
| 85 | :callback-query-id (callback-query-id cb) | ||
| 86 | :text "Unallowed callback query, don't press the button again" | ||
| 87 | :show-alert t))))) | ||
| 88 | |||
| 89 | |||
| 90 | (defun on-message (bot msg) | ||
| 91 | (block nil | ||
| 92 | (awhen (message-via-bot msg) | ||
| 93 | (unless (on-inline-bot bot msg it) | ||
| 94 | (return))) | ||
| 95 | |||
| 96 | (awhen (message-text msg) | ||
| 97 | (on-text-message bot msg it)) | ||
| 98 | |||
| 99 | (awhen (message-new-chat-members msg) | ||
| 100 | (loop for new-chat-member across it do | ||
| 101 | (on-new-member bot msg new-chat-member))))) | ||
| 102 | |||
| 103 | (defun on-new-member (bot msg new-member) | ||
| 104 | ;; TODO: Rule 11 no hating on cats on bot entry | ||
| 105 | ;; TODO: Rule 10 have fun and enjoy your time on user entry | ||
| 106 | (if (= (user-id new-member) (bot-id bot)) | ||
| 107 | nil | ||
| 108 | (send-message bot | ||
| 109 | :chat-id (message-chat-id msg) | ||
| 110 | :text (concatenate 'string "Hello there, " (user-format-name new-member) "! Be on your bestest behaviour now!!") | ||
| 111 | :parse-mode "HTML" | ||
| 112 | :reply-parameters | ||
| 113 | (make-reply-parameters | ||
| 114 | :allow-sending-without-reply t | ||
| 115 | :message-id (message-id msg) | ||
| 116 | :chat-id (message-chat-id msg))))) | ||
| 117 | |||
| 118 | (defun is-bad-text (text) | ||
| 119 | ;; TODO: | ||
| 120 | nil) | ||
| 121 | |||
| 122 | (defun on-text-message (bot msg text) | ||
| 123 | (block nil | ||
| 124 | (when (is-bad-text text) | ||
| 125 | ;; TODO: Delete message, mute & warn user | ||
| 126 | ;; 0 current warns: 5 minute mute, +1 warn | ||
| 127 | ;; 1 current warn : 10 minute mute, +1 warn | ||
| 128 | ;; 2 current warns: 30 minute mute, +1 warn | ||
| 129 | ;; 3 current warns: 1 hour mute, +1 warn | ||
| 130 | ;; 4 current warns: 1 day mute, +1 warn | ||
| 131 | ;; 5 current warns: Ban | ||
| 132 | ;; | ||
| 133 | ;; warn gets removed after a month of no warns | ||
| 134 | (return)) | ||
| 135 | |||
| 136 | (awhen (message-entities msg) | ||
| 137 | (loop for entity across it | ||
| 138 | when (and (eq (message-entity-type entity) :bot-command) | ||
| 139 | (= (message-entity-offset entity) 0)) | ||
| 140 | do (on-text-command bot msg text (message-entity-extract entity text)))) | ||
| 141 | |||
| 142 | (cond ((equal text ":3") | ||
| 143 | (send-message bot :chat-id (message-chat-id msg) | ||
| 144 | :text ">:3" | ||
| 145 | :reply-parameters (make-reply-parameters :message-id (message-id msg) | ||
| 146 | :chat-id (message-chat-id msg)))) | ||
| 147 | |||
| 148 | ((equal text ">:3") | ||
| 149 | (send-message bot :chat-id (message-chat-id msg) | ||
| 150 | :text "<b>>:3</b>" | ||
| 151 | :parse-mode "HTML" | ||
| 152 | :reply-parameters (make-reply-parameters | ||
| 153 | :message-id (message-id msg) | ||
| 154 | :chat-id (message-chat-id msg)))) | ||
| 155 | |||
| 156 | ((starts-with-ignore-case text "big ") | ||
| 157 | (let ((the-text (subseq text 4))) | ||
| 158 | (unless (is-tg-whitespace-str the-text) | ||
| 159 | (send-message bot | ||
| 160 | :chat-id (message-chat-id msg) | ||
| 161 | :text (concatenate 'string | ||
| 162 | "<b>" | ||
| 163 | (escape-xml (string-upcase the-text)) | ||
| 164 | "</b>") | ||
| 165 | :parse-mode "HTML" | ||
| 166 | :reply-parameters | ||
| 167 | (make-reply-parameters | ||
| 168 | :message-id (message-id msg) | ||
| 169 | :chat-id (message-chat-id msg)))))) | ||
| 170 | |||
| 171 | ((string-equal text "dio cane") | ||
| 172 | (let ((reply-msg-id (message-id msg)) | ||
| 173 | (reply-chat-id (message-chat-id msg))) | ||
| 174 | (awhen (message-reply-to-message msg) | ||
| 175 | (setf reply-msg-id (message-id it)) | ||
| 176 | (setf reply-chat-id (message-chat-id it))) | ||
| 177 | (send-message bot | ||
| 178 | :chat-id (message-chat-id msg) | ||
| 179 | :text "porco dio" | ||
| 180 | :reply-parameters | ||
| 181 | (make-reply-parameters | ||
| 182 | :message-id reply-msg-id | ||
| 183 | :chat-id reply-chat-id)))) | ||
| 184 | |||
| 185 | ((string-equal text "forgor") | ||
| 186 | (send-message bot | ||
| 187 | :chat-id (message-chat-id msg) | ||
| 188 | :text "💀" | ||
| 189 | :reply-parameters | ||
| 190 | (make-reply-parameters | ||
| 191 | :message-id (message-id msg) | ||
| 192 | :chat-id (message-chat-id msg)))) | ||
| 193 | |||
| 194 | ((string-equal text "huh") | ||
| 195 | (send-message bot | ||
| 196 | :chat-id (message-chat-id msg) | ||
| 197 | :text "idgi" | ||
| 198 | :reply-parameters | ||
| 199 | (make-reply-parameters | ||
| 200 | :message-id (message-id msg) | ||
| 201 | :chat-id (message-chat-id msg)))) | ||
| 202 | |||
| 203 | ((string= text "H") | ||
| 204 | (send-message bot | ||
| 205 | :chat-id (message-chat-id msg) | ||
| 206 | :text "<code>Randomly selected reminder that h > H.</code>" | ||
| 207 | :parse-mode "HTML" | ||
| 208 | :reply-parameters | ||
| 209 | (make-reply-parameters | ||
| 210 | :message-id (message-id msg) | ||
| 211 | :chat-id (message-chat-id msg)))) | ||
| 212 | |||
| 213 | ((string-equal text "porco dio") | ||
| 214 | (let ((reply-msg-id (message-id msg)) | ||
| 215 | (reply-chat-id (message-chat-id msg))) | ||
| 216 | (awhen (message-reply-to-message msg) | ||
| 217 | (setf reply-msg-id (message-id it)) | ||
| 218 | (setf reply-chat-id (message-chat-id it))) | ||
| 219 | (send-message bot | ||
| 220 | :chat-id (message-chat-id msg) | ||
| 221 | :text "dio cane" | ||
| 222 | :reply-parameters | ||
| 223 | (make-reply-parameters | ||
| 224 | :message-id reply-msg-id | ||
| 225 | :chat-id reply-chat-id)))) | ||
| 226 | |||
| 227 | ((starts-with-ignore-case text "say ") | ||
| 228 | (let ((the-text (subseq text 4))) | ||
| 229 | (unless (is-tg-whitespace-str the-text) | ||
| 230 | (send-message bot | ||
| 231 | :chat-id (message-chat-id msg) | ||
| 232 | :text the-text | ||
| 233 | :reply-parameters | ||
| 234 | (make-reply-parameters | ||
| 235 | :message-id (message-id msg) | ||
| 236 | :chat-id (message-chat-id msg)))))) | ||
| 237 | |||
| 238 | ((string-equal text "uwu") | ||
| 239 | (send-message bot | ||
| 240 | :chat-id (message-chat-id msg) | ||
| 241 | :text "OwO" | ||
| 242 | :reply-parameters | ||
| 243 | (make-reply-parameters | ||
| 244 | :message-id (message-id msg) | ||
| 245 | :chat-id (message-chat-id msg)))) | ||
| 246 | |||
| 247 | ((string-equal text "waow") | ||
| 248 | (let ((reply-msg-id (message-id msg)) | ||
| 249 | (reply-chat-id (message-chat-id msg))) | ||
| 250 | (awhen (message-reply-to-message msg) | ||
| 251 | (setf reply-msg-id (message-id it)) | ||
| 252 | (setf reply-chat-id (message-chat-id it))) | ||
| 253 | (send-message bot | ||
| 254 | :chat-id (message-chat-id msg) | ||
| 255 | :text "BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED" | ||
| 256 | :reply-parameters | ||
| 257 | (make-reply-parameters | ||
| 258 | :message-id reply-msg-id | ||
| 259 | :chat-id reply-chat-id)))) | ||
| 260 | |||
| 261 | ((string-equal text "what") | ||
| 262 | (send-message bot | ||
| 263 | :chat-id (message-chat-id msg) | ||
| 264 | :text (with-output-to-string (s) | ||
| 265 | (if (char= (elt text 0) #\w) | ||
| 266 | (write-char #\g s) | ||
| 267 | (write-char #\G s)) | ||
| 268 | (if (char= (elt text 1) #\h) | ||
| 269 | (write-string "ood " s) | ||
| 270 | (write-string "OOD " s)) | ||
| 271 | (if (char= (elt text 2) #\a) | ||
| 272 | (write-string "gir" s) | ||
| 273 | (write-string "GIR" s)) | ||
| 274 | (if (char= (elt text 3) #\t) | ||
| 275 | (write-char #\l s) | ||
| 276 | (write-char #\L s))) | ||
| 277 | :reply-parameters | ||
| 278 | (make-reply-parameters | ||
| 279 | :message-id (message-id msg) | ||
| 280 | :chat-id (message-chat-id msg)))) | ||
| 281 | ))) | ||
| 282 | |||
| 283 | (defun simplify-cmd (bot cmd) | ||
| 284 | (let ((at-idx (position #\@ cmd))) | ||
| 285 | (if (null at-idx) | ||
| 286 | (subseq cmd 1) | ||
| 287 | (let ((username (subseq cmd (1+ at-idx))) | ||
| 288 | (my-username (bot-username bot))) | ||
| 289 | (if (equal username my-username) | ||
| 290 | (subseq cmd 1 at-idx) | ||
| 291 | nil))))) | ||
| 292 | |||
| 293 | (defun on-text-command (bot msg text cmd) | ||
| 294 | (let ((simple-cmd (simplify-cmd bot cmd))) | ||
| 295 | (log-debug "text-command: ~A AKA ~A" cmd simple-cmd) | ||
| 296 | (cond ((equal simple-cmd "chatid") | ||
| 297 | (send-message bot :chat-id (message-chat-id msg) | ||
| 298 | :text (format nil "<code>~A</code>" (message-chat-id msg)) | ||
| 299 | :parse-mode "HTML" | ||
| 300 | :reply-parameters (make-reply-parameters :message-id (message-id msg) | ||
| 301 | :chat-id (message-chat-id msg)))) | ||
| 302 | |||
| 303 | ((equal simple-cmd "msginfo") | ||
| 304 | (aand (message-reply-to-message msg) | ||
| 305 | (send-message bot :chat-id (message-chat-id msg) | ||
| 306 | ;; TODO: Text needs lot more massaging | ||
| 307 | :text (jzon:stringify (arg-encode it)) | ||
| 308 | :reply-parameters | ||
| 309 | (make-reply-parameters | ||
| 310 | :message-id (message-id msg) | ||
| 311 | :chat-id (message-chat-id msg))))) | ||
| 312 | |||
| 313 | ((equal simple-cmd "ping") | ||
| 314 | (let* ((start-time (get-internal-real-time)) | ||
| 315 | (reply (send-message bot | ||
| 316 | :chat-id (message-chat-id msg) | ||
| 317 | :text "Pong! | ||
| 318 | Send time: ..." | ||
| 319 | :reply-parameters | ||
| 320 | (make-reply-parameters | ||
| 321 | :message-id (message-id msg) | ||
| 322 | :chat-id (message-chat-id msg)))) | ||
| 323 | (end-time (get-internal-real-time)) | ||
| 324 | (time-elapsed (* (/ (- end-time start-time) internal-time-units-per-second) | ||
| 325 | 1000))) | ||
| 326 | (edit-message-text bot | ||
| 327 | :chat-id (message-chat-id msg) | ||
| 328 | :message-id (message-id reply) | ||
| 329 | :text (format nil "Pong!~2%Send time: ~Gms" time-elapsed)))) | ||
| 330 | |||
| 331 | ((and (equal simple-cmd "shutdown") | ||
| 332 | (message-from msg) | ||
| 333 | (= (user-id (message-from msg)) (config-owner (bot-config bot)))) | ||
| 334 | (setf (bot-power-on bot) nil) | ||
| 335 | (send-message bot | ||
| 336 | :chat-id (message-chat-id msg) | ||
| 337 | :text "Initialising shutdown..." | ||
| 338 | :reply-parameters | ||
| 339 | (make-reply-parameters | ||
| 340 | :allow-sending-without-reply t | ||
| 341 | :message-id (message-id msg) | ||
| 342 | :chat-id (message-chat-id msg)))) | ||
| 343 | |||
| 344 | ))) | ||
| 345 | |||
| 346 | (defun report-error (bot evt err) | ||
| 347 | (log-error "While handling ~A: ~A" evt err) | ||
| 348 | (let ((msg (format nil "<code>~A</code> while handling ~&<pre>~A</pre>" | ||
| 349 | (escape-xml (format nil "~A" err)) | ||
| 350 | (escape-xml (format nil "~A" evt))))) | ||
| 351 | (send-message bot | ||
| 352 | :chat-id (config-dev-group (bot-config bot)) | ||
| 353 | :text msg | ||
| 354 | :parse-mode "HTML"))) | ||