diff --git a/chatdir.scm b/chatdir.scm index 03d09d7..e82dbe4 100644 --- a/chatdir.scm +++ b/chatdir.scm @@ -15,251 +15,53 @@ ;; (import scheme - (chicken file) (chicken file posix) (chicken pathname) (chicken io) - (chicken random) (chicken string) - srfi-1 srfi-13 srfi-19 - (prefix inotify inotify:) - (prefix xattr xattr:)) + (chicken file) (chicken file posix) (chicken pathname) (chicken io) + (chicken random) (chicken string) + srfi-1 srfi-13 srfi-19 + (prefix inotify inotify:) + (prefix xattr xattr:)) -;; Wrapper around `directory` that lists a dir's files as a relative path -(define (directory-rel #!optional (path "./")) - (let ([relative-parent (normalize-pathname (string-append path "/"))]) - (map (lambda (leaf) - (string-append relative-parent leaf)) - (directory path)))) - - -;; Returns an inotify watch-descriptor according the given path -(define (path->wd path) - (car - (filter (lambda (wd) - (string=? (normalize-pathname (inotify:wd->path wd)) - (normalize-pathname path))) - (inotify:wd-list)))) - - -;; Attempt to remove an inotify watch; if it's already been removed, no sweat -;; (This happens sometimes when inotify automatically deletes a watch) -(define (attempt-remove-watch! watch) - (handle-exceptions exn - #t - (inotify:remove-watch! watch))) - - -;; Tidies up a channel directory: Removes `online` and `offline` user links. -(define (channel-cleanup! root channel) - (let ([users-dir (subpath root channel ".users")]) - (map - (lambda (state-dir) - (if (not (substring-index state-dir "/all")) - (map - (lambda (link) - (let ([link-path (subpath users-dir state-dir link)]) - (if (symbolic-link? link-path) - (delete-file link-path)))) - (directory (subpath users-dir state-dir))))) - (directory users-dir)))) +;; —————————————————————————————————————————————————— +;; Channel management +;; —————————————————————————————————————————————————— +;; Lists all currently-joined channels. +(define (channels root) + (directory root)) ;; Creates a channel's file hierarchy; safe to run, even if the channel ;; has already been created. (define (channel-add! root channel) (let* ([path (subpath root channel)]) - (create-directory (subpath path ".in") #t) - (create-directory (subpath path ".users" "online") #t) - (create-directory (subpath path ".users" "offline") #t) - (create-directory (subpath path ".users" "all") #t) - (channel-cleanup! root channel))) + (create-directory (subpath path ".in") #t) + (create-directory (subpath path ".users" "online") #t) + (create-directory (subpath path ".users" "offline") #t) + (create-directory (subpath path ".users" "all") #t) + (channel-cleanup! root channel))) -;; Create a user's server-wide global-user directory. -;; Quite simple, compared to channel-user-add! -(define (user-add! root username) - (create-directory (subpath root ".users" username "local") #t)) - - -;; Add a user to a channel, creating their channel-user directory. -;; There are three types of channel users: -;; * Channel-only: We have no meaningful way of ever linking this user to a server-wide identity. -;; (global? #f) (global-pairity #f) -;; * Serverwide-1: The user has a server-wide identity, and data like nicknames/profile-pictures -;; can NOT be changed on a per-channel basis. channel-user is link to global-user. -;; (global #t) (global-pairity #t) -;; * Serverwide-2: The user has a server-wide identity, but their nickname/profile-picture/etc -;; can vary by the channel. -;; (global #t) (global-pairity #f) -(define (channel-user-add! root channel username - #!optional (global? #t) (global-pairity? #t) (global-name #f)) - (let* ([g-name (if global-name global-name username)] - [user-path (subpath root channel ".users" "all" username)] - [g-user-path (subpath root ".users" g-name)]) - (cond [(or (file-exists? user-path) (directory-exists? user-path)) - #f] - ;; If global, we gotta do some symlink dancing. - [global? - (user-add! root g-name) - (if global-pairity? - (create-symbolic-link (subpath "../../../.users" g-name) user-path) - (create-directory user-path #t)) - (create-symbolic-link (subpath "../../../../.users" g-name) - (subpath user-path "global")) - (create-symbolic-link (subpath "../../../" channel ".users" "all" username) - (subpath g-user-path "local" channel))] - ;; This is a channel-only user, don't bother with symlink fanciness. - [#t - (create-directory user-path #t)]))) - - -;; Return a list of all users of a channel of given state. -;; (Lists files in /$channel/.users/$state/). -(define (channel-users root channel #!optional (state "online")) - (directory (subpath root channel ".users" state))) - - -;; Sets a file in the user's directory to given value. -;; Sets /.users/$user/$key to $value. -(define (user-file-set! root username key value #!optional (xattr-alist '())) - (directory-file-set! (subpath root ".users" username) - key value xattr-alist)) - - -;; Returns the contents of a file in the user's global directory, -;; /.users/$user/$key. -(define (user-file-get root username key) - (directory-file-get (subpath root ".users" username) key)) - - -;; Sets a file in the channel-user's directory to given value. -;; Sets /$channel/.users/all/$user/$key to $value. -(define (channel-user-file-set! root channel username key value #!optional (xattr-alist '())) - (directory-file-set! (subpath root channel ".users" "all" username) - key value xattr-alist)) - - -;; Returns the contents of a file in the user's channel directory, -;; /$channel/.users/all/$user/$key. -(define (channel-user-file-get root channel username key) - (directory-file-get (subpath root channel ".users" "all" username) key)) - - -;; Disables a channel-user's online/offline/etc state. -;; That is, removes a symlink from a /$channel/.users/* directory. -(define (channel-user-disable-state! root channel username state) - (let ([state-link (subpath root channel ".users" state username)]) - (if (or (file-exists? state-link) - (symbolic-link? state-link)) - (delete-file state-link)))) - - -;; Enables a channel-user's state (online/offline/etc). -;; That is, makes a symlink to a /$channel/.users/* directory. -(define (channel-user-enable-state! root channel username state) - (let* ([state-path - (create-directory (subpath root channel ".users" state) #t)] - [user-path (subpath ".." "all" username)] - [state-link (subpath state-path username)]) - (if (not (or (file-exists? state-link) - (symbolic-link? state-link))) - (create-symbolic-link user-path - state-link)))) - - -;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not -(define (channel-user-toggle-states! root channel username enabled-state disabled-state) - (channel-user-disable-state! root channel username disabled-state) - (channel-user-enable-state! root channel username enabled-state)) - - -;; Enables a user's state (online/offline/etc), for all channels they are in. -(define (user-enable-state! root username state) - (map - (lambda (channel) - (channel-user-enable-state! root channel username state)) - (directory (subpath root ".users" username "local")))) - - -;; Disables a user's state (online/offline/etc), for all channels they are in. -(define (user-disable-state! root username state) - (map - (lambda (channel) - (channel-user-disable-state! root channel username state)) - (directory (subpath root ".users" username "local")))) - - -;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not, -;; for all channels the given user is in. -(define (user-toggle-states! root username enabled-state disabled-state) - (map - (lambda (channel) - (channel-user-toggle-states! root channel username - enabled-state disabled-state)) - (directory (subpath root ".users" username "local")))) - - -(define (write-string-to-file file value) - (call-with-output-file file - (lambda (out-port) - (write-string value #f out-port)))) - - -(define (write-port-to-file path in-port) - (call-with-output-file path - (lambda (out-port) - (copy-port in-port out-port read-byte write-byte)))) - - -(define (write-byte-list-to-file path byte-list) - (call-with-output-file path - (lambda (out-port) - (map (lambda (byte) - (write-char byte out-port)) - byte-list)))) - - -(define (read-file-to-string file) - (call-with-input-file file - (lambda (in-port) - (read-string #f in-port)))) - - -(define (directory-file-set! directory key value #!optional (xattr-alist '())) - (let ([path (subpath directory key)]) - ;; Write the contents (value) - (cond [(string? value) - (write-string-to-file path value)] - [(input-port? value) - (write-port-to-file path value)] - [(list? value) - (write-byte-list-to-file path value)]) - - ;; Write the xattrs (if applicable) - (map (lambda (xattr-cons) - (xattr:set-xattr path (symbol->string (car xattr-cons)) - (cdr xattr-cons))) - xattr-alist))) - - -(define (directory-file-get directory key) - (read-file-to-string (subpath directory key))) - - -;; Get the contents of the given file as a string, including the -(define (directory-file-get* directory key) - (let ([path (subpath directory key)]) - (cons (directory-file-get directory key) - (map (lambda (xattr) - (cons (string->symbol xattr) - (xattr:get-xattr path xattr))) - (xattr:list-xattrs path))))) +;; Tidies up a channel directory: Removes `online` and `offline` user links. +(define (channel-cleanup! root channel) + (let ([users-dir (subpath root channel ".users")]) + (map + (lambda (state-dir) + (if (not (substring-index state-dir "/all")) + (map + (lambda (link) + (let ([link-path (subpath users-dir state-dir link)]) + (if (symbolic-link? link-path) + (delete-file link-path)))) + (directory (subpath users-dir state-dir))))) + (directory users-dir)))) ;; Sets a channel's metadata value; that is, sets the contents of the file ;; /$channel/.meta/$key to $value. (define (channel-metadata-set! root channel key value #!optional (xattr-alist '())) (directory-file-set! (subpath root channel ".meta") - key value - xattr-alist)) + key value + xattr-alist)) ;; Return a specific bit of metadata of a channel, as a string @@ -278,57 +80,156 @@ (directory (subpath root channel ".meta"))) -;; Lists all currently-joined channels. -(define (channels root) - (directory root)) + +;; —————————————————————————————————————————————————— +;; User management +;; —————————————————————————————————————————————————— + +;; Create a user's server-wide global-user directory. +;; Quite simple, compared to channel-user-add! +(define (user-add! root username) + (create-directory (subpath root ".users" username "local") #t)) -;; Return a file path with the given parameters as elements of the path -;; E.g., "/etc/", "/systemd/user" "mom" => "/etc/systemd/user/mom" -(define (subpath . children) - (normalize-pathname - (reduce-right (lambda (a b) - (string-append a "/" b)) - "" children))) +;; Sets a file in the user's directory to given value. +;; Sets /.users/$user/$key to $value. +(define (user-file-set! root username key value #!optional (xattr-alist '())) + (directory-file-set! (subpath root ".users" username) + key value xattr-alist)) -;; Given a directory and a filename, return a unique filename by appending -;; a number to the end of the name, as necessary. -(define (directory-unique-file directory name #!optional (suffix "")) - (let* ([leaf - (string-append name suffix)] - [path - (subpath directory leaf)]) - (if (file-exists? path) - (directory-unique-file - directory - leaf - (number->string (+ (or (string->number suffix) 0) - .1))) - leaf))) +;; Returns the contents of a file in the user's global directory, +;; /.users/$user/$key. +(define (user-file-get root username key) + (directory-file-get (subpath root ".users" username) key)) -;; Finds an appropriate (non-colliding, non-in-use) name for a message file, -;; based on its date. -(define (message-file-leaf root channel date) - (directory-unique-file (subpath root channel) - (date->string date "[~m-~d] ~H:~M:~S"))) +;; Enables a user's state (online/offline/etc), for all channels they are in. +(define (user-enable-state! root username state) + (map + (lambda (channel) + (channel-user-enable-state! root channel username state)) + (directory (subpath root ".users" username "local")))) +;; Disables a user's state (online/offline/etc), for all channels they are in. +(define (user-disable-state! root username state) + (map + (lambda (channel) + (channel-user-disable-state! root channel username state)) + (directory (subpath root ".users" username "local")))) + + +;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not, +;; for all channels the given user is in. +(define (user-toggle-states! root username enabled-state disabled-state) + (map + (lambda (channel) + (channel-user-toggle-states! root channel username + enabled-state disabled-state)) + (directory (subpath root ".users" username "local")))) + + +;; Return a list of all users of a channel of given state. +;; (Lists files in /$channel/.users/$state/). +(define (channel-users root channel #!optional (state "online")) + (directory (subpath root channel ".users" state))) + + +;; Add a user to a channel, creating their channel-user directory. +;; There are three types of channel users: +;; * Channel-only: We have no meaningful way of ever linking this user to a +;; server-wide identity. +;; (global? #f) (global-pairity #f) +;; * Serverwide-1: The user has a server-wide identity, and data like +;; nicknames/profile-pictures can NOT be changed on a per-channel +;; basis. channel-user is link to global-user. +;; (global #t) (global-pairity #t) +;; * Serverwide-2: The user has a server-wide identity, but their +;; nickname/profile-picture/etc can vary by the channel. +;; (global #t) (global-pairity #f) +(define (channel-user-add! root channel username + #!optional (global? #t) (global-pairity? #t) (global-name #f)) + (let* ([g-name (if global-name global-name username)] + [user-path (subpath root channel ".users" "all" username)] + [g-user-path (subpath root ".users" g-name)]) + (cond [(or (file-exists? user-path) (directory-exists? user-path)) + #f] + ;; If global, we gotta do some symlink dancing. + [global? + (user-add! root g-name) + (if global-pairity? + (create-symbolic-link (subpath "../../../.users" g-name) user-path) + (create-directory user-path #t)) + (create-symbolic-link (subpath "../../../../.users" g-name) + (subpath user-path "global")) + (create-symbolic-link (subpath "../../../" channel ".users" "all" username) + (subpath g-user-path "local" channel))] + ;; This is a channel-only user, don't bother with symlink fanciness. + [#t + (create-directory user-path #t)]))) + + +;; Sets a file in the channel-user's directory to given value. +;; Sets /$channel/.users/all/$user/$key to $value. +(define (channel-user-file-set! root channel username key value #!optional (xattr-alist '())) + (directory-file-set! (subpath root channel ".users" "all" username) + key value xattr-alist)) + + +;; Returns the contents of a file in the user's channel directory, +;; /$channel/.users/all/$user/$key. +(define (channel-user-file-get root channel username key) + (directory-file-get (subpath root channel ".users" "all" username) key)) + + +;; Disables a channel-user's online/offline/etc state. +;; That is, removes a symlink from a /$channel/.users/* directory. +(define (channel-user-disable-state! root channel username state) + (let ([state-link (subpath root channel ".users" state username)]) + (if (or (file-exists? state-link) + (symbolic-link? state-link)) + (delete-file state-link)))) + + +;; Enables a channel-user's state (online/offline/etc). +;; That is, makes a symlink to a /$channel/.users/* directory. +(define (channel-user-enable-state! root channel username state) + (let* ([state-path + (create-directory (subpath root channel ".users" state) #t)] + [user-path (subpath ".." "all" username)] + [state-link (subpath state-path username)]) + (if (not (or (file-exists? state-link) + (symbolic-link? state-link))) + (create-symbolic-link user-path + state-link)))) + + +;; Ensures the enabled-state is enabled, and it's opposite (disabled-state) is not +(define (channel-user-toggle-states! root channel username enabled-state disabled-state) + (channel-user-disable-state! root channel username disabled-state) + (channel-user-enable-state! root channel username enabled-state)) + + + +;; —————————————————————————————————————————————————— +;; Message management +;; —————————————————————————————————————————————————— + ;; Create a message file for the given channel, contents, sender, etc. (define (channel-message-add! root channel contents - #!optional (sender #f) (date (current-date)) - (additional-xattrs '())) + #!optional (sender #f) (date (current-date)) + (additional-xattrs '())) (let* ([attrs-sans-sender (append - `((user.chat.date . ,(date->string date "~1T~2")) - (user.chat.channel . ,channel)) - additional-xattrs)] - [attrs (if sender - (append attrs-sans-sender `((user.chat.sender . ,sender))) - attrs-sans-sender)]) - (directory-file-set! (subpath root channel) - (message-file-leaf root channel date) - contents attrs))) + `((user.chat.date . ,(date->string date "~1T~2")) + (user.chat.channel . ,channel)) + additional-xattrs)] + [attrs (if sender + (append attrs-sans-sender `((user.chat.sender . ,sender))) + attrs-sans-sender)]) + (directory-file-set! (subpath root channel) + (channel-message-file-leaf root channel date) + contents attrs))) ;; List all messages of the given channel. @@ -340,9 +241,9 @@ (define (channel-messages-by-xattr root channel xattr value) (filter (lambda (message-leaf) - (string=? (xattr:get-xattr (subpath root channel message-leaf) - xattr) - value)) + (string=? (xattr:get-xattr (subpath root channel message-leaf) + xattr) + value)) (channel-messages root channel))) @@ -354,114 +255,126 @@ ;; List all messages sent at exactly the given date. (define (channel-messages-by-date root channel date) (channel-messages-by-xattr root channel "user.chat.date" - (date->string date "~1T~2"))) + (date->string date "~1T~2"))) ;; List all messages sent around the given date, ±deviation seconds. (define (channel-messages-by-date* root channel date deviation) (channel-messages-by-date-range root channel - (seconds->date (- (date->seconds date) deviation)) - (seconds->date (+ (date->seconds date) deviation)))) + (seconds->date (- (date->seconds date) deviation)) + (seconds->date (+ (date->seconds date) deviation)))) ;; List all messages sent within the given date range. (define (channel-messages-by-date-range root channel min-date max-date) (filter (lambda (message-leaf) - (let* ([message-path (subpath root channel message-leaf)] - [message-date (string->date (xattr:get-xattr message-path "user.chat.date") - "~Y-~m-~dT~H:~M:~S~z")]) - (and (date<=? min-date message-date) - (date<=? message-date max-date)))) + (let* ([message-path (subpath root channel message-leaf)] + [message-date (string->date (xattr:get-xattr message-path "user.chat.date") + "~Y-~m-~dT~H:~M:~S~z")]) + (and (date<=? min-date message-date) + (date<=? message-date max-date)))) (channel-messages root channel))) +;; Finds an appropriate (non-colliding, non-in-use) name for a message file, +;; based on its date. +(define (channel-message-file-leaf root channel date) + (directory-unique-file (subpath root channel) + (date->string date "[~m-~d] ~H:~M:~S"))) + + + +;; —————————————————————————————————————————————————— +;; Skeleton of a daemon +;; —————————————————————————————————————————————————— + ;; Initialization for the input loop (define (input-loop-init root-dir callbacks-alist) (let ([join-callback (alist-ref 'join-channel callbacks-alist)]) - (inotify:init!) - ;; Start watching the chatdir (for new channel joins, etc) - (inotify:add-watch! - root-dir '(onlydir moved-to moved-from delete delete-self create)) + (inotify:init!) + ;; Start watching the chatdir (for new channel joins, etc) + (inotify:add-watch! + root-dir '(onlydir moved-to moved-from delete delete-self create)) - ;; Auto-join channels with all pre-existing channel directories - (map (lambda (path) - (let ([channel-dirname (pathname-file (pathname-directory (string-append path "/")))] - [join-callback (alist-ref 'join-channel callbacks-alist)]) - (if join-callback - (apply join-callback (list channel-dirname))) + ;; Auto-join channels with all pre-existing channel directories + (map (lambda (path) + (let ([channel-dirname (pathname-file (pathname-directory (string-append path "/")))] + [join-callback (alist-ref 'join-channel callbacks-alist)]) + (if join-callback + (apply join-callback (list channel-dirname))) - (inotify:add-watch! in-path '(moved-to close-write)) - (print "Joined and watching: " in-path))) - (filter directory-exists? (directory-rel irc-dir))))) + (inotify:add-watch! in-path '(moved-to close-write)) + (print "Joined and watching: " in-path))) + (filter directory-exists? (directory-rel irc-dir))))) ;; Handles all inotify-watched file events from the top-level IRC-directory. ;; Mainly, checking for newly-joined or left channels. (define (handle-main-dir-event callbacks-alist event) (let ([flags (inotify:event-flags event)] - [leave-callback (alist-ref 'leave-channel callbacks-alist)] - [join-callback (alist-ref 'join-channel callbacks-alist)]) - (cond - ;; If a channel dir's been moved or removed, stop watching (ofc) - ;; … Also quit that room! Heck them! - [(or (member 'moved-from flags) - (member 'delete flags) - (member 'delete-self flags)) - (let* ([channel (inotify:event-name event)] - [channel-inpath - (string-append (inotify:wd->path - (inotify:event-wd event)) - channel "/.in")] - [channel-wd (path->wd channel-inpath)]) - (print "Remove watch for " channel-inpath "…") - (if (and channel-wd (member channel-wd (wd-list))) - (attempt-remove-watch! channel-wd)) - (if leave-callback - (apply leave-callback (list channel))))] + [leave-callback (alist-ref 'leave-channel callbacks-alist)] + [join-callback (alist-ref 'join-channel callbacks-alist)]) + (cond + ;; If a channel dir's been moved or removed, stop watching (ofc) + ;; … Also quit that room! Heck them! + [(or (member 'moved-from flags) + (member 'delete flags) + (member 'delete-self flags)) + (let* ([channel (inotify:event-name event)] + [channel-inpath + (string-append (inotify:wd->path + (inotify:event-wd event)) + channel "/.in")] + [channel-wd (path->wd channel-inpath)]) + (print "Remove watch for " channel-inpath "…") + (if (and channel-wd (member channel-wd (wd-list))) + (attempt-remove-watch! channel-wd)) + (if leave-callback + (apply leave-callback (list channel))))] - ;; If a dir's been created for a channel, maybe-join, then watch input! - [(or (member 'create flags) - (member 'moved-to flags)) - (let* ([channel (inotify:event->pathname event)]) - (print "Attempting to join channel " dirname "…") - (if join-callback - (apply join-callback (list path))))]))) + ;; If a dir's been created for a channel, maybe-join, then watch input! + [(or (member 'create flags) + (member 'moved-to flags)) + (let* ([channel (inotify:event->pathname event)]) + (print "Attempting to join channel " dirname "…") + (if join-callback + (apply join-callback (list path))))]))) (define (channel-joined root-dir channel) (let* ([in-path (normalize-pathname (string-append root-dir "/" channel "/.in"))]) - (inotify:add-watch! in-path '(moved-to close-write)) - (print "Began watching input " in-path "."))) + (inotify:add-watch! in-path '(moved-to close-write)) + (print "Began watching input " in-path "."))) ;; Handles an inotify event that pertains to a channel's .in/ directory (define (handle-channel-dir-event callbacks-alist event) (let* ([event-dir (pathname-directory (inotify:event->pathname event))] - [dirname (pathname-file event-dir)] - [channel (pathname-file (pathname-directory event-dir))] - [send-message-callback (alist-ref 'send-message callbacks-alist)]) - (cond - ;; If input is given to an `.in` dir… well, send that darn message! - ;; What're you wating for? - [(and (string=? dirname ".in") - send-message-callback) - (map (lambda (message) - (apply send-message (list channel message))) - (with-input-from-file (inotify:event->pathname event) - read-lines)) - (delete-file* (inotify:event->pathname event))]))) + [dirname (pathname-file event-dir)] + [channel (pathname-file (pathname-directory event-dir))] + [send-message-callback (alist-ref 'send-message callbacks-alist)]) + (cond + ;; If input is given to an `.in` dir… well, send that darn message! + ;; What're you wating for? + [(and (string=? dirname ".in") + send-message-callback) + (map (lambda (message) + (apply send-message (list channel message))) + (with-input-from-file (inotify:event->pathname event) + read-lines)) + (delete-file* (inotify:event->pathname event))]))) ;; Handle a single inotify file event, as part of the input loop (define (handle-file-event root-dir callbacks-alist event) (if (not (member 'ignored (event-flags event))) - (let* ([flags (inotify:event-flags event)] - [wd-path (inotify:wd->path (inotify:event-wd event))] - [main-dir? (string=? wd-path root-dir)]) - (if main-dir? - (handle-main-dir-event root-dir callbacks-alist event) - (handle-channel-dir-event root-dir callbacks-alist event))))) + (let* ([flags (inotify:event-flags event)] + [wd-path (inotify:wd->path (inotify:event-wd event))] + [main-dir? (string=? wd-path root-dir)]) + (if main-dir? + (handle-main-dir-event root-dir callbacks-alist event) + (handle-channel-dir-event root-dir callbacks-alist event))))) ;; The FS-backed input loop, to be run in a seperate thread (so as to not block) @@ -471,12 +384,141 @@ ;; (new-message channel text) (define (input-loop root-dir callbacks-alist) (map (lambda (event) - (handle-file-event root-dir callbacks-alist event)) - (inotify:next-events!)) + (handle-file-event root-dir callbacks-alist event)) + (inotify:next-events!)) (input-loop root-dir callbacks-alist)) + +;; —————————————————————————————————————————————————— +;; inotify utility +;; —————————————————————————————————————————————————— + +;; Returns an inotify watch-descriptor according the given path +(define (path->wd path) + (car + (filter (lambda (wd) + (string=? (normalize-pathname (inotify:wd->path wd)) + (normalize-pathname path))) + (inotify:wd-list)))) + + +;; Attempt to remove an inotify watch; if it's already been removed, no sweat +;; (This happens sometimes when inotify automatically deletes a watch) +(define (attempt-remove-watch! watch) + (handle-exceptions exn + #t + (inotify:remove-watch! watch))) + ;; Repeat after me: ;; 🎵 Symbolic links cannot have extended attributes, and that is a war-crime. 🎶 ;; 🎵 Directories cannot have extended attributes, and that is a war-crime. 🎶 + + + +;; —————————————————————————————————————————————————— +;; Directory as key/value store +;; —————————————————————————————————————————————————— + +;; Set the contents of a directory's file `key` to `value`, setting any +;; extended attributes passed as xattr-alist. +(define (directory-file-set! directory key value #!optional (xattr-alist '())) + (let ([path (subpath directory key)]) + ;; Write the contents (value) + (cond [(string? value) + (write-string-to-file path value)] + [(input-port? value) + (write-port-to-file path value)] + [(list? value) + (write-byte-list-to-file path value)]) + + ;; Write the xattrs (if applicable) + (map (lambda (xattr-cons) + (xattr:set-xattr path (symbol->string (car xattr-cons)) + (cdr xattr-cons))) + xattr-alist))) + +;; Get the contents of the given file as astring. +(define (directory-file-get directory key) + (read-file-to-string (subpath directory key))) + + +;; Get the contents of the given file as a string, including the all +;; extended attributes as an alist. +;; (contents (xattr . value) (xattr .value) …) +(define (directory-file-get* directory key) + (let ([path (subpath directory key)]) + (cons (directory-file-get directory key) + (map (lambda (xattr) + (cons (string->symbol xattr) + (xattr:get-xattr path xattr))) + (xattr:list-xattrs path))))) + + +;; Given a directory and a filename, return a unique filename by appending +;; a number to the end of the name, as necessary. +(define (directory-unique-file directory name #!optional (suffix "")) + (let* ([leaf + (string-append name suffix)] + [path + (subpath directory leaf)]) + (if (file-exists? path) + (directory-unique-file + directory + leaf + (number->string (+ (or (string->number suffix) 0) + .1))) + leaf))) + + + +;; —————————————————————————————————————————————————— +;; Misc. utility +;; —————————————————————————————————————————————————— + +;; Return a file path with the given parameters as elements of the path +;; E.g., "/etc/", "/systemd/user" "mom" => "/etc/systemd/user/mom" +(define (subpath . children) + (normalize-pathname + (reduce-right (lambda (a b) + (string-append a "/" b)) + "" children))) + +;; Wrapper around `directory` that lists a dir's files as a relative path +(define (directory-rel #!optional (path "./")) + (let ([relative-parent (normalize-pathname (string-append path "/"))]) + (map (lambda (leaf) + (string-append relative-parent leaf)) + (directory path)))) + + +;; Title says all, I'd hope. +(define (write-string-to-file file value) + (call-with-output-file file + (lambda (out-port) + (write-string value #f out-port)))) + + +;; Again, self-evident. Right? +(define (write-port-to-file path in-port) + (call-with-output-file path + (lambda (out-port) + (copy-port in-port out-port read-byte write-byte)))) + + +;; Still obvious, no? +(define (write-byte-list-to-file path byte-list) + (call-with-output-file path + (lambda (out-port) + (map (lambda (byte) + (write-char byte out-port)) + byte-list)))) + + +;; And we're still on the same page, I'd hope? +(define (read-file-to-string file) + (call-with-input-file file + (lambda (in-port) + (read-string #f in-port)))) + diff --git a/tests/tests.scm b/tests/tests.scm index e77558c..f278280 100644 --- a/tests/tests.scm +++ b/tests/tests.scm @@ -8,7 +8,7 @@ ;; Appends six random chars (define (randomize-string str) (string-append str " " - (random-bytes "ABC123" 6))) + (random-bytes "ABC123" 6))) @@ -24,8 +24,8 @@ ;; General-pupose functions ;; —————————————————————————————————————————————————— (check (subpath "/etc" "systemd/" "user" "momma") - => - "/etc/systemd/user/momma") + => + "/etc/systemd/user/momma") @@ -37,29 +37,29 @@ (define *room-meta-path* "test chatdir/dining room/.meta") (define *room-topic* "Here we can discuss everything relating to food.\nCooking, dining, etc!\nThe only limit is your palette!\n") (define *room-topic-xattrs* (list (cons 'user.chat.user "admin-tan") - (cons 'user.chat.date (number->string (pseudo-random-integer 9999))))) + (cons 'user.chat.date (number->string (pseudo-random-integer 9999))))) (directory-file-set! *room-meta-path* "topic" *room-topic* *room-topic-xattrs*) (check (directory-file-get *room-meta-path* "topic") - => - *room-topic*) + => + *room-topic*) (check (directory-file-get* *room-meta-path* "topic") - => - (cons *room-topic* *room-topic-xattrs*)) + => + (cons *room-topic* *room-topic-xattrs*)) (define *room-topic-2* (randomize-string *room-topic*)) (define *room-topic-xattrs-2* (alist-update 'user.chat.user "admin-mom" *room-topic-xattrs*)) (channel-metadata-set! *dir* *room* "topic" - *room-topic-2* - *room-topic-xattrs-2*) + *room-topic-2* + *room-topic-xattrs-2*) (check (channel-metadata-get *dir* *room* "topic") - => - *room-topic-2*) + => + *room-topic-2*) (check (channel-metadata-get* *dir* *room* "topic") - => - (cons *room-topic-2* *room-topic-xattrs-2*)) + => + (cons *room-topic-2* *room-topic-xattrs-2*)) @@ -93,49 +93,49 @@ (check (directory-file-get* (subpath *dir* *room*) *msg-name*) - => - (list *msg-text* - (cons 'user.chat.date (date->string *msg-date* "~1T~2")) - *msg-xattr* - (cons 'user.chat.sender *msg-sender*) - (cons 'user.chat.channel *room*))) + => + (list *msg-text* + (cons 'user.chat.date (date->string *msg-date* "~1T~2")) + *msg-xattr* + (cons 'user.chat.sender *msg-sender*) + (cons 'user.chat.channel *room*))) (check (directory-file-get* (subpath *dir* *room*) *msg-name-2*) - => - (list *msg-text-2* - (cons 'user.chat.date (date->string *msg-date-2* "~1T~2")) - (cons 'user.chat.sender *msg-sender-2*) - (cons 'user.chat.channel *room*))) + => + (list *msg-text-2* + (cons 'user.chat.date (date->string *msg-date-2* "~1T~2")) + (cons 'user.chat.sender *msg-sender-2*) + (cons 'user.chat.channel *room*))) (check (list (find (lambda (a) (string=? *msg-name* a)) - (channel-messages *dir* *room*)) - (find (lambda (a) (string=? *msg-name-2* a)) - (channel-messages *dir* *room*))) - => - (list *msg-name* *msg-name-2*)) + (channel-messages *dir* *room*)) + (find (lambda (a) (string=? *msg-name-2* a)) + (channel-messages *dir* *room*))) + => + (list *msg-name* *msg-name-2*)) (check (list (<= 2 (length (channel-messages-by-sender *dir* *room* "maya"))) - (find (lambda (a) (string=? *msg-name-3* a)) - (channel-messages-by-sender *dir* *room* "maya"))) - => - (list #t *msg-name-3*)) + (find (lambda (a) (string=? *msg-name-3* a)) + (channel-messages-by-sender *dir* *room* "maya"))) + => + (list #t *msg-name-3*)) (check (find (lambda (a) (string=? *msg-name-3* a)) - (channel-messages-by-date *dir* *room* *msg-date-3*)) - => - *msg-name-3*) + (channel-messages-by-date *dir* *room* *msg-date-3*)) + => + *msg-name-3*) (check (let ([messages - (channel-messages-by-date-range *dir* *room* *msg-date-3* *msg-date-4*)]) - (list (find (lambda (a) (string=? *msg-name-3* a)) messages) - (find (lambda (a) (string=? *msg-name-4* a)) messages))) - => - (list *msg-name-3* *msg-name-4*)) + (channel-messages-by-date-range *dir* *room* *msg-date-3* *msg-date-4*)]) + (list (find (lambda (a) (string=? *msg-name-3* a)) messages) + (find (lambda (a) (string=? *msg-name-4* a)) messages))) + => + (list *msg-name-3* *msg-name-4*)) @@ -147,26 +147,26 @@ (define *new-room-users* (subpath *new-room-path* ".users")) (define *new-room-all* (subpath *new-room-users* "all")) (if (directory-exists? *new-room-path*) - (delete-directory (subpath *dir* *new-room*) #t)) + (delete-directory (subpath *dir* *new-room*) #t)) (channel-add! *dir* *new-room*) (check (and (directory-exists? *new-room-path*) - (directory-exists? *new-room-all*)) - => - *new-room-all*) + (directory-exists? *new-room-all*)) + => + *new-room-all*) (define *new-room-online* (subpath *new-room-path* ".users" "online")) (create-symbolic-link "./" (subpath *new-room-online* "birdo")) (create-symbolic-link "./" (subpath *new-room-online* "mondo")) (check (sort (directory *new-room-online*) string<) - => - '("birdo" "mondo")) + => + '("birdo" "mondo")) (channel-cleanup! *dir* *new-room*) (check (directory *new-room-online*) - => - '()) + => + '()) @@ -175,24 +175,24 @@ ;; —————————————————————————————————————————————————— (define *users-dir* (subpath *dir* ".users")) (if (directory-exists? *users-dir*) - (delete-directory *users-dir* #t)) + (delete-directory *users-dir* #t)) (if (directory-exists? *new-room-users*) - (delete-directory *new-room-users* #t)) + (delete-directory *new-room-users* #t)) ;; Create a global user-directory. (user-add! *dir* "birdo") (check (string? (directory-exists? (subpath *dir* ".users" "birdo"))) - => - #t) + => + #t) ;; Check a room-only account; it has no global directory. (channel-user-add! *dir* *new-room* "mondo" #f #f) (check (and (not (directory-exists? (subpath *users-dir* "mondo"))) - (not (symbolic-link? (subpath *new-room-all* "mondo"))) - (string? (directory-exists? (subpath *new-room-all* "mondo")))) - => - #t) + (not (symbolic-link? (subpath *new-room-all* "mondo"))) + (string? (directory-exists? (subpath *new-room-all* "mondo")))) + => + #t) ;; Check a room user-directory, that matches up one-to-one with a @@ -202,17 +202,17 @@ (channel-user-add! *dir* *new-room* "birdo" #t #t) (channel-user-file-set! *dir* *new-room* "birdo" "nick" "rose") (check (read-symbolic-link (subpath *new-room-all* "birdo")) - => - "../../../.users/birdo") + => + "../../../.users/birdo") (check (read-symbolic-link (subpath *new-room-all* "birdo" "global")) - => - "../../../../.users/birdo") + => + "../../../../.users/birdo") (check (read-symbolic-link (subpath *users-dir* "birdo" "local" *new-room*)) - => - (subpath "../../../" *new-room* ".users" "all" "birdo")) + => + (subpath "../../../" *new-room* ".users" "all" "birdo")) (check (user-file-get *dir* "birdo" "nick") - => - "rose") + => + "rose") ;; Check a room user-directory with corresponding global user-directory, @@ -222,42 +222,42 @@ (channel-user-file-set! *dir* *new-room* "mawa" "nick" "mawarth") (user-file-set! *dir* "mawa" "nick" "magma") (check (and (not (symbolic-link? (subpath *new-room-all* "mawa"))) - (symbolic-link? (subpath *new-room-all* "mawa" "global")) - (directory-exists? (subpath *new-room-all* "mawa")) - (string? (directory-exists? (subpath *users-dir* "mawa"))) - ) - => - #t) + (symbolic-link? (subpath *new-room-all* "mawa" "global")) + (directory-exists? (subpath *new-room-all* "mawa")) + (string? (directory-exists? (subpath *users-dir* "mawa"))) + ) + => + #t) (check (user-file-get *dir* "mawa" "nick") - => - "magma") + => + "magma") (check (channel-user-file-get *dir* *new-room* "mawa" "nick") - => - "mawarth") + => + "mawarth") ;; Make sure user-states (online/offline) work! (channel-user-enable-state! *dir* *new-room* "mawa" "online") (check (read-symbolic-link (subpath *new-room-users* "online" "mawa")) - => - "../all/mawa") + => + "../all/mawa") (channel-user-toggle-states! *dir* *new-room* "mawa" "offline" "online") (check (list (symbolic-link? (subpath *new-room-users* "online" "mawa")) - (read-symbolic-link (subpath *new-room-users* "offline" "mawa"))) - => - '(#f "../all/mawa")) + (read-symbolic-link (subpath *new-room-users* "offline" "mawa"))) + => + '(#f "../all/mawa")) (channel-user-disable-state! *dir* *new-room* "mawa" "offline") (check (symbolic-link? (subpath *new-room-users* "offline" "mawa")) - => - #f) + => + #f) (user-enable-state! *dir* "mawa" "online") (check (list (symbolic-link? (subpath *new-room-users* "online" "mawa")) - (symbolic-link? (subpath *dir* *room* ".users" "online" "mawa"))) - => - '(#t #t)) + (symbolic-link? (subpath *dir* *room* ".users" "online" "mawa"))) + => + '(#t #t)) ;; ——————————————————————————————————————————————————