]> NullRing Git Server - nullerbot.git/commitdiff
Initial Commit
authorAndrei <andreisva2023@gmail.com>
Sat, 14 Feb 2026 04:57:05 +0000 (20:57 -0800)
committerAndrei <andreisva2023@gmail.com>
Sat, 14 Feb 2026 04:57:05 +0000 (20:57 -0800)
.gitignore [new file with mode: 0644]
README.org [new file with mode: 0644]
nullbot.asd [new file with mode: 0644]
src/api.lisp [new file with mode: 0644]
src/main.lisp [new file with mode: 0644]
tests/main.lisp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b9fa3c1
--- /dev/null
@@ -0,0 +1,9 @@
+*.abcl
+*.fasl
+*.dx32fsl
+*.dx64fsl
+*.lx32fsl
+*.lx64fsl
+*.x86f
+*~
+.#*
diff --git a/README.org b/README.org
new file mode 100644 (file)
index 0000000..b89e75e
--- /dev/null
@@ -0,0 +1,5 @@
+* Matrix-Bot 
+
+** Usage
+
+** Installation
diff --git a/nullbot.asd b/nullbot.asd
new file mode 100644 (file)
index 0000000..033d589
--- /dev/null
@@ -0,0 +1,28 @@
+(defsystem "nullbot"
+  :version "0.0.1"
+  :author "Andrei Șova"
+  :license "MIT"
+  :depends-on (:com.inuoe.jzon
+               :dexador
+               :bordeaux-threads
+               :cl-hash-util
+               :quri
+               :flexi-streams
+               :split-sequence)
+  :components ((:module "src"
+                :components
+                ((:file "api")
+                 (:file "main" :depends-on ("api")))))
+  :description "A bot for nullring on matrix"
+  :in-order-to ((test-op (test-op "nullbot/tests"))))
+
+(defsystem "nullbot/tests"
+  :author "Andrei Șova"
+  :license "MIT"
+  :depends-on (:nullbot
+               :rove)
+  :components ((:module "tests"
+                :components
+                ((:file "main"))))
+  :description "Test system for nullbot"
+  :perform (test-op (op c) (symbol-call :rove :run c)))
diff --git a/src/api.lisp b/src/api.lisp
new file mode 100644 (file)
index 0000000..fb4cc20
--- /dev/null
@@ -0,0 +1,122 @@
+;; readers beware: this is currently a very barebones library
+
+(defpackage nullbot/matrix-api
+  (:use #:cl
+        #:cl-hash-util)
+  (:local-nicknames
+   (:jzon :com.inuoe.jzon)
+   (:fs :flexi-streams))
+  (:export
+   #:matrix-user
+   #:homeserver
+   #:name
+   #:listening
+   #:token
+   #:lock
+   #:matrix-bot
+   #:sendmsg
+   #:on-event
+   #:start
+   #:stop))
+(in-package #:nullbot/matrix-api)
+
+(defclass matrix-user ()
+  ((homeserver
+    :type string
+    :initarg :homeserver
+    :initform "matrix.org"
+    :reader homeserver)
+   (name
+    :type string
+    :initarg :name
+    :initform "matrix-user"
+    :reader name)
+   (listening
+    :type boolean
+    :initform nil
+    :accessor listening)
+   (token
+    :type string
+    :initarg :token
+    :initform ""
+    :reader token)
+   (lock
+    :type bt2:lock
+    :initform (bt2:make-lock)
+    :reader lock)))
+
+(defclass matrix-bot (matrix-user) ()
+  (:default-initargs :name "matrix-bot"))
+
+(defgeneric request (obj endpoint &rest rest)
+  (:method ((obj matrix-user) endpoint &rest rest &aux (headers))
+    (declare (type string endpoint))
+
+    (when (>= (length rest) 3) (setf headers (car (last rest))))
+    (bt2:with-lock-held ((lock obj))
+      (push `("Authorization" . ,(format nil "Bearer ~a" (token obj))) headers))
+    (jzon:parse (dexador:request (format nil "https://~a/_matrix/client/v3~a"
+                                         (homeserver obj) endpoint)
+                                 :headers headers
+                                 :method (car rest)
+                                 :content (jzon:stringify (cadr rest))
+                                 :verbose nil))))
+
+(defgeneric on-event (obj event room-id)
+  (:method ((obj matrix-user) event room-id)
+    (format t "Event Received: ~a~%" event)))
+
+(defun randint (start end)
+  (+ start (random (+ 1 (- end start)))))
+
+(defun rand-string (len &aux (arr (make-array len)))
+  (loop for i from 0 below len do
+    (setf (aref arr i) (randint 65 90)))
+  (fs:octets-to-string arr))
+
+(defgeneric sendmsg (obj room-id content)
+  (:method ((obj matrix-user) room-id content
+            &aux
+              (msg (make-hash-table :test #'equal))
+              (encoded-room-id (quri:url-encode room-id))
+              (unique-str (rand-string 20)))
+    (setf (gethash "msgtype" msg) "m.text")
+    (setf (gethash "body" msg) content)
+    (request obj (format nil "/rooms/~a/send/m.room.message/~a"
+                         encoded-room-id
+                         unique-str)
+             :put
+             msg
+             '(("Content-Type" . "application/json")))))
+
+(defgeneric get-events (obj rooms-join room-id)
+  (:method ((obj matrix-user) rooms-join room-id
+            &aux
+              (room-table (gethash room-id rooms-join))
+              (events
+               (hash-get room-table '("timeline" "events"))))
+    (when events
+      (loop for event across events do
+        (on-event obj event room-id)))))
+
+(defgeneric start (obj)
+  (:method-combination progn)
+  (:method ((obj matrix-user))
+    (setf (listening obj) t)
+    (bt2:make-thread (lambda (&aux
+                                (since)
+                                (sync-route "/sync?timeout=30000"))
+                       (loop while (bt2:with-lock-held ((lock obj)) (listening obj)) do
+                         (when since
+                           (setf sync-route (format nil "/sync?timeout=30000&since=~a" since)))
+                         (let* ((response (request obj sync-route :get))
+                                (rooms-join (hash-get response '("rooms" "join"))))
+                           (when rooms-join (loop for room-id being each hash-key of rooms-join
+                                                  do (when since (get-events obj rooms-join room-id))))
+                           (setf since (gethash "next_batch" response))))
+                       (format t "Shutting down...~%"))
+                     :name (format nil "~a Poll Thread" (name obj)))))
+
+(defgeneric stop (obj)
+  (:method ((obj matrix-user))
+    (bt2:with-lock-held ((lock obj)) (setf (listening obj) nil))))
diff --git a/src/main.lisp b/src/main.lisp
new file mode 100644 (file)
index 0000000..ee37ddc
--- /dev/null
@@ -0,0 +1,37 @@
+(defpackage nullbot
+  (:use #:cl
+        #:cl-hash-util)
+  (:local-nicknames
+   (:jzon :com.inuoe.jzon)
+   (:mapi :nullbot/matrix-api)
+   (:sseq :split-sequence))
+  (:export
+   #:start))
+(in-package #:nullbot)
+
+(defclass nullbot (mapi:matrix-bot) ())
+(defparameter *bot* (make-instance 'nullbot
+                                   :token (uiop:getenv "NULLBOT_TOKEN")
+                                   :homeserver "matrix.nullring.xyz"))
+
+(defun process-roommsg
+    (content room-id sender
+     &aux
+       (msgtype (gethash "msgtype" content))
+       (body (gethash "body" content))
+       (split-body (sseq:split-sequence #\Space body))
+       (command (car split-body)))
+  (format t "processing msg~%")
+  (when (and (> (length body) 0) (equal (aref (car split-body) 0) #\$))
+    (cond
+      ((string= command "$help")
+       (mapi:sendmsg *bot* room-id "Unlike some other bots, I'm nice :3")))))
+
+(defmethod mapi:on-event
+    ((obj nullbot) event room-id
+     &aux
+       (msgtype (gethash "type" event))
+       (sender (gethash "sender" event)))
+  (cond
+    ((string= msgtype "m.room.message")
+     (process-roommsg (gethash "content" event) room-id sender))))
diff --git a/tests/main.lisp b/tests/main.lisp
new file mode 100644 (file)
index 0000000..84d23a9
--- /dev/null
@@ -0,0 +1,10 @@
+(defpackage matrix-bot/tests/main
+  (:use :cl
+        :matrix-bot
+        :rove))
+(in-package :matrix-bot/tests/main)
+
+;; NOTE: To run this test file, execute `(asdf:test-system :matrix-bot)' in your Lisp.
+(deftest test-target-1
+  (testing "should (= 1 1) to be true"
+    (ok (= 1 1))))