packages feed

matterhorn-40800.0.0: src/Config.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
module Config
  ( Config(..)
  , PasswordSource(..)
  , findConfig
  , getCredentials
  , defaultConfig
  ) where

import           Prelude ()
import           Prelude.Compat

import           Control.Applicative
import           Control.Monad (forM)
import           Control.Monad.Trans.Except
import           Data.Ini.Config
import qualified Data.Map.Strict as M
import           Data.Maybe (catMaybes, fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import           Data.Monoid ((<>))
import           System.Directory (makeAbsolute)
import           System.Process (readProcess)

import           IOUtil
import           FilePaths
import           Types
import           Types.KeyEvents

defaultPort :: Int
defaultPort = 443

fromIni :: IniParser Config
fromIni = do
  conf <- section "mattermost" $ do
    configUser           <- fieldMbOf "user" stringField
    configHost           <- fieldMbOf "host" stringField
    configTeam           <- fieldMbOf "team" stringField
    configPort           <- fieldDefOf "port" number (configPort defaultConfig)
    configChannelListWidth <- fieldDefOf "channelListWidth" number
                              (configChannelListWidth defaultConfig)
    configTimeFormat     <- fieldMbOf "timeFormat" stringField
    configDateFormat     <- fieldMbOf "dateFormat" stringField
    configTheme          <- fieldMbOf "theme" stringField
    configThemeCustomizationFile <- fieldMbOf "themeCustomizationFile" stringField
    configAspellDictionary <- fieldMbOf "aspellDictionary" stringField
    configURLOpenCommand <- fieldMbOf "urlOpenCommand" stringField
    configURLOpenCommandInteractive <- fieldFlagDef "urlOpenCommandIsInteractive" False
    configSmartBacktick  <- fieldFlagDef "smartbacktick"
      (configSmartBacktick defaultConfig)
    configShowOlderEdits <- fieldFlagDef "showOlderEdits"
      (configShowOlderEdits defaultConfig)
    configShowBackground <- fieldDefOf "showBackgroundActivity" backgroundField
      (configShowBackground defaultConfig)
    configShowMessagePreview <- fieldFlagDef "showMessagePreview"
      (configShowMessagePreview defaultConfig)
    configShowTypingIndicator <- fieldFlagDef "showTypingIndicator"
      (configShowTypingIndicator defaultConfig)
    configEnableAspell <- fieldFlagDef "enableAspell"
      (configEnableAspell defaultConfig)
    configActivityBell <- fieldFlagDef "activityBell"
      (configActivityBell defaultConfig)
    configHyperlinkingMode <- fieldFlagDef "hyperlinkURLs"
      (configHyperlinkingMode defaultConfig)
    configPass <- (Just . PasswordCommand <$> field "passcmd") <|>
                  (Just . PasswordString  <$> field "pass") <|>
                  pure Nothing
    configUnsafeUseHTTP <-
      fieldFlagDef "unsafeUseUnauthenticatedConnection" False

    let configAbsPath = Nothing
        configUserKeys = mempty
    return Config { .. }
  keys <- sectionMb "keybindings" $ do
    fmap (M.fromList . catMaybes) $ forM allEvents $ \ ev -> do
      kb <- fieldMbOf (keyEventName ev) parseBindingList
      case kb of
        Nothing      -> return Nothing
        Just binding -> return (Just (ev, binding))
  return conf { configUserKeys = fromMaybe mempty keys }

backgroundField :: T.Text -> Either String BackgroundInfo
backgroundField t =
  case t of
    "Disabled" -> Right Disabled
    "Active" -> Right Active
    "ActiveCount" -> Right ActiveCount
    _ -> Left ("Invalid value " <> show t
              <> "; must be one of: Disabled, Active, ActiveCount")

stringField :: T.Text -> Either String T.Text
stringField t =
    case isQuoted t of
        True -> Right $ parseQuotedString t
        False -> Right t

parseQuotedString :: T.Text -> T.Text
parseQuotedString t =
    let body = T.drop 1 $ T.init t
        unescapeQuotes s | T.null s = s
                         | "\\\"" `T.isPrefixOf` s = "\"" <> unescapeQuotes (T.drop 2 s)
                         | otherwise = (T.singleton $ T.head s) <> unescapeQuotes (T.drop 1 s)
    in unescapeQuotes body

isQuoted :: T.Text -> Bool
isQuoted t =
    let quote = "\""
    in (quote `T.isPrefixOf` t) &&
       (quote `T.isSuffixOf` t)

defaultConfig :: Config
defaultConfig =
    Config { configAbsPath                   = Nothing
           , configUser                      = Nothing
           , configHost                      = Nothing
           , configTeam                      = Nothing
           , configPort                      = defaultPort
           , configPass                      = Nothing
           , configTimeFormat                = Nothing
           , configDateFormat                = Nothing
           , configTheme                     = Nothing
           , configThemeCustomizationFile    = Nothing
           , configSmartBacktick             = True
           , configURLOpenCommand            = Nothing
           , configURLOpenCommandInteractive = False
           , configActivityBell              = False
           , configShowBackground            = Disabled
           , configShowMessagePreview        = False
           , configEnableAspell              = False
           , configAspellDictionary          = Nothing
           , configUnsafeUseHTTP             = False
           , configChannelListWidth          = 20
           , configShowOlderEdits            = True
           , configUserKeys                  = mempty
           , configShowTypingIndicator       = False
           , configHyperlinkingMode          = True
           }

findConfig :: Maybe FilePath -> IO (Either String Config)
findConfig Nothing = do
    cfg <- locateConfig configFileName
    case cfg of
        Nothing -> return $ Right defaultConfig
        Just path -> getConfig path
findConfig (Just path) = getConfig path

getConfig :: FilePath -> IO (Either String Config)
getConfig fp = runExceptT $ do
  absPath <- convertIOException $ makeAbsolute fp
  t <- (convertIOException $ T.readFile absPath) `catchE`
       (\e -> throwE $ "Could not read " <> show absPath <> ": " <> e)
  case parseIniFile t fromIni of
    Left err -> do
      throwE $ "Unable to parse " ++ absPath ++ ":" ++ err
    Right conf -> do
      actualPass <- case configPass conf of
        Just (PasswordCommand cmdString) -> do
          let (cmd:rest) = T.unpack <$> T.words cmdString
          output <- convertIOException (readProcess cmd rest "") `catchE`
                    (\e -> throwE $ "Could not execute password command: " <> e)
          return $ Just $ T.pack (takeWhile (/= '\n') output)
        Just (PasswordString pass) -> return $ Just pass
        Nothing -> return Nothing

      return conf { configPass = PasswordString <$> actualPass
                  , configAbsPath = Just absPath
                  }

-- | Returns the hostname, username, and password from the config. Only
-- returns Just if all three have been provided. The idea is that if
-- this returns Nothing, we're missing at least some of these values.
getCredentials :: Config -> Maybe ConnectionInfo
getCredentials config = do
    pass <- configPass config
    passStr <- case pass of
        PasswordString p -> return p
        PasswordCommand _ ->
            error $ "BUG: unexpected credentials state: " <>
                    show (configPass config)

    ConnectionInfo <$> configHost config
                   <*> (pure $ configPort config)
                   <*> configUser config
                   <*> (pure passStr)