Initial commit for git.lain.church
This commit is contained in:
commit
2a54fc3e0d
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
bin/
|
||||
*.bin
|
||||
*.fasl
|
||||
*.db
|
32
live-chat-cgi.lisp
Normal file
32
live-chat-cgi.lisp
Normal file
@ -0,0 +1,32 @@
|
||||
(in-package #:live-chat-cgi)
|
||||
|
||||
(defun print-http-header ()
|
||||
"Print standard HTTP headers for CGI."
|
||||
(format t "Content-Type: text/html~%~%"))
|
||||
|
||||
(defun cgi-handler ()
|
||||
"Handle CGI requests and dispatch them to the appropriate logic."
|
||||
(let ((request-method (string-downcase (or (getenv "REQUEST_METHOD") ""))))
|
||||
(print-http-header)
|
||||
(cond
|
||||
((string= request-method "get")
|
||||
(let ((path (getenv "PATH_INFO")))
|
||||
(cond
|
||||
((string= path "/chat-messages")
|
||||
(render-chat-messages))
|
||||
((string= path "/")
|
||||
(render-chat-ui))
|
||||
(t
|
||||
(format t "Not Found")))))
|
||||
((string= request-method "post")
|
||||
;; Read the POST data
|
||||
(let ((input (read-line *standard-input*)))
|
||||
(when (and input (not (string= input "")))
|
||||
(let ((params (split-sequence:split-sequence #\& input)))
|
||||
(dolist (param params)
|
||||
(let* ((pair (split-sequence:split-sequence #\= param))
|
||||
(key (first pair))
|
||||
(value (second pair)))
|
||||
(when (string= key "message")
|
||||
(handle-post-message value))))))
|
||||
(render-chat-messages))))))
|
70
live-chat-core.lisp
Normal file
70
live-chat-core.lisp
Normal file
@ -0,0 +1,70 @@
|
||||
(in-package #:cl-user)
|
||||
|
||||
(in-package #:live-chat-core)
|
||||
|
||||
;; (defvar *messages* (make-array 0 :adjustable t :fill-pointer t)
|
||||
;; "Array to store chat messages.")
|
||||
|
||||
(defun handle-post-message (message)
|
||||
"Handle a new message being posted to the chat."
|
||||
(insert-message message)
|
||||
;; (vector-push-extend message *messages*)
|
||||
(format nil "Message received: ~a" message))
|
||||
|
||||
(defun render-chat-messages ()
|
||||
"Render the list of chat messages as HTML."
|
||||
(let ((messages (fetch-messages)))
|
||||
(if messages
|
||||
(cl-who:with-html-output-to-string (*standard-output* nil :indent t)
|
||||
(:h3 :class "title is-3" "Chat Messages")
|
||||
(loop for msg in messages
|
||||
do (cl-who:htm
|
||||
(:div :class "box" (cl-who:str msg)))))
|
||||
"")))
|
||||
|
||||
(defun render-chat-ui ()
|
||||
"Render the main chat page with HTMX integration."
|
||||
(cl-who:with-html-output-to-string (*standard-output* nil :prologue t :indent t)
|
||||
(:html :lang "en"
|
||||
(:head
|
||||
(:meta :charset "utf-8")
|
||||
(:meta :name "viewport" :content "width=device-width, initial-scale=1")
|
||||
(:title "Live Chat")
|
||||
(:link :rel "stylesheet"
|
||||
:href "https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.2/css/bulma.min.css"
|
||||
:integrity "sha512-RpeJZX3aH5oZN3U3JhE7Sd+HG8XQsqmP3clIbu4G28p668yNsRNj3zMASKe1ATjl/W80wuEtCx2dFA8xaebG5w=="
|
||||
:crossorigin "anonymous"
|
||||
:referrerpolicy "no-referrer")
|
||||
(:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js"
|
||||
:integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw=="
|
||||
:crossorigin "anonymous"
|
||||
:referrerpolicy "no-referrer"))
|
||||
(:body
|
||||
(:section :class "section"
|
||||
(:div :class "container"
|
||||
(:h1 :class "title" "Live Chat")
|
||||
|
||||
;; Form for sending messages
|
||||
(:form :id "post-message-form"
|
||||
:name "post-message-form"
|
||||
:hx-post "/post-message"
|
||||
:hx-swap "innerHTML"
|
||||
:hx-target "#chat-messages"
|
||||
(:div :class "field has-addons"
|
||||
(:div :class "control is-expanded"
|
||||
(:input :class "input is-expanded"
|
||||
:form "post-message-form"
|
||||
:placeholder "Ender your message..."
|
||||
:type "text" :name "message"))
|
||||
(:div :class "control"
|
||||
(:button :class "button is-link is-light"
|
||||
:form "post-message-form"
|
||||
:type "submit" "Send"))))))
|
||||
|
||||
;; Chat messages will be updated by HTMX via GET to /chat-messages
|
||||
(:section :class "section"
|
||||
(:div :class "container"
|
||||
:id "chat-messages"
|
||||
:hx-get "/chat-messages"
|
||||
:hx-trigger "every 2s"
|
||||
(cl-who:str (render-chat-messages))))))))
|
50
live-chat-db.lisp
Normal file
50
live-chat-db.lisp
Normal file
@ -0,0 +1,50 @@
|
||||
(in-package #:live-chat-db)
|
||||
|
||||
(defvar *db* nil
|
||||
"Database connection object.")
|
||||
|
||||
(defun open-database ()
|
||||
"Open the SQLite database."
|
||||
(setf *db* (dbi:connect-cached
|
||||
:sqlite3 :database-name
|
||||
(asdf:system-relative-pathname :live-chat "chat.db"))))
|
||||
|
||||
(defun close-database ()
|
||||
"Close the SQLite database."
|
||||
(when *db* (dbi:disconnect *db*)))
|
||||
|
||||
(defun create-messages-table ()
|
||||
"Create the messages table if it doesn't exist."
|
||||
(open-database)
|
||||
(let ((query (dbi:prepare *db*
|
||||
"CREATE TABLE IF NOT EXISTS messages
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content TEXT NOT NULL)")))
|
||||
(dbi:execute query (list)))
|
||||
(close-database))
|
||||
|
||||
(defun insert-message (message)
|
||||
"Insert a new message into the database."
|
||||
(open-database)
|
||||
(let ((query (dbi:prepare *db*
|
||||
"INSERT INTO messages (content) VALUES (?)")))
|
||||
(dbi:execute query (list message))
|
||||
(close-database)
|
||||
message))
|
||||
|
||||
(defun fetch-messages ()
|
||||
"Fetch all messages from the database."
|
||||
(open-database)
|
||||
(let* ((query (dbi:prepare *db*
|
||||
"SELECT content FROM messages ORDER BY id"))
|
||||
(query (dbi:execute query (list)))
|
||||
(results (loop for row = (dbi:fetch query)
|
||||
while row
|
||||
collect (getf row :|content|))))
|
||||
(close-database)
|
||||
(reverse results)))
|
||||
|
||||
(defun clear-messages ()
|
||||
"Clear all messages from the database."
|
||||
(let ((query (dbi:prepare *db* "DELETE FROM messages")))
|
||||
(dbi:execute query (list))))
|
43
live-chat-hunchentoot.lisp
Normal file
43
live-chat-hunchentoot.lisp
Normal file
@ -0,0 +1,43 @@
|
||||
(in-package #:live-chat-hunchentoot)
|
||||
|
||||
(defvar *server* nil
|
||||
"The server acceptor.")
|
||||
|
||||
(hunchentoot:define-easy-handler (chat-index :uri "/") ()
|
||||
"Handle GET requests to / and render the chat UI."
|
||||
(render-chat-ui))
|
||||
|
||||
(hunchentoot:define-easy-handler (chat-handler :uri "/chat-messages") ()
|
||||
"Handle GET requests to /chat-messages and render the chat UI."
|
||||
(render-chat-messages))
|
||||
|
||||
(hunchentoot:define-easy-handler (chat-post-handler :uri "/post-message" :default-request-type :post) ()
|
||||
"Handle POST requests to /post-message and process the message."
|
||||
(let ((message (hunchentoot:post-parameter "message")))
|
||||
(when (and message (not (string= message "")))
|
||||
(handle-post-message message)))
|
||||
(render-chat-messages))
|
||||
|
||||
(defun start-chat-server (&optional (address "localhost") (port 8080))
|
||||
"Start the Hunchentoot chat server."
|
||||
(setf (cl-who:html-mode) :html5
|
||||
*server* (make-instance 'hunchentoot:easy-acceptor
|
||||
:address address
|
||||
:port port))
|
||||
(hunchentoot:start *server*)
|
||||
(format t "Chat server started on port 8080~%")
|
||||
(wait-for-hunchentoot-listener "hunchentoot-listener-"))
|
||||
|
||||
;; https://stackoverflow.com/a/30424968
|
||||
(defun wait-for-hunchentoot-listener (name)
|
||||
(bt2:join-thread
|
||||
(find-if
|
||||
(lambda (th)
|
||||
(prefixp name (bt2:thread-name th)))
|
||||
(bt2:all-threads))))
|
||||
|
||||
;; https://github.com/brown/base?tab=readme-ov-file#prefixp-prefix-sequence-key-test-eql
|
||||
(defun prefixp (prefix sequence &key (test #'eql))
|
||||
"Does PREFIX match a prefix of SEQUENCE?"
|
||||
(let ((mismatch (mismatch prefix sequence :test test)))
|
||||
(or (null mismatch) (= mismatch (length prefix)))))
|
5
live-chat-main.lisp
Normal file
5
live-chat-main.lisp
Normal file
@ -0,0 +1,5 @@
|
||||
(in-package #:live-chat)
|
||||
|
||||
(defun main ()
|
||||
(create-messages-table)
|
||||
(start-chat-server))
|
21
live-chat.asd
Normal file
21
live-chat.asd
Normal file
@ -0,0 +1,21 @@
|
||||
;;;; live-chat.asd
|
||||
|
||||
(asdf:defsystem #:live-chat
|
||||
:description "Twitch.TV-like live chat on the web. Uses HTMX to poll for messages."
|
||||
:author "Bubblegumdrop <staticsunn@gmail.com>"
|
||||
:license "WTFPL 2+"
|
||||
:version "0.1.0"
|
||||
:defsystem-depends-on (:deploy) ;; so you need to quickload deploy sometime before.
|
||||
:build-operation "deploy-op" ;; instead of program-op for asdf:make
|
||||
:build-pathname "live-chat"
|
||||
:entry-point "live-chat::main"
|
||||
:depends-on (#:bordeaux-threads
|
||||
#:cl-dbi
|
||||
#:cl-who
|
||||
#:hunchentoot)
|
||||
:components ((:file "package")
|
||||
(:file "live-chat-db")
|
||||
(:file "live-chat-core")
|
||||
(:file "live-chat-hunchentoot")
|
||||
(:file "live-chat-cgi")
|
||||
(:file "live-chat-main")))
|
22
makefile
Normal file
22
makefile
Normal file
@ -0,0 +1,22 @@
|
||||
PROG := live-chat
|
||||
|
||||
SRC := $(wildcard *.asd) $(wildcard *.lisp)
|
||||
BIN := $(PROG).bin
|
||||
|
||||
all: install
|
||||
|
||||
install: $(BIN)
|
||||
|
||||
bin/$(PROG): $(SRC)
|
||||
sbcl --load $< \
|
||||
--eval "(load \"live-chat.asd\")" \
|
||||
--eval "(asdf:load-system :live-chat)" \
|
||||
--eval "(asdf:make 'live-chat)"
|
||||
|
||||
$(BIN): bin/$(PROG)
|
||||
mv $< $@
|
||||
|
||||
clean:
|
||||
$(RM) -rf *~ bin $(BIN)
|
||||
|
||||
.PHONY: all install
|
47
package.lisp
Normal file
47
package.lisp
Normal file
@ -0,0 +1,47 @@
|
||||
(in-package #:cl-user)
|
||||
|
||||
(defpackage #:live-chat-db
|
||||
(:use #:cl)
|
||||
(:local-nicknames (#:cl-dbi #:dbi))
|
||||
(:export #:*db*
|
||||
#:open-database
|
||||
#:close-database
|
||||
#:create-messages-table
|
||||
#:insert-message
|
||||
#:fetch-messages))
|
||||
|
||||
(defpackage #:live-chat-core
|
||||
(:use #:cl)
|
||||
(:local-nicknames (#:cl-who #:cl-who))
|
||||
(:import-from #:live-chat-db
|
||||
#:insert-message
|
||||
#:fetch-messages)
|
||||
(:export #:render-chat-messages
|
||||
#:render-chat-ui
|
||||
#:handle-post-message
|
||||
#:*messages*))
|
||||
|
||||
(defpackage #:live-chat-cgi
|
||||
(:use #:cl)
|
||||
(:import-from #:live-chat-core)
|
||||
(:export #:cgi-handler))
|
||||
|
||||
(defpackage #:live-chat-hunchentoot
|
||||
(:use #:cl)
|
||||
(:import-from #:live-chat-core
|
||||
#:handle-post-message
|
||||
#:render-chat-messages
|
||||
#:render-chat-ui)
|
||||
(:local-nicknames (#:cl-who #:cl-who)
|
||||
(#:hunchentoot #:hunchentoot))
|
||||
(:export #:start-chat-server))
|
||||
|
||||
(uiop:define-package #:live-chat
|
||||
(:use #:cl)
|
||||
(:use-reexport #:live-chat-core
|
||||
#:live-chat-cgi
|
||||
#:live-chat-db
|
||||
#:live-chat-hunchentoot))
|
||||
|
||||
(in-package #:live-chat)
|
||||
|
3
readme.org
Normal file
3
readme.org
Normal file
@ -0,0 +1,3 @@
|
||||
* What It Is
|
||||
|
||||
Twitch.TV-like live chat on the web. Uses HTMX to poll for messages. No database support (yet).
|
Loading…
Reference in New Issue
Block a user