packages feed

htalkat 0.1.1 → 0.1.2

raw patch · 18 files changed

+303/−167 lines, 18 filesdep ~basedep ~bytestringdep ~cryptonite

Dependency ranges changed: base, bytestring, cryptonite, memory, mtl, text, time, transformers

Files

CHANGELOG.md view
@@ -1,1 +1,10 @@-0.1.1: First release+# htalkat Changelog++## 0.1.2+* Fix compatibility problems with older ghc; any base >= 4.9 should now work.+* Fix linebreaks in dumb client.+* Add per-command options mirroring most config options.+* Add options to use dumb client in place of curses client.++## 0.1.1+* First release
+ Command.hs view
@@ -0,0 +1,32 @@+-- This file is part of htalkat+-- Copyright (C) 2021 Martin Bays <mbays@sdf.org>+--+-- This program is free software: you can redistribute it and/or modify+-- it under the terms of version 3 of the GNU General Public License as+-- published by the Free Software Foundation, or any later version.+--+-- You should have received a copy of the GNU General Public License+-- along with this program.  If not, see http://www.gnu.org/licenses/.++module Command where++import           Data.Char (toLower)+import           Data.List (isPrefixOf)+import           Safe      (headMay)++data Command+    = Help+    | Identity+    | Name+    | Answer+    | Connect+    | Listen+    deriving (Eq,Ord,Show,Enum)++commands :: [Command]+commands = enumFrom Help++cmdOfStr :: String -> Maybe Command+cmdOfStr s = headMay [ c+    | c <- commands+    , s `isPrefixOf` (toLower <$> show c) ]
Config.hs view
@@ -10,18 +10,25 @@  module Config where +import           Safe             (readMay) import           System.Directory (doesFileExist) import           System.Exit      (exitFailure) import           System.FilePath  ((</>)) +import           Opts import           Util +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ data Config = Config     { listen_host        :: String     , listen_port        :: Int     , accept_unnamed     :: Bool     , curses_local_top   :: Bool     , curses_log         :: Bool+    , use_dumb_client    :: Bool     , interactive_client :: [String]     } deriving (Read) @@ -38,17 +45,32 @@     , "    , curses_local_top = True  # Display you above remote in curses client"     , "    , curses_log = False  # Log conversations to files in ~/.htalkat/logs/"     , ""+    , "    , use_dumb_client = False  # Prefer dumb line-based client to curses client"+    , ""     , "    # interactive_client: if non-empty, run in place of built-in client."     , "    # First string is the command to run, and subsequent strings are arguments."     , "    # It will be executed with two further arguments:"-    , "    # the name of the remote user,"-    , "    # and the path to a unix domain socket to interact with."+    , "    # the path to a unix domain socket to interact with,"+    , "    # and the name of the remote user."     , "    # Minimal example:"     , "    #, interactive_client = [\"sh\", \"-c\","-    , "    #    \"socat unix-connect:\\\"$2\\\" stdio\", \"talkatc\"]"+    , "    #    \"socat unix-connect:\\\"$1\\\" stdio\", \"talkatc\"]"     , "    , interactive_client = []"     , "    }"     ]++applyOptToConf :: Opt -> Config -> Config+applyOptToConf (Host h) conf = conf { listen_host = h }+applyOptToConf (Port pStr) conf | Just p <- readMay pStr = conf { listen_port = p }+applyOptToConf AcceptUnnamed conf = conf { accept_unnamed = True }+applyOptToConf BlockUnnamed conf = conf { accept_unnamed = False }+applyOptToConf DumbClient conf = conf { use_dumb_client = True }+applyOptToConf CursesClient conf = conf { use_dumb_client = False }+applyOptToConf LocalTop conf = conf { curses_local_top = True }+applyOptToConf LocalBottom conf = conf { curses_local_top = False }+applyOptToConf Log conf = conf { curses_log = True }+applyOptToConf NoLog conf = conf { curses_log = False }+applyOptToConf _ conf = conf  createConfigFileIfNecessary :: FilePath -> IO () createConfigFileIfNecessary ddir =
CursesClient.hs view
@@ -9,7 +9,6 @@ -- along with this program.  If not, see http://www.gnu.org/licenses/.  {-# LANGUAGE BangPatterns      #-}-{-# LANGUAGE CPP               #-} {-# LANGUAGE LambdaCase        #-} {-# LANGUAGE OverloadedStrings #-} @@ -47,6 +46,10 @@ import           System.Posix.Signals #endif +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ newtype Reversed = Reversed {getReversed :: T.Text}  data StreamState = StreamState@@ -430,6 +433,7 @@ eraseLast strm n = do     ss <- gets $ getSS strm     ww <- liftIO getWidth+    p <- gets (streamView . getSS strm)     let cur = getReversed $ streamCurLine ss         mightDecreaseHeight =             -- +1 to handle case of wrapping at double-width char@@ -438,7 +442,7 @@     modify . mapSS strm . mapCurLine $ T.drop n     if mightDecreaseHeight         then redrawStream strm-        else liftIO $ do+        else when (p == 0) . liftIO $ do             let win = streamWin ss             (y,x) <- getYX win             mvWAddStr win y (x - wErase) (replicate wErase ' ')
DumbClient.hs view
@@ -54,7 +54,7 @@ printWithErasures :: T.Text -> IO () printWithErasures = mapM_ (doLine . T.unpack) . T.lines where     doLine :: String -> IO ()-    doLine = foldM_ go []+    doLine s = foldM_ go [] s >> putChar '\n'      go [] '\b'    = pure []     go (h:t) '\b' = erase (charWidth h) >> pure t
Identity.hs view
@@ -8,7 +8,6 @@ -- You should have received a copy of the GNU General Public License -- along with this program.  If not, see http://www.gnu.org/licenses/. -{-# LANGUAGE CPP               #-} {-# LANGUAGE LambdaCase        #-} {-# LANGUAGE OverloadedStrings #-} @@ -46,6 +45,10 @@ import           User import           Util +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ data IdentityType = IdConnect | IdListen deriving Eq  loadIdentity :: FilePath -> IdentityType -> IO (Maybe Credential)@@ -98,8 +101,8 @@             secKey <- Ed25519.generateSecretKey             let promptCN = do                     putStrLn "Enter a public name for this identity (can be blank)."-                    putStrLn "This will be shown to anyone you connect to, and only to them."-                    putStrLn "(You can change this later by rerunning this command)"+                    putStrLn "This will be shown to anyone you connect to, but not to incoming callers."+                    putStrLn "(You can reset this name later with 'htalkat i NEW_NAME')"                     promptLine "Public name: "             cn <- maybe promptCN pure mCN             connectChain <- generateSelfSigned secKey cn
Incoming.hs view
@@ -25,6 +25,10 @@ import           Petname import           Util +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ type Incoming = Int  addIncoming :: FilePath -> Certificate -> FilePath -> Int -> IO Incoming
Notify.hs view
@@ -8,8 +8,6 @@ -- You should have received a copy of the GNU General Public License -- along with this program.  If not, see http://www.gnu.org/licenses/. -{-# LANGUAGE CPP #-}- module Notify where  import           Control.Monad      (void)@@ -25,6 +23,10 @@ import           Fingerprint import           Petname import           Util++#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif  notifyOfIncoming :: FilePath -> Certificate -> Petname -> IO () notifyOfIncoming ddir cert petname = do
Opts.hs view
@@ -10,20 +10,36 @@  {-# LANGUAGE Safe #-} -module Opts (help, parseArgs, Opt(..)) where+module Opts (globalHelp, localHelp, parseGlobal, parseLocal, Opt(..)) where  import           System.Console.GetOpt +import qualified Command               as C+ data Opt+    -- global:     = DataDir FilePath     | SocksHost String     | SocksPort String     | Help     | Version+    -- local:+    | Port String+    | Host String+    | BlockUnnamed+    | AcceptUnnamed+    | ListPending+    | SpawnInteractive FilePath+    | DumbClient+    | CursesClient+    | LocalTop+    | LocalBottom+    | Log+    | NoLog     deriving (Eq, Ord, Show) -options :: [OptDescr Opt]-options =+globalOptions :: [OptDescr Opt]+globalOptions =     [ Option ['d'] ["datadir"] (ReqArg DataDir "PATH") "default: ~/.htalkat, or $HTALKAT_DIR if set"     , Option ['S'] ["socks-host"] (ReqArg SocksHost "HOST") "use SOCKS5 proxy"     , Option ['P'] ["socks-port"] (ReqArg SocksPort "PORT") "port for SOCKS5 proxy (default: 1080)"@@ -31,11 +47,45 @@     , Option ['h'] ["help"] (NoArg Help) "show usage information"     ] -help :: String -> String-help = (`usageInfo` options)+localOptions :: C.Command -> [OptDescr Opt]+localOptions com =+    Option ['h'] ["help"] (NoArg Help) "show usage information" :+    case com of+        C.Listen ->+            [ Option ['p'] ["port"] (ReqArg Port "PORT") "Port to listen on (default: 5518)"+            , Option ['H'] ["host"] (ReqArg Host "HOST") "Host to listen on (empty means bind all available)"+            , Option ['b'] ["block-unnamed"] (NoArg BlockUnnamed) "Reject connections from unnamed users"+            , Option ['a'] ["accept-unnamed"] (NoArg AcceptUnnamed) "Accept connections from unnamed users (default)"+            ]+        C.Answer ->+            [ Option ['l'] ["list"] (NoArg ListPending) "List unanswered incoming connections"+            , Option ['i'] ["interactive-client"] (ReqArg SpawnInteractive "SOCKET_PATH") "Directly invoke interactive client"+            ] <> interactiveClientOptions+        C.Connect -> interactiveClientOptions+        _ -> []+    where interactiveClientOptions =+            [ Option ['d'] ["dumb"] (NoArg DumbClient) "Use dumb line-based client"+            , Option ['c'] ["curses"] (NoArg CursesClient) "Use curses client (default)"+            , Option ['t'] ["top"] (NoArg LocalTop) "Put you on top in curses client"+            , Option ['b'] ["bottom"] (NoArg LocalBottom) "Put you on bottom in curses client (default)"+            , Option ['L'] ["log"] (NoArg Log) "Log conversation"+            , Option ['N'] ["no-log"] (NoArg NoLog) "Don't log conversation (default)"+            ] -parseArgs :: [String] -> IO ([Opt],[String])-parseArgs argv =-    case getOpt RequireOrder options argv of+globalHelp :: String -> String+globalHelp = (`usageInfo` globalOptions)++localHelp :: C.Command -> String -> String+localHelp c = (`usageInfo` localOptions c)++parseArgs :: [OptDescr Opt] -> [String] -> IO ([Opt],[String])+parseArgs opts argv =+    case getOpt RequireOrder opts argv of         (o,n,[])   -> return (o,n)         (_,_,errs) -> ioError . userError $ concat errs++parseGlobal :: [String] -> IO ([Opt],[String])+parseGlobal = parseArgs globalOptions++parseLocal :: C.Command -> [String] -> IO ([Opt],[String])+parseLocal = parseArgs . localOptions
Petname.hs view
@@ -27,6 +27,10 @@ import           Mundanities import           User +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ data Petname = Named String | Unnamed Int     deriving (Eq) 
README.md view
@@ -1,9 +1,10 @@ # htalkat: Haskell client and server for talkat (Talk Across TLS) -## Building from source-Install the haskell package manager cabal; e.g. on a debian system:+# Building from source+Install the haskell package manager cabal, and the ncurses library and headers;+e.g. on a debian system: ```-$ sudo apt-get install cabal-install+$ sudo apt-get install cabal-install libncursesw5-dev ``` Then in the htalkat directory, run: ```@@ -11,10 +12,10 @@ ``` The resulting binary will be installed by default as `~/.cabal/bin/htalkat`. -### Compile-time options+## Compile-time options * `cabal install -f -curses`: compile without curses; a very simple dumb client is used instead. -## Basic usage+# Basic usage ``` # Create your cryptographic identity: htalkat i@@ -33,10 +34,10 @@ htalkat h ``` -## Configuration+# Configuration After first run, you can edit ~/.htalkat/htalkat.conf for general configuration options, and ~/.htalkat/notify.sh to set how the server notifies you of incoming connections. -## Portability+# Portability htalkat was written with POSIX systems in mind, and has only been tested on them, but it may also be possible to compile and run it on Windows. Please let me know if you try.  -- mbays@sdf.org
RelayStream.hs view
@@ -89,8 +89,7 @@         killThread decodeTTThread      _ <- takeMVar finished-    TLS.bye ctxt-    ignoreIOErr $ killThread sockThread+    ignoreIOErr $ TLS.bye ctxt >> killThread sockThread     tryTakeMVar sockMV >>= \case         Nothing   -> pure ()         Just sock -> S.gracefulClose sock 1000
TLSTalk.hs view
@@ -8,7 +8,6 @@ -- You should have received a copy of the GNU General Public License -- along with this program.  If not, see http://www.gnu.org/licenses/. -{-# LANGUAGE CPP               #-} {-# LANGUAGE LambdaCase        #-} {-# LANGUAGE OverloadedStrings #-} @@ -60,8 +59,11 @@  #ifdef CURSES import           CursesClient-#else+#endif import           DumbClient++#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup #endif  bindingNamedSocket :: FilePath -> (S.Socket -> IO a) -> IO a@@ -72,9 +74,8 @@         S.listen sock 1         pure sock -serve :: FilePath -> Credential -> IO ()-serve ddir cred = errorOnNoLock <=< withTryFileLock listenLockPath Exclusive $ \_ -> do-    conf <- loadConfig ddir+serve :: FilePath -> Config -> Credential -> IO ()+serve ddir conf cred = errorOnNoLock <=< withTryFileLock listenLockPath Exclusive $ \_ -> do     let params = def                 { serverShared = def { sharedCredentials = Credentials [cred] }                 , serverSupported = def@@ -110,20 +111,23 @@         S.setSocketOption sock S.NoDelay 1         context <- contextNew sock params         handshake context-        Just cert <- (takeTailCert =<<) <$> getClientCertificateChain context-        (if accept_unnamed conf then ((Just <$>) .) . lookupOrAddPetname else lookupPetname)-                ddir (spkiFingerprint cert) >>= \case+        mCert <- (takeTailCert =<<) <$> getClientCertificateChain context+        case mCert of             Nothing -> pure ()-            Just petname -> withSystemTempDirectory "htalkat" $ \tdir -> do-                let sockPath = tdir </> "sock"-                bindingNamedSocket sockPath $ \dSock -> do-                    -- Serial numbers ensure we don't delete the wrong dir-                    serial <- modifyMVar serialMVar $ \n -> pure (n+1,n)-                    incoming <- addIncoming ddir cert sockPath serial-                    notifyOfIncoming ddir cert petname-                    relayStream context WriteFirst dSock-                    withFileLock (incomingDir ddir </> ".lock") Exclusive $ \_ ->-                        cleanIncoming ddir (Just serial) incoming+            Just cert -> (if accept_unnamed conf+                    then ((Just <$>) .) . lookupOrAddPetname else lookupPetname)+                        ddir (spkiFingerprint cert) >>= \case+                Nothing -> pure ()+                Just petname -> withSystemTempDirectory "htalkat" $ \tdir -> do+                    let sockPath = tdir </> "sock"+                    bindingNamedSocket sockPath $ \dSock -> do+                        -- Serial numbers ensure we don't delete the wrong dir+                        serial <- modifyMVar serialMVar $ \n -> pure (n+1,n)+                        incoming <- addIncoming ddir cert sockPath serial+                        notifyOfIncoming ddir cert petname+                        relayStream context WriteFirst dSock+                        withFileLock (incomingDir ddir </> ".lock") Exclusive $ \_ ->+                            cleanIncoming ddir (Just serial) incoming     where     listenLockPath = ddir </> ".listen_lock"     errorOnNoLock :: Maybe a -> IO ()@@ -136,8 +140,8 @@     = NoSocksProxy     | Socks5Proxy String String -connect :: FilePath -> Credential -> String -> SocksProxy -> Host -> Fingerprint -> IO ()-connect ddir cred name socksProxy (Host hostname port) fp = do+connect :: FilePath -> Config -> Credential -> String -> SocksProxy -> Host -> Fingerprint -> IO ()+connect ddir conf cred name socksProxy (Host hostname port) fp = do     let serverId = if port == defaultTalkatPort             then BS.empty             else TS.encodeUtf8 . TS.pack . (':':) $ show port@@ -160,7 +164,6 @@         let path = tdir </> "sock"         bindingNamedSocket path $ \dSock -> do             _ <- forkIO $ relayStream context WriteSecond dSock-            conf <- loadConfig ddir             spawnInteractiveClient ddir conf name path     where     openSocket :: IO S.Socket@@ -196,8 +199,8 @@     , cipher_TLS13_AES128CCM_SHA256     ] -answerLast :: FilePath -> Maybe Fingerprint -> IO ()-answerLast ddir mFp = do+answerLast :: FilePath -> Config -> Maybe Fingerprint -> IO ()+answerLast ddir conf mFp = do     mInfo <- withFileLock (incomingDir ddir </> ".lock") Exclusive $ \_ -> do         lastIncoming ddir mFp >>= \case             Just incoming -> do@@ -208,7 +211,6 @@             Nothing -> pure Nothing     case mInfo of         Just (petname, sockPath) -> do-            conf <- loadConfig ddir             spawnInteractiveClient ddir conf (showPetname petname) sockPath         Nothing -> putStrLn "Nothing to answer." @@ -218,7 +220,7 @@         void . rawSystem command $ args ++ [name, sockPath]     | otherwise = #ifdef CURSES-        do+        if not $ use_dumb_client conf then do             mLog <- if curses_log conf                 then do                     createDirectoryIfMissing True $ ddir </> "logs"@@ -227,12 +229,11 @@                         "-" <> show epochSecs <.> "log") AppendMode                 else pure Nothing             cursesClient (curses_local_top conf) mLog name sockPath-#else-        dumbClient sockPath+        else #endif+        dumbClient sockPath -spawnDefaultInteractiveClient :: FilePath -> String -> FilePath -> IO ()-spawnDefaultInteractiveClient ddir name sockPath = do-    conf <- loadConfig ddir+spawnDefaultInteractiveClient :: FilePath -> Config -> String -> FilePath -> IO ()+spawnDefaultInteractiveClient ddir conf name sockPath = do     spawnInteractiveClient ddir (conf { interactive_client = [] })         name sockPath
Talkat.hs view
@@ -8,14 +8,12 @@ -- You should have received a copy of the GNU General Public License -- along with this program.  If not, see http://www.gnu.org/licenses/. -{-# LANGUAGE CPP        #-} {-# LANGUAGE LambdaCase #-}  module Main where  import           Control.Monad      (forM_, mplus, when)-import           Data.Char          (toLower)-import           Data.List          (isPrefixOf, sort)+import           Data.List          (sort) import           Data.Maybe         (fromMaybe, isJust) import           Safe               (headMay) import           System.Directory   (createDirectoryIfMissing,@@ -28,6 +26,7 @@ import           System.Posix.Files (ownerModes, setFileMode) #endif +import           Command import           Config import           Fingerprint import           Host@@ -41,35 +40,22 @@ import           Util import           Version -import qualified Opts--data Command-    = Help-    | Identity-    | Name-    | Answer-    | Connect-    | Listen-    deriving (Eq,Ord,Show,Enum)--commands :: [Command]-commands = enumFrom Help+import qualified Opts               as O -cmdOfStr :: String -> Maybe Command-cmdOfStr s = headMay [ c-    | c <- commands-    , s `isPrefixOf` (toLower <$> show c) ]+#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif  die :: String -> IO () die s = putStrLn s >> exitFailure  main :: IO () main = do-    (opts,args) <- Opts.parseArgs =<< getArgs-    when (Opts.Version `elem` opts) $ putStrLn version >> exitSuccess+    (opts,args) <- O.parseGlobal =<< getArgs+    when (O.Version `elem` opts) $ putStrLn version >> exitSuccess      ddir <- do-        let optDir = headMay [ path | Opts.DataDir path <- opts ]+        let optDir = headMay [ path | O.DataDir path <- opts ]         envDir <- lookupEnv "HTALKAT_DIR"         defDir <- (</> ".htalkat") <$> getHomeDirectory         pure . fromMaybe defDir $ optDir `mplus` envDir@@ -84,26 +70,30 @@         createNotifyScriptIfNecessary ddir      let socksProxy = maybe (const NoSocksProxy) Socks5Proxy-            (headMay [ h | Opts.SocksHost h <- opts ])-            . fromMaybe "1080" $ headMay [ p | Opts.SocksPort p <- opts ]+            (headMay [ h | O.SocksHost h <- opts ])+            . fromMaybe "1080" $ headMay [ p | O.SocksPort p <- opts ] -    let (mcmd,args') = if Opts.Help `elem` opts then (Just Help, args)+    let (mcmd,args') = if O.Help `elem` opts then (Just Help, args)             else (cmdOfStr =<< headMay args, drop 1 args) +    conf <- loadConfig ddir     case mcmd of         Nothing -> do             isConnectArg <- case args of                 [target] -> isJust <$> resolveTarget ddir target                 _        -> pure False             if isConnectArg-                then doCmd ddir socksProxy args Connect+                then doCmd ddir conf socksProxy [] args Connect                 else die "Unknown command/name. Use 'htalkat h' for help."-        Just cmd -> doCmd ddir socksProxy args' cmd+        Just cmd -> do+            (lOpts,lArgs) <- O.parseLocal cmd args'+            let conf' = foldr applyOptToConf conf lOpts+            doCmd ddir conf' socksProxy lOpts lArgs cmd -doCmd :: [Char] -> SocksProxy -> [String] -> Command -> IO ()-doCmd ddir socksProxy args = \case+doCmd :: FilePath -> Config -> SocksProxy -> [O.Opt] -> [String] -> Command -> IO ()+doCmd ddir conf socksProxy opts args = \case     Help -> case args of-        [] -> putStr . Opts.help . init . concat $ (<>"\n") <$>+        [] -> putStr . O.globalHelp . init . concat $ (<>"\n") <$>             [ "Usage: htalkat [OPTION...] COMMAND [ARG...]"             , ""             , "Commands:"@@ -122,60 +112,11 @@             , ""             , "Options:"             ]-        [c] | Just cmd <- cmdOfStr c -> mapM_ putStrLn $ case cmd of-            Help -> [ "htalkat h[elp] [COMMAND]"-                , "  Show help [on command]." ]-            Identity ->-                [ "htalkat i[dentity] [PUBLIC_NAME]"-                , "  Create new identity (prompting for public name if omitted),"-                , "  or show existing identity."-                , "  If PUBLIC_NAME is given and identity exists, change public name in identity."-                ]-            Name ->-                [ "htalkat n[ame] [talkat:]FP[@HOST] [NAME]"-                , "  Set NAME as a synonym for the user identified by the given fingerprint."-                , "  The name will be shown when receiving a call from the user."-                , "  If a host is specified, then NAME can be used with the c[onnect] command."-                , "  If NAME already exists, it will be overwritten."-                , "  If NAME is omitted, it will be prompted for; this makes a good URI handler."-                , "htalkat n[ame] NAME1 NAME2:"-                , "  As above, but setting NAME2 to whatever NAME1 is currently set to."-                , "  NAME1 may be of the form +N (+1, +2 etc); these pseudonames are"-                , "  automatically assigned to unknown incoming callers."-                , "htalkat n[ame]:"-                , "  List known names."-                , ""-                , "Names are saved as files in " <> show ddir </> "names" <> "."-                , "To delete, rename, or copy names, manipulate these files directly." ]-            Connect ->-                [ "htalkat c[onnect] NAME"-                , "  Connect to user at host as previously named with the n[ame] command."-                , "htalkat c[onnect] [talkat:]FP@HOST"-                , "  Call host. It is important to obtain the correct fingerprint of the person"-                , "  you intend to call, not just give whatever fingerprint is served by the host."-                , ""-                , "The command 'c[onnect]' can normally be omitted."-                , "NAME@HOST also works."-                ]-            Answer -> [ "htalkat a[nswer] [NAME]"-                , "  Answer most recent incoming call, restricting to calls from NAME if given."-                , ""-                , "htalkat a[nswer] --list"-                , "  List unanswered incoming connections."-                , ""-                , "htalkat a[nswer] --interactive-client NAME SOCKET_PATH"-                , "  Directly invoke interactive client,"-                , "  for use with the interactive_client config option."-                ]-            Listen -> [ "htalkat l[isten]"-                , "  Start server process which will listen for calls and announce them."-                , "  Other users will be able to connect to you at talkat:FP@HOST[:PORT],"-                , "  where FP is as given by i[dentity], HOST is your hostname or IP address,"-                , "  and PORT is a non-standard port if you set one."-                , "  See " <> ddir </> "listen.conf" <> " for configuration options,"-                , "  and " <> ddir </> "notify.sh" <> " to set up notifications." ]+        [c] | Just cmd <- cmdOfStr c -> putStr $ cmdHelp ddir cmd         _ -> pure () +    cmd | O.Help `elem` opts -> putStr $ cmdHelp ddir cmd+     Identity -> createOrShowIdentity ddir $ headMay args      Name -> case args of@@ -188,7 +129,7 @@                     name:_ -> die $ "Invalid name: " <> name                     [] -> do                         name <- promptLine $ "Enter name to assign to " <> showUser user <> ": "-                        doCmd ddir socksProxy [target,name] Name+                        doCmd ddir conf socksProxy [] [target,name] Name         [] -> do             names <- sort <$> loadNames ddir             forM_ names $ \name -> do@@ -198,19 +139,21 @@                     Just (User fp mh) -> showFingerprint fp <>                         maybe "" (("@" <>) . showHost) mh         _ -> die "Usage: htalkat n [talkat:]FP[@HOST[:PORT]] NAME; htalkat n NAME1 NAME2"-    Answer -> case args of-        s:_ | s `elem` ["-l","--list"] -> mapM_ putStrLn =<< listIncoming ddir-        [s,name,sockPath] | s `elem` ["-i","--interactive-client"] ->-            spawnDefaultInteractiveClient ddir name sockPath-        [target] ->++    Answer | O.ListPending `elem` opts -> mapM_ putStrLn =<< listIncoming ddir+    Answer | Just sockPath <- headMay [ p | O.SpawnInteractive p <- opts ] ->+        case args of+            [name] -> spawnDefaultInteractiveClient ddir conf name sockPath+            _      -> die "Usage: htalkat a -i SOCK_PATH NAME"+    Answer | [target] <- args ->             resolveTarget ddir target >>= \case                 Nothing          -> die $ "Unknown: " <> target-                Just (User fp _) -> answerLast ddir (Just fp)-        [] -> answerLast ddir Nothing-        _ -> die "Usage: htalkat a [--list] [NAME]"+                Just (User fp _) -> answerLast ddir conf (Just fp)+    Answer -> answerLast ddir conf Nothing+     Listen -> loadIdentity ddir IdListen >>= \case         Nothing   -> die "You must first create an identity with 'htalkat i'."-        Just cred -> serve ddir cred+        Just cred -> serve ddir conf cred     Connect -> loadIdentity ddir IdConnect >>= \case         Nothing -> die "You must first create an identity with 'htalkat i'."         Just cred -> case args of@@ -220,8 +163,59 @@                 Just (User _ Nothing) ->                     die $ "No host associated with '" <> target <> "'."                 Just (User fp (Just host)) ->-                    connect ddir cred name socksProxy host fp+                    connect ddir conf cred name socksProxy host fp                     where                     name | Just pet <- parsePetname target = showPetname pet                         | otherwise = showHost host             _ -> die "Usage: htalkat c NAME[@HOST]; htalkat c [talkat:]FP@HOST"++cmdHelp :: FilePath -> Command -> String+cmdHelp ddir c = O.localHelp c . unlines $ cmdHelp' c+    where+    cmdHelp' Help =+        [ "htalkat h[elp] [COMMAND]"+        , "  Show help [on command]." ]+    cmdHelp' Identity =+        [ "htalkat i[dentity] [PUBLIC_NAME]"+        , "  Create new identity (prompting for public name if omitted),"+        , "  or show existing identity."+        , "  If PUBLIC_NAME is given and identity exists, change public name in identity."+        ]+    cmdHelp' Name =+        [ "htalkat n[ame] [talkat:]FP[@HOST] [NAME]"+        , "  Set NAME as a synonym for the user identified by the given fingerprint."+        , "  The name will be shown when receiving a call from the user."+        , "  If a host is specified, then NAME can be used with the c[onnect] command."+        , "  If NAME already exists, it will be overwritten."+        , "  If NAME is omitted, it will be prompted for; this makes a good URI handler."+        , "htalkat n[ame] NAME1 NAME2:"+        , "  As above, but setting NAME2 to whatever NAME1 is currently set to."+        , "  NAME1 may be of the form +N (+1, +2 etc); these pseudonames are"+        , "  automatically assigned to unknown incoming callers."+        , "htalkat n[ame]:"+        , "  List known names."+        , ""+        , "Names are saved as files in " <> ddir </> "names" <> "."+        , "To delete, rename, or copy names, manipulate these files directly." ]+    cmdHelp' Connect =+        [ "htalkat c[onnect] NAME"+        , "  Connect to user at host as previously named with the n[ame] command."+        , "htalkat c[onnect] [talkat:]FP@HOST"+        , "  Call host. It is important to obtain the correct fingerprint of the person"+        , "  you intend to call, not just give whatever fingerprint is served by the host."+        , ""+        , "The command 'c[onnect]' can normally be omitted."+        , "NAME@HOST also works."+        ]+    cmdHelp' Answer =+        [ "htalkat a[nswer] [NAME]"+        , "  Answer most recent incoming call, restricting to calls from NAME if given."+        ]+    cmdHelp' Listen =+        [ "htalkat l[isten]"+        , "  Start server process which will listen for calls and announce them."+        , "  Other users will be able to connect to you at talkat:FP@HOST[:PORT],"+        , "  where FP is as given by i[dentity], HOST is your hostname or IP address,"+        , "  and PORT is a non-standard port if you set one."+        , "  See " <> ddir </> "listen.conf" <> " for configuration options,"+        , "  and " <> ddir </> "notify.sh" <> " to set up notifications." ]
TimedText.hs view
@@ -19,6 +19,10 @@ import qualified Data.Text.Lazy             as T import qualified Data.Text.Lazy.Encoding    as T +#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif+ type TimedText = [ Either Int Char ]  pauseMax :: Int
User.hs view
@@ -16,11 +16,15 @@     , showUser     ) where -import           Control.Monad (guard, msum)-import           Data.List     (stripPrefix)+import           Control.Monad  (guard, msum)+import           Data.List      (stripPrefix)  import           Fingerprint import           Host++#if !(MIN_VERSION_base(4,11,0))+import           Data.Semigroup+#endif  data User = User     { userFP   :: Fingerprint
Version.hs view
@@ -16,4 +16,4 @@ programName = "htalkat"  version :: String-version = "0.1.1"+version = "0.1.2"
htalkat.cabal view
@@ -1,6 +1,6 @@-cabal-version:      >=1.18+cabal-version:      1.18 name:               htalkat-version:            0.1.1+version:            0.1.2 license:            GPL-3 license-file:       COPYING maintainer:         mbays@sdf.org@@ -24,11 +24,13 @@  flag curses     description: Enable Curses UI+    manual: True  executable htalkat     main-is:          Talkat.hs     other-modules:         Certificate+        Command         Config         DumbClient         Fingerprint@@ -51,15 +53,16 @@         WCWidth      default-language: Haskell2010-    ghc-options:      -threaded -Wall+    default-extensions: CPP+    ghc-options:      -threaded -Wall -Wcompat     build-depends:-        base >=4.6 && <5,+        base >=4.9 && <5,         array >=0.3 && <0.6,         asn1-encoding <0.10,         asn1-types >=0.3.4 && <0.4,-        bytestring >=0.10.4.0 && <0.12,+        bytestring >=0.10.8.0 && <0.12,         containers >=0.5.5.1 && <0.7,-        cryptonite >=0.26 && <0.30,+        cryptonite >=0.26 && <0.31,         data-default-class >=0.1.2.0 && <0.2,         data-hash >=0.2.0.1 && <0.3,         directory >=1.2.1.0 && <1.4,@@ -67,8 +70,8 @@         filelock <0.2,         filepath >=1.3.0.2 && <1.5,         hourglass >=0.2.12 && <0.3,-        memory >=0.14 && <0.17,-        mtl >=2.0 && <2.3,+        memory >=0.14 && <0.18,+        mtl >=2.0 && <2.4,         network >=2.4.2.3 && <3.2,         network-simple >=0.4.3 && <0.5,         pem >=0.2.4 && <0.3,@@ -76,10 +79,10 @@         rset <1.1,         safe >=0.3.19 && <0.4,         temporary >= 1.2 && <1.4,-        text >=1.1.0.0 && <1.3,-        time <1.13,+        text >=1.1.0.0 && <2.1,+        time <1.14,         tls >=1.5.4 && <1.6,-        transformers >=0.3.0.0 && <0.6,+        transformers >=0.3.0.0 && <0.7,         x509 >=1.7.5 && <1.8,         x509-validation >=1.6.11 && <1.7