spidercat.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. ;;
  2. ;; Copyright 2023, Jaidyn Levesque <jadedctrl@posteo.at>
  3. ;;
  4. ;; This program is free software: you can redistribute it and/or
  5. ;; modify it under the terms of the GNU General Public License as
  6. ;; published by the Free Software Foundation, either version 3 of
  7. ;; the License, or (at your option) any later version.
  8. ;;
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. ;;
  17. (import scheme
  18. (chicken file) (chicken io) (chicken sort) (chicken string)
  19. (chicken irregex) (chicken pretty-print)
  20. srfi-1 srfi-19
  21. (prefix chatdir chatdir:)
  22. (prefix intarweb intarweb:)
  23. (prefix spiffy spiffy:)
  24. (prefix uri-common uri:))
  25. ;; Santize text for placement in HTML.
  26. (define (html-encode-string text)
  27. (irregex-replace/all
  28. "{" (spiffy:htmlize text) "&#123;"))
  29. ;; Generate HTML from a template-file, substituting in variables as appropriate.
  30. (define (html-from-template template-file variables-alist)
  31. (let ([text (call-with-input-file template-file
  32. (lambda (in-port) (read-string #f in-port)))])
  33. (map (lambda (variable-pair)
  34. (set! text
  35. (irregex-replace/all
  36. (string-append "{{" (car variable-pair) "}}")
  37. text
  38. (cdr variable-pair))))
  39. variables-alist)
  40. text))
  41. ;; Generate HTML for a listing of all rooms the user's joined.
  42. (define (room-listing-html irc-dir)
  43. (html-from-template
  44. "templates/room-list.html"
  45. `(("LIST_ITEMS"
  46. . ,(reduce-right
  47. string-append
  48. ""
  49. (map (lambda (room)
  50. (room-list-item-html irc-dir room))
  51. (chatdir:channels irc-dir)))))))
  52. (define (room-list-item-html irc-dir room)
  53. (let* ([messages (channel-messages-sorted irc-dir room)]
  54. [last-message (if (null? messages)
  55. #f (last messages))]
  56. [message-text (if last-message
  57. (car last-message) "")]
  58. [message-sender (if last-message
  59. (or (alist-ref 'user.chat.sender
  60. (cdr last-message))
  61. "")
  62. "")]
  63. [message-time
  64. (if last-message
  65. (date->string (alist-ref 'user.chat.date
  66. (cdr last-message))
  67. "[~H:~M:~S]")
  68. "")])
  69. (html-from-template
  70. "templates/room-list-item.html"
  71. `(("ROOM_TITLE" . ,(html-encode-string room))
  72. ("ROOM_ID" . ,(uri:uri-encode-string room))
  73. ("LAST_MSG" . ,message-text)
  74. ("LAST_TIME" . ,message-time)
  75. ("LAST_MSG_SENDER" . ,message-sender)))))
  76. ;; “Send” a message to the given chatdir root, simply by creating a file.
  77. ;; That was easy!
  78. (define (send-message irc-dir channel message)
  79. (with-output-to-file
  80. (string-append irc-dir "/" channel "/.in/a")
  81. (lambda ()
  82. (write-string message))))
  83. ;; Returns all of a channel's messages — in alist format, with parsed datetimes.
  84. (define (channel-messages irc-dir channel)
  85. (map (lambda (msg-alist)
  86. (let ([date-str (alist-ref 'user.chat.date (cdr msg-alist))])
  87. (append
  88. (list (car msg-alist))
  89. (alist-update 'user.chat.date
  90. (string->date date-str "~Y-~m-~dT~H:~M:~S~z")
  91. (cdr msg-alist)))))
  92. (map (lambda (message)
  93. (chatdir:channel-message-get irc-dir channel message))
  94. (chatdir:channel-messages irc-dir channel))))
  95. ;; Returns all of a channel's messages, sorted in order of datetime.
  96. (define (channel-messages-sorted irc-dir channel)
  97. (sort
  98. (channel-messages irc-dir channel)
  99. (lambda (a b)
  100. (let ([date-a (alist-ref 'user.chat.date (cdr a))]
  101. [nano-a (alist-ref 'user.chat.date.nanoseconds (cdr a))]
  102. [date-b (alist-ref 'user.chat.date (cdr b))]
  103. [nano-b (alist-ref 'user.chat.date.nanoseconds (cdr b))])
  104. (cond [(and (date=? date-a date-b)
  105. nano-a nano-b)
  106. (> (string->number nano-b)
  107. (string->number nano-a))]
  108. [#t
  109. (date<? date-b date-a)])))))
  110. (define (channel-online-users irc-dir channel)
  111. (directory
  112. (string-append irc-dir "/" channel "/.users/online/")))
  113. (define (room-users-html irc-dir channel)
  114. (html-from-template
  115. "templates/room-user-list.html"
  116. `(("ROOM_TITLE" . ,(uri:uri-decode-string channel))
  117. ("LIST_ITEMS"
  118. . ,(reduce-right
  119. string-append ""
  120. (map (lambda (user)
  121. (room-users-item-html irc-dir channel user))
  122. (channel-online-users
  123. irc-dir
  124. (uri:uri-decode-string channel))))))))
  125. (define (room-users-item-html irc-dir channel user)
  126. (html-from-template
  127. "templates/room-user-list-item.html"
  128. `(("USER_NAME" . ,user))))
  129. (define (room-index-html irc-dir channel)
  130. (html-from-template
  131. "templates/room-index.html"
  132. `(("ROOM_TITLE" . ,(uri:uri-decode-string channel))
  133. ("ROOM_ID" . ,(uri:uri-encode-string channel)))))
  134. (define (room-send-html)
  135. (html-from-template "templates/room-send.html" '()))
  136. ;; Generate the HTML listing a room's chat messages.
  137. (define (room-messages-html irc-dir channel)
  138. (html-from-template
  139. "templates/room-messages.html"
  140. `(("ROOM_TITLE" . ,(uri:uri-decode-string channel))
  141. ("LIST_ITEMS"
  142. . ,(reduce-right
  143. string-append ""
  144. (map (lambda (message)
  145. (room-messages-item-html irc-dir channel message))
  146. (channel-messages-sorted
  147. irc-dir
  148. (uri:uri-decode-string channel))))))))
  149. ;; Generate the HTML for a specific message in a specific room.
  150. ;; Used to substitute {{LIST_ITEMS}} in the room-messages template.
  151. (define (room-messages-item-html irc-dir channel message)
  152. (if (and (list? message)
  153. (string? (car message)))
  154. (html-from-template
  155. "templates/room-messages-item.html"
  156. `(("MESSAGE_SENDER"
  157. . ,(html-encode-string
  158. (or (alist-ref 'user.chat.sender (cdr message)) "")))
  159. ("MESSAGE_DATE"
  160. . ,(html-encode-string
  161. (date->string
  162. (alist-ref 'user.chat.date (cdr message))
  163. "~Y-~m-~d")))
  164. ("MESSAGE_TIME"
  165. . ,(html-encode-string
  166. (date->string
  167. (alist-ref 'user.chat.date (cdr message))
  168. "~H:~M:~S")))
  169. ("MESSAGE_TEXT"
  170. . ,(html-encode-string
  171. (car message)))))
  172. ""))
  173. ;; Send response for a listing of joined rooms.
  174. (define (http-get-rooms-list irc-dir #!optional request path)
  175. (spiffy:send-response status: 'ok
  176. body: (room-listing-html irc-dir)))
  177. (define (http-get-room-dir irc-dir #!optional request path)
  178. (let* ([channel (third path)]
  179. [channel? (member channel (chatdir:channels irc-dir))]
  180. [sub-path (if (eq? (length path) 4)
  181. (fourth path) #f)])
  182. (cond
  183. [(not channel?)
  184. (spiffy:send-response code: 404
  185. body: "<h1>That's not a channel, smh!!</h1>")]
  186. [(equal? sub-path "users")
  187. (spiffy:send-response status: 'ok
  188. body: (room-users-html irc-dir channel))]
  189. [(equal? sub-path "messages")
  190. (spiffy:send-response status: 'ok
  191. body: (room-messages-html irc-dir channel))]
  192. [(equal? sub-path "send")
  193. (spiffy:send-response status: 'ok
  194. body: (room-send-html))]
  195. [(or (not sub-path) (string=? sub-path ""))
  196. (spiffy:send-response status: 'ok
  197. body: (room-index-html irc-dir channel))])))
  198. (define (http-post-room-dir irc-dir #!optional request path)
  199. (let* ([channel (third path)]
  200. [request-data (intarweb:read-urlencoded-request-data request 50000)])
  201. (if (alist-ref 'message request-data)
  202. (begin
  203. (send-message irc-dir channel (alist-ref 'message request-data))
  204. (sleep 1)))
  205. (http-get-room-dir irc-dir request (list '/ "room" channel "messages"))))
  206. ;; Send response for the / index.
  207. (define (http-get-root #!optional irc-dir request path)
  208. (spiffy:send-response status: 'ok body:
  209. (html-from-template "templates/index.html" '())))
  210. ;; Send a 404 response, with disappointed text.
  211. (define (http-404 #!optional irc-dir request path)
  212. (spiffy:send-response code: 404 body: "<h1>Sad!</h1>"))
  213. ;; Send the static style CSS.
  214. (define (http-get-style #!optional irc-dir request path)
  215. (spiffy:send-response
  216. status: 'ok
  217. body: (call-with-input-file "templates/style.css"
  218. (lambda (in-port) (read-string #f in-port)))
  219. headers: '((content-type "text/css"))))
  220. ;; An associative list of all GET handlers, to be used by assoc-by-path.
  221. (define http-get-handlers
  222. `(((/ "room") . ,http-get-rooms-list)
  223. ((/ "room" "*") . ,http-get-room-dir)
  224. ((/ "style.css") . ,http-get-style)
  225. ((/ "*") . ,http-404)
  226. (("*") . ,http-get-root)))
  227. ;; An associative list of POST handlers, to be used by assoc-by-path.
  228. (define http-post-handlers
  229. `(((/ "room" "*") . ,http-post-room-dir)))
  230. ;; Get a pair from an associative list based on the closest match to the
  231. ;; given path. Wild-cards acceptable! For example…
  232. ;; '(/ "dad" "mom") matches, in order of precedence:
  233. ;; '(/ "dad" "mom") '(/ "dad" "*") '(/ "*")
  234. (define (assoc-by-path path-list alist #!optional (top-level #t))
  235. (let* ([our-list=
  236. (lambda (a b)
  237. (list= equal? a b))]
  238. [path-list
  239. (if (eq? (string-length (last path-list)) 0)
  240. (drop-right path-list 1)
  241. path-list)]
  242. [parent-path (drop-right path-list 1)]
  243. [exact
  244. (and top-level
  245. (assoc path-list
  246. alist our-list=))]
  247. [inexact
  248. (assoc (append parent-path '("*"))
  249. alist our-list=)])
  250. (or exact
  251. inexact
  252. (and (not (null? parent-path))
  253. (assoc-by-path parent-path alist #f)))))
  254. ;; Handle all GET requests.
  255. (define (http-get irc-dir request continue)
  256. (let* ([path (uri:uri-path (intarweb:request-uri request))]
  257. [handler (assoc-by-path path http-get-handlers)])
  258. (if handler
  259. (apply (cdr handler) (list irc-dir request path))
  260. (continue))))
  261. ;; Handle all POST requests.
  262. (define (http-post irc-dir request continue)
  263. (let* ([path (uri:uri-path (intarweb:request-uri request))]
  264. [handler (assoc-by-path path http-post-handlers)])
  265. (if handler
  266. (apply (cdr handler) (list irc-dir request path))
  267. (continue))))
  268. ;; Creates a handler for all HTTP requests, with the given IRC dir.
  269. (define (make-http-handler irc-dir)
  270. (lambda (continue)
  271. (let* ([request (spiffy:current-request)]
  272. [request-type (intarweb:request-method request)])
  273. (cond [(eq? request-type 'GET)
  274. (http-get irc-dir request continue)]
  275. [(eq? request-type 'POST)
  276. (http-post irc-dir request continue)]
  277. [#t
  278. (intarweb:continue)]))))
  279. ;; Kick off the HTTP server.
  280. (define (start-server irc-dir)
  281. (spiffy:vhost-map `((".*" . ,(make-http-handler irc-dir))))
  282. (spiffy:root-path irc-dir)
  283. (spiffy:start-server port: 8080))
  284. ;; Check if a `list` begins with the elements of another list.
  285. (define (starts-with? list list-start #!optional (= equal?))
  286. (list= =
  287. (take list (length list-start))
  288. list-start))
  289. (start-server "/home/jaidyn/Chat/IRC/leagueh/")