#!/usr/bin/env hy ;; Copyright (C) 2019 Jakob L. Kreuze ;; ;; This program is free software: you can redistribute it and/or modify it under ;; the terms of the GNU General Public License as published by the Free Software ;; Foundation, either version 3 of the License, or (at your option) any later ;; version. ;; ;; This program is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS ;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more ;; details. ;; ;; You should have received a copy of the GNU General Public License along with ;; this program. If not, see . "brisket.hy: Script for interacting with NNTP servers." (setv __author__ "Jakob L. Kreuze") (setv __copyright__ "Copyright (C) 2019 Jakob L. Kreuze") (setv __license__ "GPL") (setv __version__ "0.1.0: Kansas City-style") (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" "version"]) (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 version [&rest args] "Display the version of Brisket the user is currently running. usage: version" (print (.format "brisket version {} ________ < Uhm... > -------- \ ^__^ \ (oo)\\_______ (__)\\ )\\/\\ ||----w | || ||" __version__))) (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 describe-group [session &rest args] "Describe the first group matching the pattern. usage: describe-groups [pattern]" (when (< (len args) 1) (err "usage: describe-group [group]") (return)) (let [group-name (first args) description (.description session group-name)] (if (= (len description) 0) (err "ERROR: Group does not have a description.") (print description)))) (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' and 'version', as the commands doesn't require a ;; connection to the NNTP server. (cond [(= command "help") (help #* (drop 2 argv))] [(= command "version") (version #* (drop 2 argv))] [True (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 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"), ;; ))