packages feed

alto-0: src/Alto/Compile.hs

{-# LANGUAGE OverloadedStrings
  , ScopedTypeVariables
  , TupleSections
  , FlexibleContexts
 #-}
module Alto.Compile where

import           Alto.Menu
import           Control.Lens
import qualified Control.Monad.Catch as E
import           Control.Monad.Writer
import           Control.Monad.State
import qualified Crypto.Hash.SHA256 as SHA256
import           Crypto.Scrypt (ScryptParams)
import qualified Crypto.Scrypt as Scrypt
import qualified Data.ByteString.Base64.URL as B64
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Map as Map
import           Data.Maybe (fromJust)
import qualified Data.Set as Set
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import           System.RandomString

saltDerivingParams :: ScryptParams
saltDerivingParams = Scrypt.defaultParams

type MenuM a = StateT CompState IO a
type EntryM a = WriterT [MenuEntry] (StateT CompState IO) a

-- | Loads a subgraph if it exists, otherwise compiles it.
subGraph :: Text -> MenuM Menu -> MenuM Menu
subGraph sgName desc =
  E.catch (lift $ refSubGraph sgName) $ \(_::E.SomeException) -> do
    ms <- lift $ compileRoot sgName desc
    lift $ saveSubGraph sgName ms
    return . fromJust $ ms^.menuMap.at (ms^.topMenu.mid)

-- | Compiles a MenuSystem given a name we produce a salt from.
--   Any menu systems sharing tags must agree on the project name.
compileRoot :: Text -> MenuM Menu -> IO MenuSystem
compileRoot name desc = do
  let compSalt = B64.encode . Scrypt.getEncryptedPass . Scrypt.encryptPass
                 saltDerivingParams (Scrypt.Salt $ TE.encodeUtf8 "Jektulv!OCod3gob6Glaj@") .
                 Scrypt.Pass . TE.encodeUtf8 $ name
  (rm, (CSt _ mnmp _)) <- desc `runStateT` (CSt compSalt mempty mempty)
  return $ MenuSystem mnmp rm

idBytes :: Int
idBytes = 128 `div` 8

-- | Generate a (hopefully) unique ID based off the name, pseudo-salted from the root name.
--   The root derived pseudo salt is expensively generated to make guessing attacks
--   fairly unreasonable. Truly though this is just to keep the honest honest.
genTagID :: MonadState CompState m => Text -> m Tag
genTagID nm = do
  ss <- use salt
  -- If our parts encode the same, we are the same.
  return . T.init . TE.decodeUtf8 . B64.encode . BS.take idBytes .
    SHA256.hashlazy .
    BSL.fromChunks $ [TE.encodeUtf8 nm, ss]

genMenuID :: MenuM MenuID
genMenuID = lift $ randomString (StringOpts Base58 idBytes)

-- | Import a menu system for use in this menu system.
--   Returns the root of said menu system.
importMenuSystem :: MenuSystem -> MenuM Menu
importMenuSystem ms = do
  menus <>= (ms ^. menuMap)
  return (ms ^. topMenu)

runEntryM :: EntryM () -> MenuM [MenuEntry]
runEntryM = execWriterT

updateEntries :: Menu -> EntryM () -> MenuM ()
updateEntries m entry =
  void $ updateEntries' m entry

updateEntries' :: Menu -> EntryM () -> MenuM Menu
updateEntries' m entry = do
  ents <- runEntryM entry
  let
    m' = m & entries <>~ ents
  updateMenu m'
  return m'

menu' :: TagChange -> EntryM () -> MenuM Menu
menu' exitTC ents = do
  es <- runEntryM ents
  cid <- genMenuID
  let mn = Menu cid exitTC es
  -- Make sure this ID isn't already in use.
  omns <- menus <<%= (Map.insert cid mn)
  when (cid `Map.member` omns) $
    error ("The menu "<>(show es)<>" was already in used!")
  return $ mn

menu :: EntryM () -> MenuM Menu
menu = menu' mempty

updateMenu :: Menu -> MenuM ()
updateMenu m =
  menus #%= (Map.insert (m ^. mid) m)

uniqueTag :: MonadState CompState m => Text -> m Tag
uniqueTag t = do
  tid <- genTagID t
  existed <- use $ tags.contains tid
  when existed $ error "Tag already existed!"
  return tid

-- | Add an entry to the menu
ent :: MenuEntry -> EntryM ()
ent = tell . pure

mnAction :: Menu -> EntryType
mnAction m = SubMenu mempty (m^.mid) Nothing

andLogic :: TagLogic -> TagLogic -> TagLogic
andLogic nl Always = nl
andLogic nl (TLAnd ol) = TLAnd $ nl:ol
andLogic nl ol = TLAnd [nl, ol]

-- | Require a tag to be set for a menu entry to be displayed.
infixl 5 &+
(&+) :: MenuEntry -> Tag -> MenuEntry
(&+) e t = e & display %~ andLogic (TagSet t)

-- | Require a tag to be unset for a menu entry to be displayed.
infixl 5 &-
(&-) :: MenuEntry -> Tag -> MenuEntry
(&-) e t = e & display %~ andLogic (TagUnset t)

-- | Requires a specific tag logic to be true.
infixl 5 &=
(&=) :: MenuEntry -> TagLogic -> MenuEntry
(&=) e tl = e & display %~ andLogic tl

-- Make a MenuEntry link to a submenu
-- (|-$) :: MenuEntry -> MenuM Menu -> MenuM MenuEntry

infixl 5 |->
(|->) :: MenuEntry -> Menu -> MenuEntry
(|->) e m = e & reaction .~ SubMenu (e^.reaction.onAction) (m ^. mid) Nothing

infixl 5 |-=
(|-=) :: MenuEntry -> Action -> MenuEntry
(|-=) e a = e & reaction .~ Action (e^.reaction.onAction) (Just a)

infixl 5 |-==
(|-==) :: MenuEntry -> EntryType -> MenuEntry
(|-==) e a = e & reaction .~ (a&onAction.~(e^.reaction.onAction))

infixl 5 |-//
(|-//) :: MenuEntry -> Text -> MenuEntry
(|-//) e u = e |-= (Nav u)

infixl 5 |-#
(|-#) :: MenuEntry -> Text -> EmbedSize -> MenuEntry
(|-#) e u es = e |-= (Embed u es)

-- | Make a MenuEntry set a tag.
infixl 5 |-+
(|-+) :: MenuEntry -> Tag -> MenuEntry
(|-+) e t = e & reaction.onAction.setTags <>~ (Map.singleton t "")

-- | Make a MenuEntry sset a number of tags.
infixl 5 |-+*
(|-+*) :: MenuEntry -> [Tag] -> MenuEntry
(|-+*) e t = e & reaction.onAction.setTags <>~ (Map.fromList . map (,"") $ t)

-- | Make a MenuEntry set a tag.
infixl 5 |-+=
(|-+=) :: MenuEntry -> Tag -> Text -> MenuEntry
(|-+=) e t v = e & reaction.onAction.setTags <>~ (Map.singleton t v)

infixl 5 |-<>
(|-<>) :: MenuEntry -> TagChange -> MenuEntry
(|-<>) e tc = e & reaction.onAction <>~ tc

-- | Make a MenuEntry unset a tag.
infixl 5 |--
(|--) :: MenuEntry -> Tag -> MenuEntry
(|--) e t = e & reaction.onAction.unsetTags <>~ (Set.singleton t)

-- | Make a MenuEntry unset a number of tags.
infixl 5 |--*
(|--*) :: MenuEntry -> [Tag] -> MenuEntry
(|--*) e t = e & reaction.onAction.unsetTags <>~ (Set.fromList t)

-- | Like |-> but generates where it links off the value of a tag.
infixl 5 |=>
(|=>) :: MenuEntry -> Text -> Tag -> MenuEntry
(|=>) e mpre tg = e & reaction .~ SubMenu (e^.reaction.onAction) mpre (Just tg)