(import [configparser [ConfigParser]]) (import [os [getenv makedirs]]) (import [os.path [dirname expanduser]]) (import [sys [argv exit stderr]]) (import [re [match]]) (import [nntplib [NNTP-SSL]]) (import [subprocess [Popen PIPE]]) (require [hy.contrib.walk [let]]) (setv *auth-source* (expanduser "~/.authinfo.gpg")) (setv *config-path* (expanduser "~/.config/brisket/config.ini")) (setv *recognized-commands* ["describe-group" "help" "list-group" "list-groups"]) (defn help [&rest args] "Display the documentation for a command to stdout. usage: help [command]" (unless (= (len args) 1) (err (.format "usage: help [command]")) (return)) (let [command (first args)] (unless (in command *recognized-commands*) (err (.format "unrecognized command: {}" command)) (exit 1)) (let [handler (get (globals) (.replace command "-" "_"))] (print (. handler __doc__))))) (defn list-groups [session &rest args] "List groups on the server, matching a pattern if one is given. usage: list-groups [pattern (optional)]" (let [pattern (if (>= (len args) 1) (nth args 0)) groups (second (.list session))] (for [group groups] (if (or (is pattern None) (match pattern group.group)) (print group.group))))) (defn make-config [] "Create a default configuration file. Creates the configuration directory if it does not exist, as well as an INI file populated with default configuration values." (let [config (ConfigParser)] (assoc config "Server" {}) (assoc (get config "Server") "Host" "news.eternal-september.org") (makedirs (dirname *config-path*)) (with [out (open *config-path* "w+")] (.write config out)) config)) (defn load-config [] "Load the configuration file for Brisket. Returns a default configuration, written to `*config-path*`, if a configuration file cannot be found." (let [config (ConfigParser)] (if (= 1 (len (.read config *config-path*))) config (make-config)))) (defn find-host [config] "Returns the host that Brisket should connect to. The lookup order is: - NNTPSERVER environment variable. - 'Host' in the 'Server' section of the configuration file." (or (getenv "NNTPSERVER") (if (and (in "Server" config) (in "Host" (get config "Server"))) (get (get config "Server") "Host")))) (defn get-user-info [host] "Decode *auth-source* and return the line containing HOST." (let [process (Popen ["gpg" "-q" "-d" *auth-source*] :stdout PIPE) output (.decode (first (.communicate process)))] (for [line (.split output "\n")] (when (.startswith line (.format "machine {}" host)) (return line))))) (defn get-username [host] "Return the username for HOST as specified by *auth-source*." (let [info (get-user-info host) start-index (.find info "login")] (unless (= -1 start-index) (let [sliced (.split (.join "" (drop start-index info)))] (if (> (len sliced) 1) (.strip (nth sliced 1) "\"")))))) (defn get-password [host] "Return the password for HOST as specified by *auth-source*." (let [info (get-user-info host) start-index (.find info "password")] (unless (= -1 start-index) (let [sliced (.split (.join "" (drop start-index info)))] (if (> (len sliced) 1) (.strip (nth sliced 1) "\"")))))) (defn err [&rest args] "Displays ARGS to stderr." (.write stderr (+ (first args) "\n") #* (rest args))) (when (= __name__ "__main__") (unless (>= (len argv) 2) (err (.format "usage: {} [command] [args]" (first argv))) (err "recognized commands:") (for [command *recognized-commands*] (err (.format " - {}" command))) (exit 1)) (let [config (load-config) host (find-host config) command (nth argv 1)] (when (is host None) (err (.format "no host specified")) (exit 1)) (unless (in command *recognized-commands*) (err (.format "unrecognized command: {}" command)) (exit 1)) ;; Special case for 'help', as it's the one command that doesn't require a ;; connection to the NNTP server. (if (= command "help") (help #* (drop 2 argv)) (let [user (get-username host) password (get-password host)] (with [session (NNTP_SSL host :user user :password password)] ;; Otherwise, pull the function out of the global namespace. (let [handler (get (globals) (.replace command "-" "_"))] (handler session #* (drop 2 argv)))))))) ;; # To-be-converted Python ;; ;; def describe_group(n, *args): ;; if len(args) < 1: ;; err("usage: describe-group [group]") ;; return ;; ;; _, descs = n.descriptions(args[0]) ;; ;; for desc in descs: ;; print(desc) ;; ;; def list_group(n, *args): ;; if len(args) < 1: ;; err("usage: list-group [group]") ;; return ;; ;; _, _, first, last, _ = n.group(args[0]) ;; _, overviews = n.over((first, last)) ;; ;; for article_num, overview in overviews: ;; print("\"{}\" by {} on {}".format( ;; overview.get("subject"), ;; overview.get("from"), ;; overview.get("date"), ;; ))