aboutsummaryrefslogtreecommitdiff
path: root/src/BangState.hs
blob: 1b7664909b8622843cc8c1554a0a052168178b10 (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
{-# 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
    -- we may want to actually poll this. oops.
    ans <- httpJSON "https://duckduckgo.com/bang.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