summaryrefslogtreecommitdiff
path: root/src/Config.hs
blob: 7fe9e6a09e2034554cf3c4217bcff03d907c6a6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeData #-}
{-# LANGUAGE TypeFamilies #-}
module Config (
  Config, Config'(..), ConfigStage(..), IfFull,
  Channel(..),
  readConfig, enrichConfig,
) where

import Data.Char (isSpace)
import Data.List (foldl')
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Text.Read (readMaybe)

import Util


-- Example config file:
--
-- ```
-- port 8080
-- logs /var/lib/znc/users/ircbrowse/moddata/log
-- channel freenode #haskell fn-haskell
-- channel liberachat #haskell haskell
-- channel liberachat #xmonad xmonad
-- ```
--
-- Lines starting with '#' are ignored.


type Config = Config' Full

-- | Whether a 'Config' is straight from the 'User', or enriched with more
-- information ('Full').
type data ConfigStage = User | Full

data Config' (stage :: ConfigStage) = Config
  { uconfChannels :: IfUser stage (Snoc (Text, Text, Text))
      -- ^ (network, channel, alias); channel includes '#'. Alias is used for
      -- URLs and must be globally unique.

  , confLogsDir :: FilePath
  , confPort :: Int
      -- ^ Port at which to host the HTTP server

  , econfChannels :: IfFull stage [Channel]
      -- ^ Network-channel pairs, in user-specified order
  , econfChan2Alias :: IfFull stage (Map Channel Text)
      -- ^ The URL alias of a network-channel pair
  , econfAlias2Chan :: IfFull stage (Map Text Channel)
      -- ^ The network-channel pair corresponding to each alias
  }

deriving instance Show (Config' User)
deriving instance Show (Config' Full)

type family IfUser stage a where
  IfUser User a = a
  IfUser Full a = ()

type family IfFull stage a where
  IfFull User a = ()
  IfFull Full a = a

-- | network, channelname
data Channel = Channel { chanNetwork :: Text, chanChannel :: Text }
  deriving (Show, Eq, Ord)

readConfig :: FilePath -> IO (Config' User)
readConfig path = foldl' parseLine initConfig . lines <$> readFile path

enrichConfig :: Config' User -> Config
enrichConfig conf = conf
  { uconfChannels = ()
  , econfChannels = [Channel nw ch | (nw, ch, _) <- uchannels]
  , econfChan2Alias = Map.fromList [(Channel nw ch, alias) | (nw, ch, alias) <- uchannels]
  , econfAlias2Chan = Map.fromList [(alias, Channel nw ch) | (nw, ch, alias) <- uchannels]
  }
  where
    uchannels = toList (uconfChannels conf)

initConfig :: Config' User
initConfig = Config
  { uconfChannels = SnocNil
  , confLogsDir = error "config: Log directory not set with 'logs'"
  , confPort = 8000
  , econfChannels = ()
  , econfChan2Alias = ()
  , econfAlias2Chan = ()
  }

parseLine :: Config' User -> String -> Config' User
parseLine conf line  -- skip empty lines and comments
  | case dropWhile isSpace line of
      [] -> True
      '#':_ -> True
      _ -> False
  = conf
parseLine conf line =
  let (cmd, rest) = break (== ' ') line
  in case cmd of
       "port" ->
         case readMaybe (trim rest) of
           Just port | 0 <= port, port < 65536 -> conf { confPort = port }
           _ -> error "config: Invalid port number"
       "logs" ->
         let dir = dropWhile isSpace rest
         in if null dir
              then error "config: Empty 'logs' directory"
              else conf { confLogsDir = dropWhile isSpace rest }
       "channel" ->
         case words (trim rest) of
           [network, chan, alias]
             | all (not . null) [network, chan, alias]
             -> conf { uconfChannels = uconfChannels conf `Snoc` (T.pack network, T.pack chan, T.pack alias) }
           _ -> error $ "config: Invalid channel spec: " ++ trim rest
       _ -> error $ "config: Invalid line with command '" ++ cmd ++ "'"
  where
    trim :: String -> String
    trim = dropWhile isSpace . reverse . dropWhile isSpace . reverse