Plaster

common-lisp
(in-package :cl-discord-api) (defparameter *ws-url* "wss://gateway.discord.gg/?v=6&encoding=json&compression=nil") ;;;https://discordapp.com/developers/docs/topics/opcodes-and-status-codes ;;;gateway codes (defconstant +dispatch+ 0) ;;dispatches an event (defconstant +heartbeat+ 1) ;;used for ping checking (defconstant +identify+ 2) ;;used for lclient handshake (defconstant +status-update+ 3) ;;used to update the client status (defconstant +voice-status-update+ 4) ;;used to join/move/leave voice channels (defconstant +resume+ 6) ;;used to resume a closed connection (defconstant +reconnect+ 7) ;;used to tell clients to reconnect to the gateway (defconstant +request-guild-members+ 8) ;;used to request guild members (defconstant +invalid-session+ 9) ;;used to notify client they have an invalid session id (defconstant +hello+ 10) ;;sent immediately after connecting, contains heartbeat and server debug information (defconstant +heartbeat-ack+ 11) ;;sent immediately following a client heartbeat that was received (defparameter *unix-epoch* (encode-universal-time 0 0 0 1 1 1970 0) "Seconds since 1970") (defun since-unix-epoch () (- (get-universal-time) *unix-epoch*)) (defclass bot () ((connection :accessor connection :initarg :connection) (token :reader token :initarg :token) (heartbeat :accessor heartbeat) (afk-since :accessor afk-since) (id :accessor id) (session-id :accessor session-id) (last-sequence :initarg :last-sequence :accessor last-sequence ) (ack-flag :accessor ack-flag :initarg :ack-flag) (disconnectedp :accessor disconnectedp :initarg :disconnectedp)) (:documentation "The main class for the bot")) (defun make-bot (token) (make-instance 'bot :token token :connection (wsd:make-client *ws-url*) :last-sequence nil :disconnectedp nil :ack-flag t)) ;;;https://discordapp.com/developers/docs/topics/gateway#payloads ;;;these functions get specific parts of the payload object as defined in the link above (defun get-key-val (key alist) (declare (list alist)) (cdr (assoc key alist :test #'equal))) (defun get-op (json-alist) (get-key-val :OP json-alist)) (defun get-data (json-alist) (get-key-val :D json-alist)) (defun get-sequence (json-alist) (get-key-val :S json-alist)) (defun get-event (json-alist) (get-key-val :T json-alist)) (defun get-heartbeat-from-decoded-hello (hello) (cdr (second (assoc ':D hello)))) (defun connect (bot) (declare (bot bot)) ;; (sleep 0.1) (wsd:start-connection (connection bot)) (wsd:on :open (connection bot) (lambda () (format t "Connected.~%"))) (wsd:on :message (connection bot) (lambda (message) (let ((decoded (cl-json:decode-json-from-source message))) (format *standard-output* "--RECEIVED~% event ~A~% op ~A~% sequence ~A~% data ~A~%--RECEIVED~%" (get-event decoded) (get-op decoded) (get-sequence decoded) (get-data decoded)) ;; (format *standard-output* "~%~% Complete ~%~% ~A~%" decoded) ; (sleep 0.2) (do-based-on-server-sent-op-codes bot decoded)))) (wsd:on :error (connection bot) (lambda (error) (format t "Got an error: ~S~%" error))) (wsd:on :close (connection bot) (lambda (&key code reason) (format t "Closed because '~A' (Code=~A)~%" reason code)))) ;(wsd:send *client* "Hi") (defun do-based-on-server-sent-op-codes (bot message) "Takes in a decoded message (so an alist) and then performs the correct operation based on the OP code field" (let ((op (the integer (get-op message)))) ;(format *standard-output* "OP IS ~A~%" op) (case op ;;this is technically undefined behaviour but YOLO (#.+dispatch+ (handle-dispatched-event bot message)) (#.+heartbeat+ (send-heartbeat bot)) (#.+reconnect+ "reconnect") (#.+invalid-session+ (handle-invalid-session bot message)) (#.+hello+ (handle-hello bot message)) (#.+heartbeat-ack+ (handle-ack bot)) (otherwise (error "OP code received from server ~A is not a valid OP code" op))))) (defun handle-ack (bot) (setf (ack-flag bot) t)) (defun handle-hello (bot message) "A function that handles the +hello+ that is sent by the server. This function will set the heartbeat time, start the heartbeat function and then send the identify payload" (let ((heart (get-heartbeat-from-decoded-hello message))) ;(format *standard-output* "OP HELLO RECEIVED ~%") (setf (heartbeat bot) heart) (bt:make-thread (lambda () (handle-heartbeat bot)) :name (format nil "~A:heartbeat" (token bot))) (sleep 0.1) (format *standard-output* "DISCONNECTEDP VAL ~A~%" (disconnectedp bot)) (if (not (disconnectedp bot)) (send-identify bot) (progn (send-resume bot) (setf (disconnectedp bot) nil))))) (defun handle-invalid-session (bot message) "a function that handles the +invalid-session+ that is sent by the server." (let ((d (get-key-val ':D message))) (format *standard-output* "D is ~A~%" d) (format *standard-output* "INVALID SESSION ~A BE RESUMED~%" (if d "CAN" (prog1 "CANNOT" (wsd:close-connection (connection bot))))))) ;;;this above needs work, needs to resume or just end connection and then reconnect (defun make-status (bot status game afk) "Creates an update status payload" ;; https://discordapp.com/developers/docs/topics/gateway#update-status (let ((since (if (equal afk :true) (if (afk-since bot) (afk-since bot) (setf (afk-since bot) (since-unix-epoch))) :false))) (list (cons "since" since) (cons "game" game) (cons "afk" afk) (cons "status" status)))) (defun send (bot &key op data) ;;sends op and data to connection on bot (declare (integer op)) (declare ((or list integer) data)) (let* ((to-send (list (cons :op op) (cons :d data))) (done (cl-json:encode-json-alist-to-string to-send))) (declare (list to-send)) (the integer op) (the list to-send) (the (simple-array character) done) (format *standard-output* "--SENDING~% Sending OP: ~A~% DATA: ~S~%--SENDING~%" op data) ;(cl-json:encode-json-alist-to-string to-send))) (wsd:send-text (connection bot) done))) (defun send-identify (bot) "sends an identify op code to the server" ;;https://discordapp.com/developers/docs/topics/gateway#identifying-example-gateway-identify (let* ((token (token bot)) (data (list (cons :token (format nil "Bot ~A" token)) '(:properties (:$os . "linux") (:$browser . "cl-discord-api") (:$device . "cl-discord-api-bot")) '(:compress . :false) '(:large-threshold . 250) '(:shard . (0 1)) (cons :presence (make-status bot "online" :false :false))))) (send bot :op +identify+ :data data))) (defun send-resume (bot) "sends an op code 6 resume" ;;https://discordapp.com/developers/docs/topics/gateway#resume (let* ((tk (token bot)) (si (session-id bot)) (seq (last-sequence bot)) (data (list (cons :token tk) (cons :session-id si) (cons :seq seq)))) (send bot :op +resume+ :data data))) (defun send-heartbeat (bot) "Sends an op code 1 heartbeat" ;https://discordapp.com/developers/docs/topics/gateway#heartbeat (send bot :op +heartbeat+ :data (last-sequence bot))) (defun handle-heartbeat (bot) "The function that is used to send the heartbeats" (loop :do (sleep (/ (heartbeat bot) 1000)) :if (ack-flag bot) :do (setf (ack-flag bot) nil) (send-heartbeat bot) :else :do (wsd:close-connection (connection bot) "disconnected" "1005") (setf (disconnectedp bot) t);;hopefully by quitting and then (connect bot);;setting disconnectedp to t when we connect next (return "done")));; a resume is sent instead of an identify hopefully xD (defun test () (let ((bot (make-bot *bot-token*))) (sleep 0.5) (connect bot) bot)) (defun endtest (bot) (wsd:close-connection (connection bot) "testing oof" "1000") (let ((thread-name (format nil "~A:heartbeat" (token bot)))) (kill-thread thread-name))) (defun kill-thread (name) "Kills the thread name" (mapcar (lambda (thread) (when (string= name (bt:thread-name thread)) (bt:destroy-thread thread))) (bt:all-threads))) (defun handle-dispatched-event (bot message) "Handles the events that are received from the server" (declare (list message)) (let* ((event (get-event message)) (seq (get-sequence message)) (event-key (intern event :KEYWORD))) ; (format t "~A"(type-of event)) (case event-key;; turn from string to keyword (:READY (let* ((data (get-data message)) (id (get-key-val :id data)) (session-id (get-key-val :session--id data))) (setf (session-id bot) session-id (id bot) id (last-sequence bot) seq))) (:GUILD_CREATE (setf *guild-create-test* (get-data message))) (:MESSAGE_CREATE (execute-hooks event-key (list message))) (otherwise (progn (setf (last-sequence bot) seq) (format *standard-output* "~%Event ~A is not implemented yet~%" event)))))) ; (format *standard-output* "~%message: ~A~%" message))))))