reddit-pub/reddit_pub/src/MyLib.hs

132 lines
4.3 KiB
Haskell
Raw Normal View History

2021-10-27 23:30:48 +03:00
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
2022-04-20 20:29:04 +03:00
{-# LANGUAGE NumericUnderscores #-}
2021-10-25 19:04:24 +03:00
{-# LANGUAGE OverloadedStrings #-}
2021-10-27 23:30:48 +03:00
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE LambdaCase #-}
2021-10-27 21:10:22 +03:00
module MyLib (defaultMain) where
2021-10-25 19:04:24 +03:00
2022-04-20 20:29:04 +03:00
import Control.Concurrent (threadDelay)
2021-10-25 19:04:24 +03:00
import Control.Exception (bracket)
import Control.Lens
2022-04-20 20:29:04 +03:00
import Control.Monad (forever, void)
2021-10-27 23:30:48 +03:00
import Data.Aeson (FromJSON, ToJSON, Value)
2021-10-27 21:10:22 +03:00
import qualified Data.Aeson as A
2022-04-20 20:29:04 +03:00
import Data.Aeson.Lens (_String, key)
import Data.Bool (bool)
2021-10-25 19:04:24 +03:00
import Data.Config
2021-10-27 23:30:48 +03:00
import Data.Deriving.Aeson
2021-10-27 21:10:22 +03:00
import Data.Foldable (for_)
import Data.Functor.Contravariant ((>$<))
import Data.Text (Text)
2022-04-20 20:29:04 +03:00
import qualified Data.Text.IO as TI
2021-10-25 19:04:24 +03:00
import qualified Data.Text.Strict.Lens as T
2021-10-27 23:30:48 +03:00
import qualified Database.SQLite.Simple as SQL
import GHC.Generics (Generic)
import qualified Membership
2021-10-25 19:04:24 +03:00
import Network.AMQP
2021-10-27 23:30:48 +03:00
( Channel
, DeliveryMode(Persistent)
2022-04-20 20:29:04 +03:00
, closeConnection
, declareExchange
2021-10-27 23:30:48 +03:00
, exchangeName
, exchangeType
, msgBody
, msgDeliveryMode
2022-04-20 20:29:04 +03:00
, newExchange
2021-10-27 23:30:48 +03:00
, newMsg
2022-04-20 20:29:04 +03:00
, openChannel
, openConnection
, publishMsg
2021-10-27 23:30:48 +03:00
)
2022-04-20 20:29:04 +03:00
import Network.Reddit (RedditId(RedditId), publishEntries)
2021-10-25 19:04:24 +03:00
import Network.Wreq.Session (newSession)
2022-05-16 21:42:11 +03:00
import Reddit.Publish (Publish(..))
import Text.Printf (printf)
import Data.ByteString.Lazy (ByteString)
2021-10-27 23:30:48 +03:00
data MessageType = Create | Update
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON, FromJSON)
data Message = Message
{ messageType :: MessageType
, messageIdentifier :: RedditId
, messageContent :: Value
}
deriving stock (Show, Eq, Generic)
deriving (ToJSON, FromJSON)
via AesonCodec (Field (CamelCase <<< DropPrefix "message")) Message
2021-10-25 19:04:24 +03:00
toMessage :: SQL.Connection -> Publish IO (Maybe Message) -> Publish IO Value
toMessage sqlConn (Publish p) = Publish $ \entry -> do
case RedditId <$> (entry ^? key "id" . _String) of
Nothing -> p Nothing
Just redditId -> do
event <- bool Create Update <$> Membership.isSeen sqlConn redditId
p $ Just $ Message event redditId entry
2021-10-25 19:04:24 +03:00
sqlRecorder :: SQL.Connection -> Publish IO (Maybe RedditId)
sqlRecorder conn = Publish $ maybe (pure ()) (Membership.recordSeen conn)
amqpPublisher :: Channel -> Text -> Publish IO (Maybe ByteString)
amqpPublisher channel exchange = Publish $ \case
Nothing -> pure ()
Just lbs ->
void $ publishMsg channel exchange routingKey (message lbs)
2021-10-25 19:04:24 +03:00
where
2021-10-27 21:10:22 +03:00
routingKey = "doesn't matter on fanout"
2021-10-25 19:04:24 +03:00
message lbs = newMsg
{ msgBody = lbs
, msgDeliveryMode = Just Persistent
}
stdoutPublisher :: Publish IO String
stdoutPublisher = Publish putStrLn
data Fetch
= Fetch Fetcher
| PublishMessage Message
| ParseFailed
fetchToLog :: Fetch -> String
fetchToLog (Fetch fetcher) = printf "Refreshing %s" (show $ fetcherSubreddit fetcher)
fetchToLog ParseFailed = printf "Failed parsing"
fetchToLog (PublishMessage msg) = messageToLog msg
where
messageToLog :: Message -> String
messageToLog m = printf "Publishing %s as type %s" (show $ messageIdentifier m) (show $ messageType m)
2022-02-10 21:47:09 +02:00
defaultMain :: FilePath -> IO ()
defaultMain path = do
conf <- readConfig path
2022-02-10 22:34:55 +02:00
pass <- getPassword (conf ^. amqp . password)
2021-10-27 23:21:09 +03:00
let rabbitConnect = openConnection
2021-10-25 19:04:24 +03:00
(conf ^. amqp . host . T.unpacked)
(conf ^. amqp . vhost)
(conf ^. amqp . username)
2022-02-10 22:34:55 +02:00
pass
2021-10-27 23:21:09 +03:00
bracket rabbitConnect closeConnection $ \conn -> do
SQL.withConnection (conf ^. sqlite) $ \sqlConn -> do
SQL.execute_ sqlConn "create table if not exists membership (reddit_id primary key)"
chan <- openChannel conn
declareExchange chan newExchange { exchangeName = "reddit_posts", exchangeType = "fanout" }
sess <- newSession
let encoder = amqpPublisher chan "reddit_posts"
2021-10-27 23:21:09 +03:00
recorder = sqlRecorder sqlConn
publisher = (fmap A.encode >$< encoder) <> (fmap messageIdentifier >$< recorder) <> (maybe ParseFailed PublishMessage >$< logger)
logger = fetchToLog >$< stdoutPublisher
2022-04-20 20:29:04 +03:00
forever $ do
for_ (conf ^. fetchers) $ \fetcher -> do
publish logger (Fetch fetcher)
publishEntries (toMessage sqlConn publisher) sess fetcher
2022-04-20 20:29:04 +03:00
threadDelay (15 * 60_000_000)
2022-02-10 22:34:55 +02:00
getPassword :: Password -> IO Text
getPassword (Password p) = pure p
getPassword (File path) = TI.readFile path