(in-package :cl-user) (defpackage :birthday-boy (:use :cl) (:export main*)) (in-package :birthday-boy) (load (sb-ext:posix-getenv "ASDF")) (asdf:load-systems :dexador :local-time :jonathan :cl-yaml :alexandria) (defvar *birthdays* nil "Hashtable of birthdays.") (defvar *config* nil "Hashtable of needed configuration for the bot.") (defun now-string () (local-time:format-timestring nil (local-time:now) :format local-time:+iso-8601-format+)) ;; 2008-03-01T19:42:34.608506+01:00 (defvar *txn-counter* 0) (defun make-txn-id () (format nil "~A~A" (now-string) (incf *txn-counter*))) (defun make-json-request (method uri json-content &key auth) "Generic HTTP request function using dex:request with JSON content" (let ((headers (if auth `((:|Authorization| . ,(format nil "Bearer ~A" (gethash "bot_access_token" *config*))) (:content-type . "application/json")) '((:content-type . "application/json"))))) (dex:request uri :method method :headers headers :content (jojo:to-json json-content)))) ;; Replace your existing functions with these: (defun put-json-content (uri json-content) (make-json-request 'put uri json-content)) (defun put-json-content-auth (uri json-content) (make-json-request 'put uri json-content :auth t)) (defun post-json-content (uri json-content) (make-json-request 'post uri json-content)) (defun post-json-content-auth (uri json-content) (make-json-request 'post uri json-content :auth t)) (defun make-homeserver-endpoint (endpoint) (format nil "~a~a" (gethash "homeserver_url" *config*) endpoint)) (defun get-access-token () (let ((res (jojo:parse (post-json-content (make-homeserver-endpoint "/_matrix/client/v3/login" ) (list :|identifier| (list :|type| "m.id.user" :|user| (gethash "bot_name" *config*)) :|type| "m.login.password" :|password| (gethash "bot_pw" *config*)))))) (values (getf res :|access_token|) (getf res :|device_id|)))) (defun message (msg &optional (room-id "GENERAL2_ROOM_ID")) (put-json-content-auth (make-homeserver-endpoint (format nil "/_matrix/client/v3/rooms/~a/send/m.room.message/~a" (gethash room-id *config*) (make-txn-id))) (list :|msgtype| "m.text" :|body| msg))) (defun is-today (tstamp) "Check just the day and month of today and tstamp and return true if its today" (let ((today (local-time:now))) (if (and (eql (local-time:timestamp-day today) (local-time:timestamp-day tstamp)) (eql (local-time:timestamp-month today) (local-time:timestamp-month tstamp))) t nil))) (defun is-today* (day month) (is-today (local-time:encode-timestamp 0 0 0 0 day month 1900))) (defun collect-bdays () (loop for person in *birthdays* when (is-today* (gethash "day" person) (gethash "month" person)) collect person)) (defun make-bday-message (person) (format nil (alexandria:random-elt (gethash "bot_message_template" *config*)) person)) (defun seed-random-state () "Seed the random state with current time for true randomness between runs" (setf *random-state* (make-random-state t))) (defun main () (dolist (person (collect-bdays)) (message (make-bday-message (gethash "person" person)) "ROOM_ID"))) (defun check-required-env-vars () (let ((required-vars '("bot_name" "bot_pw" "bot_access_token" "homeserver_url" "room_id" "bot_message_template")) (missing-vars '())) (dolist (var required-vars) (unless (gethash var *config*) (push var missing-vars))) (when missing-vars (format t "ERROR: Missing required environment variables:~%") (dolist (var missing-vars) (format t " - ~A~%" var)) (sb-ext:exit :code 1)) t)) (defun main* () (let* ((args sb-ext:*posix-argv*) (config-file (or (second args) "./config.yml"))) (format t "Using config file: ~A~%" config-file) (unwind-protect (let* ((base-config (cl-yaml:parse (pathname config-file))) (*birthdays* (gethash "birthdays" base-config)) (*config* (gethash "config" base-config))) (seed-random-state) (check-required-env-vars) (main)) (sb-ext:exit :code 0))))