{-# LANGUAGE OverloadedStrings, LambdaCase #-} module BangState ( BangState, initBangState, getBangsJSON, getBangsBrotli, addBang, getBangsHash, ) where import Codec.Compression.Brotli (compress) import Control.Applicative ((<|>)) import Control.Concurrent.STM.TQueue import Control.Concurrent.STM.TVar import Control.Monad (forever) import Crypto.Hash (hashlazy, Digest, SHA512) import Data.Aeson import Data.ByteArray.Encoding (convertToBase, Base(Base64)) import Data.ByteString (ByteString, toStrict, empty, take) import Data.Text (Text) import Data.Text.Encoding (decodeASCII) import GHC.Conc import Network.HTTP.Simple (Response, httpJSON, getResponseBody) import System.AtomicWrite.Writer.LazyByteString (atomicWriteFile) import Data.Bang data BangState = BangState { ownBangs :: TVar Bangs, ddgBangs :: TVar Bangs, serializedBangs :: TVar ByteString, brotliBangs :: TVar ByteString, serializedHash :: TVar Text, syncFileNotifications :: TQueue () } reserialize :: BangState -> STM () reserialize s = do own <- readTVar $ ownBangs s ddg <- readTVar $ ddgBangs s let jsonBs = encode $ own <> ddg -- left biased union writeTVar (serializedBangs s) $ toStrict $ jsonBs writeTVar (brotliBangs s) $ toStrict $ compress jsonBs -- hash: for now, base64 of the first 120 bits of the SHA512 of the bangs writeTVar (serializedHash s) $ decodeASCII $ Data.ByteString.take 20 $ convertToBase Base64 $ (hashlazy jsonBs :: Digest SHA512) writeTQueue (syncFileNotifications s) () -- spawns a thread for syncing the current state to disk. do *NOT* use forkIO on this -- function, it does so internally, and using forkIO may result in a (hard to trigger) -- race condition spawnFileSyncThread :: BangState -> IO () spawnFileSyncThread s = do _ <- forkIO $ forever $ do nextBangs <- atomically $ do readTQueue $ syncFileNotifications s readTVar (ownBangs s) atomicWriteFile "bangs.json" $ encode nextBangs atomically $ reserialize s loadOwnBangs :: IO Bangs loadOwnBangs = eitherDecodeFileStrict "bangs.json" <|> return (Left "") >>= \case Left _ -> return mempty Right b -> return b -- also spawns a thread for disk synchronization initBangState :: IO BangState initBangState = do -- TODO error handling for ddg bang polling, as well as regular polling ans <- httpJSON "https://duckduckgo.com/bang.v260.js" :: IO (Response DDGBangs) s <- BangState <$> (loadOwnBangs >>= newTVarIO) <*> newTVarIO (toBangs $ getResponseBody ans) <*> newTVarIO empty -- initially filled in by 'spawnFileSyncThread' <*> newTVarIO empty -- same as above <*> newTVarIO "" -- same as above <*> newTQueueIO spawnFileSyncThread s return s getBangsJSON :: BangState -> IO ByteString getBangsJSON s = readTVarIO $ serializedBangs s getBangsBrotli :: BangState -> IO ByteString getBangsBrotli s = readTVarIO $ brotliBangs s addBang :: Text -> Text -> Text -> BangState -> IO () addBang bn bu bdp s = atomically $ do modifyTVar (ownBangs s) (singletonBangs bn bu bdp <>) reserialize s getBangsHash :: BangState -> IO Text getBangsHash s = readTVarIO $ serializedHash s