chatdir.scm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. ;; Copyright 2023, Jaidyn Levesque <jadedctrl@posteo.at>
  2. ;;
  3. ;; This program is free software: you can redistribute it and/or
  4. ;; modify it under the terms of the GNU General Public License as
  5. ;; published by the Free Software Foundation, either version 3 of
  6. ;; the License, or (at your option) any later version.
  7. ;;
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. ;;
  16. (module chatdir
  17. (channels channel-add! channel-cleanup!
  18. channel-metadata-set! channel-metadata-get channel-metadata-get* channel-metadata
  19. user-add! user-file-set! user-file-get
  20. user-enable-state! user-disable-state! user-toggle-states!
  21. channel-users channel-user-add! channel-user-file-set! channel-user-file-get
  22. channel-user-disable-state! channel-user-enable-state! channel-user-toggle-states!
  23. channel-message-add! channel-messages channel-message-get
  24. channel-messages-by-xattr channel-messages-by-sender
  25. channel-messages-by-date channel-messages-by-date* channel-messages-by-date-range
  26. )
  27. (import scheme
  28. (chicken file) (chicken file posix) (chicken pathname) (chicken port)
  29. (chicken io) (chicken random) (chicken string)
  30. srfi-1 srfi-13 srfi-18 srfi-19
  31. (prefix xattr xattr:))
  32. ;; ——————————————————————————————————————————————————
  33. ;; Channel management
  34. ;; ——————————————————————————————————————————————————
  35. ;; Lists all currently-joined channels.
  36. (define (channels root)
  37. (append (directory root) '(".server")))
  38. ;; Creates a channel's file hierarchy; safe to run, even if the channel
  39. ;; has already been created.
  40. (define (channel-add! root channel)
  41. (let* ([path (subpath root channel)])
  42. (create-directory (subpath path ".in") #t)
  43. (create-directory (subpath path ".users" "online") #t)
  44. (create-directory (subpath path ".users" "offline") #t)
  45. (create-directory (subpath path ".users" "all") #t)
  46. (channel-cleanup! root channel)))
  47. ;; Tidies up a channel directory: Removes `online` and `offline` user links.
  48. (define (channel-cleanup! root channel)
  49. (let ([users-dir (subpath root channel ".users")])
  50. (map
  51. (lambda (state-dir)
  52. (if (not (substring-index state-dir "/all"))
  53. (map
  54. (lambda (link)
  55. (let ([link-path (subpath users-dir state-dir link)])
  56. (if (symbolic-link? link-path)
  57. (delete-file link-path))))
  58. (directory (subpath users-dir state-dir)))))
  59. (directory users-dir))))
  60. ;; Sets a channel's metadata value; that is, sets the contents of the file
  61. ;; /$channel/.meta/$key to $value.
  62. (define (channel-metadata-set! root channel key value #!optional (xattr-alist '()))
  63. (directory-file-set! (subpath root channel ".meta")
  64. key value
  65. xattr-alist))
  66. ;; Return a specific bit of metadata of a channel, as a string
  67. (define (channel-metadata-get root channel key)
  68. (directory-file-get (subpath root channel ".meta") key))
  69. ;; Return a cons-list of a channel's metadata, with the file-content followed by
  70. ;; an alist of the extended attributes
  71. (define (channel-metadata-get* root channel key)
  72. (directory-file-get* (subpath root channel ".meta") key))
  73. ;; Return a list of all metadata key (files in /$channel/.meta/).
  74. (define (channel-metadata root channel)
  75. (directory (subpath root channel ".meta")))
  76. ;; ——————————————————————————————————————————————————
  77. ;; User management
  78. ;; ——————————————————————————————————————————————————
  79. ;; Create a user's server-wide global-user directory.
  80. ;; Quite simple, compared to channel-user-add!
  81. (define (user-add! root username)
  82. (create-directory (subpath root ".users" username "local") #t))
  83. ;; Sets a file in the user's directory to given value.
  84. ;; Sets /.users/$user/$key to $value.
  85. (define (user-file-set! root username key value #!optional (xattr-alist '()))
  86. (directory-file-set! (subpath root ".users" username)
  87. key value xattr-alist))
  88. ;; Returns the contents of a file in the user's global directory,
  89. ;; /.users/$user/$key.
  90. (define (user-file-get root username key)
  91. (directory-file-get (subpath root ".users" username) key))
  92. ;; Enables a user's state (online/offline/etc), for all channels they are in.
  93. (define (user-enable-state! root username state)
  94. (map
  95. (lambda (channel)
  96. (channel-user-enable-state! root channel username state))
  97. (directory (subpath root ".users" username "local"))))
  98. ;; Disables a user's state (online/offline/etc), for all channels they are in.
  99. (define (user-disable-state! root username state)
  100. (map
  101. (lambda (channel)
  102. (channel-user-disable-state! root channel username state))
  103. (directory (subpath root ".users" username "local"))))
  104. ;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not,
  105. ;; for all channels the given user is in.
  106. (define (user-toggle-states! root username enabled-state disabled-state)
  107. (map
  108. (lambda (channel)
  109. (channel-user-toggle-states! root channel username
  110. enabled-state disabled-state))
  111. (directory (subpath root ".users" username "local"))))
  112. ;; Return a list of all users of a channel of given state.
  113. ;; (Lists files in /$channel/.users/$state/).
  114. (define (channel-users root channel #!optional (state "online"))
  115. (directory (subpath root channel ".users" state)))
  116. ;; Add a user to a channel, creating their channel-user directory.
  117. ;; There are three types of channel users:
  118. ;; * Channel-only: We have no meaningful way of ever linking this user to a
  119. ;; server-wide identity.
  120. ;; (global? #f) (global-pairity #f)
  121. ;; * Serverwide-1: The user has a server-wide identity, and data like
  122. ;; nicknames/profile-pictures can NOT be changed on a per-channel
  123. ;; basis. channel-user is link to global-user.
  124. ;; (global #t) (global-pairity #t)
  125. ;; * Serverwide-2: The user has a server-wide identity, but their
  126. ;; nickname/profile-picture/etc can vary by the channel.
  127. ;; (global #t) (global-pairity #f)
  128. (define (channel-user-add! root channel username
  129. #!optional (global? #t) (global-pairity? #t) (global-name #f))
  130. (let* ([g-name (if global-name global-name username)]
  131. [user-path (subpath root channel ".users" "all" username)]
  132. (user-global-path (subpath user-path "global"))
  133. [g-user-path (subpath root ".users" g-name)]
  134. [g-local-path (subpath g-user-path "local" channel)])
  135. (cond [(or (file-exists? user-path) (directory-exists? user-path)
  136. (symbolic-link? user-path))
  137. #f]
  138. ;; If global, we gotta do some symlink dancing.
  139. [global?
  140. (user-add! root g-name)
  141. (if global-pairity?
  142. (create-symbolic-link (subpath "../../../.users" g-name) user-path)
  143. (create-directory user-path #t))
  144. (if (not (symbolic-link? user-global-path))
  145. (create-symbolic-link (subpath "../../../../.users" g-name)
  146. user-global-path))
  147. (if (not (symbolic-link? g-local-path))
  148. (create-symbolic-link (subpath "../../../" channel ".users" "all" username)
  149. g-local-path))]
  150. ;; This is a channel-only user, don't bother with symlink fanciness.
  151. [#t
  152. (create-directory user-path #t)])))
  153. ;; Sets a file in the channel-user's directory to given value.
  154. ;; Sets /$channel/.users/all/$user/$key to $value.
  155. (define (channel-user-file-set! root channel username key value #!optional (xattr-alist '()))
  156. (directory-file-set! (subpath root channel ".users" "all" username)
  157. key value xattr-alist))
  158. ;; Returns the contents of a file in the user's channel directory,
  159. ;; /$channel/.users/all/$user/$key.
  160. (define (channel-user-file-get root channel username key)
  161. (directory-file-get (subpath root channel ".users" "all" username) key))
  162. ;; Disables a channel-user's online/offline/etc state.
  163. ;; That is, removes a symlink from a /$channel/.users/* directory.
  164. (define (channel-user-disable-state! root channel username state)
  165. (let ([state-link (subpath root channel ".users" state username)])
  166. (if (or (file-exists? state-link)
  167. (symbolic-link? state-link))
  168. (delete-file state-link))))
  169. ;; Enables a channel-user's state (online/offline/etc).
  170. ;; That is, makes a symlink to a /$channel/.users/* directory.
  171. (define (channel-user-enable-state! root channel username state)
  172. (let* ([state-path
  173. (create-directory (subpath root channel ".users" state) #t)]
  174. [user-path (subpath ".." "all" username)]
  175. [state-link (subpath state-path username)])
  176. (if (not (or (file-exists? state-link)
  177. (symbolic-link? state-link)))
  178. (create-symbolic-link user-path
  179. state-link))))
  180. ;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not
  181. (define (channel-user-toggle-states! root channel username enabled-state disabled-state)
  182. (channel-user-disable-state! root channel username disabled-state)
  183. (channel-user-enable-state! root channel username enabled-state))
  184. ;; ——————————————————————————————————————————————————
  185. ;; Message management
  186. ;; ——————————————————————————————————————————————————
  187. ;; Create a message file for the given channel, contents, sender, etc.
  188. (define (channel-message-add! root channel contents
  189. #!optional (sender #f) (date (current-date))
  190. (additional-xattrs '()))
  191. ;; If the client sorts messages by date, we want each message to at least be
  192. ;; made .001s apart (e.g., if the above `date` default of current-date is used).
  193. (thread-sleep! .001) ; Ergo, the sleep.
  194. (let* ([attrs-sans-sender (append
  195. `((user.chat.channel . ,channel)
  196. (user.chat.date . ,(date->string date "~1T~2"))
  197. (user.chat.date.nanoseconds
  198. . ,(number->string (date-nanosecond date))))
  199. additional-xattrs)]
  200. [attrs (if sender
  201. (append attrs-sans-sender `((user.chat.sender . ,sender)))
  202. attrs-sans-sender)])
  203. (directory-file-set! (subpath root channel)
  204. (channel-message-file-leaf root channel date)
  205. contents attrs)))
  206. ;; List all messages of the given channel.
  207. (define (channel-messages root channel)
  208. (filter
  209. (lambda (file)
  210. (let ([path (subpath root channel file)])
  211. (and (file-exists? path)
  212. (not (directory-exists? path)))))
  213. (directory (subpath root channel))))
  214. ;; Return a message's whole data.
  215. (define (channel-message-get root channel message)
  216. (directory-file-get* (subpath root channel) message))
  217. ;; List all messages that have the given xattr set to the given value.
  218. (define (channel-messages-by-xattr root channel xattr value)
  219. (filter
  220. (lambda (message-leaf)
  221. (string=? (xattr:get-xattr (subpath root channel message-leaf)
  222. xattr)
  223. value))
  224. (channel-messages root channel)))
  225. ;; List all messages from the given sender.
  226. (define (channel-messages-by-sender root channel sender)
  227. (channel-messages-by-xattr root channel "user.chat.sender" sender))
  228. ;; List all messages sent at exactly the given date.
  229. (define (channel-messages-by-date root channel date)
  230. (channel-messages-by-xattr root channel "user.chat.date"
  231. (date->string date "~1T~2")))
  232. ;; List all messages sent around the given date, ±deviation seconds.
  233. (define (channel-messages-by-date* root channel date deviation)
  234. (channel-messages-by-date-range root channel
  235. (seconds->date (- (date->seconds date) deviation))
  236. (seconds->date (+ (date->seconds date) deviation))))
  237. ;; List all messages sent within the given date range.
  238. (define (channel-messages-by-date-range root channel min-date max-date)
  239. (filter
  240. (lambda (message-leaf)
  241. (let* ([message-path (subpath root channel message-leaf)]
  242. [message-date (string->date (xattr:get-xattr message-path "user.chat.date")
  243. "~Y-~m-~dT~H:~M:~S~z")])
  244. (and (date<=? min-date message-date)
  245. (date<=? message-date max-date))))
  246. (channel-messages root channel)))
  247. ;; Finds an appropriate (non-colliding, non-in-use) name for a message file,
  248. ;; based on its date.
  249. (define (channel-message-file-leaf root channel date)
  250. (directory-unique-file (subpath root channel)
  251. (date->string date "[~m-~d] ~H:~M:~S")))
  252. ;; ——————————————————————————————————————————————————
  253. ;; Directory as key/value store
  254. ;; ——————————————————————————————————————————————————
  255. ;; Set the contents of a directory's file `key` to `value`, setting any
  256. ;; extended attributes passed as xattr-alist.
  257. (define (directory-file-set! directory key value #!optional (xattr-alist '()))
  258. (let ([path (subpath (create-directory directory #t)
  259. key)])
  260. ;; Write the contents (value)
  261. (cond [(string? value)
  262. (write-string-to-file path value)]
  263. [(input-port? value)
  264. (write-port-to-file path value)]
  265. [(list? value)
  266. (write-byte-list-to-file path value)]
  267. ;; If no data sent (e.g., value is #f), at least make the file!
  268. [(not (file-exists? path))
  269. (write-string-to-file path "")])
  270. ;; Write the xattrs (if applicable)
  271. (map (lambda (xattr-cons)
  272. (xattr:set-xattr path (symbol->string (car xattr-cons))
  273. (cdr xattr-cons)))
  274. xattr-alist)))
  275. ;; Get the contents of the given file as astring.
  276. (define (directory-file-get directory key)
  277. (let ([path (subpath directory key)])
  278. (if (and (file-exists? path)
  279. (not (directory-exists? path)))
  280. (read-file-to-string (subpath directory key))
  281. #f)))
  282. ;; Get the contents of the given file as a string, including the all
  283. ;; extended attributes as an alist.
  284. ;; (contents (xattr . value) (xattr .value) …)
  285. (define (directory-file-get* directory key)
  286. (let ([path (subpath directory key)]
  287. [contents (directory-file-get directory key)])
  288. (if contents
  289. (cons contents
  290. (map (lambda (xattr)
  291. (cons (string->symbol xattr)
  292. (xattr:get-xattr path xattr)))
  293. (xattr:list-xattrs path))))))
  294. ;; Given a directory and a filename, return a unique filename by appending
  295. ;; a number to the end of the name, as necessary.
  296. (define (directory-unique-file directory name #!optional (suffix ""))
  297. (let* ([leaf
  298. (string-append name (if (not (string-null? suffix)) "." "")
  299. suffix)]
  300. [path
  301. (subpath directory leaf)])
  302. (if (file-exists? path)
  303. (directory-unique-file
  304. directory
  305. name
  306. (string-pad
  307. (number->string (+ (or (and (string? suffix)
  308. (string->number suffix))
  309. 0)
  310. 1))
  311. 4 #\0))
  312. leaf)))
  313. ;; ——————————————————————————————————————————————————
  314. ;; Misc. utility
  315. ;; ——————————————————————————————————————————————————
  316. ;; Return a file path with the given parameters as elements of the path
  317. ;; E.g., "/etc/", "/systemd/user" "mom" => "/etc/systemd/user/mom"
  318. (define (subpath . children)
  319. (normalize-pathname
  320. (reduce-right (lambda (a b)
  321. (string-append a "/" b))
  322. "" children)))
  323. ;; Title says all, I'd hope.
  324. (define (write-string-to-file file value)
  325. (call-with-output-file file
  326. (lambda (out-port)
  327. (write-string value #f out-port))))
  328. ;; Again, self-evident. Right?
  329. (define (write-port-to-file path in-port)
  330. (call-with-output-file path
  331. (lambda (out-port)
  332. (copy-port in-port out-port read-byte write-byte))))
  333. ;; Still obvious, no?
  334. (define (write-byte-list-to-file path byte-list)
  335. (call-with-output-file path
  336. (lambda (out-port)
  337. (map (lambda (byte)
  338. (write-char byte out-port))
  339. byte-list))))
  340. ;; And we're still on the same page, I'd hope?
  341. (define (read-file-to-string file)
  342. (call-with-input-file file
  343. (lambda (in-port)
  344. (read-string #f in-port))))
  345. ) ;; chatdir module