{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Puppy.Database (
connect,
Connection,
DB,
runDB,
transaction,
getActorById,
getChannelByActorId,
getActivityById,
getObjectById,
getUserByName,
insertNewUser,
insertNewChannel,
insertNewActor,
insertNewActivity,
insertFollowRequest,
acceptFollowRequest,
cancelFollowRequest,
storeActivity,
) where
import Control.Monad ((>=>))
import Database.SQLite.Simple (Connection, query, Only (..), execute, ToRow (..), withTransaction, open)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.FromRow
import Database.SQLite.Simple.ToField (ToField (..))
import Data.Functor ((<&>), ($>))
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.Time
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Fail (Fail)
import Effectful.TH (makeEffect)
import Prelude hiding (id)
import Puppy.Protocol.ActivityStreams
import Puppy.Types
import qualified Data.Aeson as JSON
import qualified Data.UUID as UUID
import qualified Puppy.Crypto.RSA as RSA
data DB :: Effect where
GetActorById :: Id -> DB m (Maybe Actor)
GetChannelByActorId :: Id -> DB m (Maybe Channel)
GetUserByName :: Text -> DB m (Maybe User)
GetActivityById :: Id -> DB m (Maybe (Activity Id))
InsertNewUser :: User -> DB m ()
InsertNewChannel :: Channel -> DB m ()
InsertNewActor :: Actor -> DB m ()
InsertNewActivity :: Activity Id -> DB m ()
InsertNewDocument :: Document -> DB m ()
Transaction :: m a -> DB m a
InsertFollowRequest :: (Id, Id) -> DB m ()
AcceptFollowRequest :: (Id, Id) -> DB m ()
CancelFollowRequest :: (Id, Id) -> DB m ()
makeEffect ''DB
getObjectById :: DB :> es => Id -> Eff es (Maybe Object)
getObjectById objId = getActivityById objId >>= \case
Just activity -> getObjectById activity.object <&> fmap
(\(obj :: Object) -> toObject (activity $> obj))
Nothing -> getActorById objId <&> fmap toObject
storeActivity :: (DB :> es) => Activity Object -> Eff es ()
storeActivity a =
storeObject a.object >> insertNewActivity (a <&> (.id))
where
storeObject obj
| Just o <- fromObject obj = insertNewActor o
| Just o <- fromObject obj = insertNewDocument o
| Just o <- fromObject obj = storeActivity o
| otherwise = error "impossible!"
connect :: (IOE :> es) => Eff es Connection
connect = liftIO $ open ".state/db.sqlite"
runDB
:: (IOE :> es, Fail :> es)
=> Connection
-> Eff (DB : es) a
-> Eff es a
runDB conn = interpret $ \env a -> do
localSeqUnliftIO env $ \unlift -> case a of
GetActorById actorId -> just <$> query conn
"select id, inbox, outbox, followers, following, accountName, displayName, bio, keyId, publicKeyPem, locked, type from actors where id = ?"
(Only actorId)
GetChannelByActorId actorId -> just <$> query conn
"select id, linkedActorId, privateKeyPem from channels where linkedActorId = ?"
(Only actorId)
GetUserByName userName -> just <$> query conn
"select id, userName from users where userName = ?"
(Only userName)
GetActivityById activityId -> just <$> query conn
"select id, actor, object, audienceTo, audienceCc, time, type from activities where id = ?"
(Only activityId)
InsertNewUser (User { userId, userName, mainChannel }) -> execute conn
"insert into users (id, userName, mainChannel) values (?, ?, ?)"
(userId, userName, mainChannel)
InsertNewChannel (Channel { channelId, linkedActorId, privateKeyPem }) -> execute conn
"insert into channels (id, linkedActorId, privateKeyPem) values (?, ?, ?)"
(channelId, linkedActorId, RSA.encodePrivateKey privateKeyPem)
InsertNewActor actor -> execute conn
"insert into actors (id, inbox, outbox, followers, following, accountName, displayName, bio, keyId, publicKeyPem, locked, type) values (?,?,?,?,?,?,?,?,?,?,?,?) on conflict do nothing"
actor
InsertNewDocument _ -> pure () InsertNewActivity activity -> execute conn
"insert into activities (id, actor, object, audienceTo, audienceCc, time, type) values (?, ?, ?, ?, ?, ?, ?) on conflict do nothing"
activity
Transaction actions -> withTransaction conn (unlift actions)
InsertFollowRequest (follower, followee) -> execute conn
"insert into follows (follower, followee) values (?, ?)"
(follower, followee)
AcceptFollowRequest (follower, followee) -> do record <- just <$> query conn
"select acceptTime, rejectTime, follower, followee from follows where follower = ? and followee = ?"
(follower, followee)
now <- getCurrentTime
case record of
Just (rec :: FollowRecord) -> execute conn
"update follows set acceptTime = ? where follower = ? and followee = ?"
(now, rec.follower, rec.followee)
Nothing -> execute conn
"insert into follows (follower, followee, acceptTime) values (?, ?, ?)"
(follower, followee, now)
CancelFollowRequest inputs -> execute conn
"delete from follows where follower = ? and followee = ?"
inputs
where
just [a] = Just a
just _ = Nothing
instance FromRow Actor where
fromRow = do !id <- field
!inbox <- field
!outbox <- field
!followers <- field
!following <- field
!accountName <- field
!displayName <- field
!summary <- field
!publicKeyId <- field
!publicKeyPem <- field <&> fromJust . RSA.decodePublicKey
!locked <- field
!subtype <- field
pure (Actor {
publicKey = PublicKey {
id = publicKeyId,
owner = id,
publicKeyPem
},
..
})
instance ToRow Actor where
toRow a = [
toField a.id,
toField a.inbox,
toField a.outbox,
toField a.followers,
toField a.following,
toField a.accountName,
toField a.displayName,
toField a.summary,
toField a.publicKey.id,
toField (RSA.encodePublicKey a.publicKey.publicKeyPem),
toField a.locked,
toField a.subtype
]
instance FromRow Channel where
fromRow = do !channelId <- field
!linkedActorId <- field
!privateKeyPem <- field <&> fromJust . RSA.decodePrivateKey
let settings = ChannelSettings { autoAcceptFollows = True }
pure (Channel { .. })
instance FromRow User where
fromRow = User <$> field
<*> field
<*> field
instance FromRow (Activity Id) where
fromRow = do !id <- field
!actor <- field
!object <- field
!to <- field <&> fromJust . JSON.decode
!cc <- field <&> fromJust . JSON.decode
!time <- field
!subtype <- field
pure (Activity { .. })
instance ToRow (Activity Id) where
toRow a = [
toField a.id,
toField a.actor,
toField a.object,
toField (JSON.encode a.to),
toField (JSON.encode a.cc),
toField a.time,
toField a.subtype
]
data FollowRecord
= FollowRecord {
acceptTime :: Maybe UTCTime,
rejectTime :: Maybe UTCTime,
follower :: Id,
followee :: Id
}
instance FromRow FollowRecord where
fromRow = do !acceptTime <- field
!rejectTime <- field
!follower <- field
!followee <- field
pure (FollowRecord { .. })
instance ToRow FollowRecord where
toRow a = [
toField a.acceptTime,
toField a.rejectTime,
toField a.follower,
toField a.followee
]
instance FromField Id where
fromField = fmap Id . fromField
instance ToField Id where
toField = toField . (.url)
instance FromField Inbox where
fromField = fmap Inbox . fromField
instance ToField Inbox where
toField = toField . (.url)
instance FromField UUID.UUID where
fromField = fmap (fromJust . UUID.fromByteString)
. fromField
instance ToField UUID.UUID where
toField = toField . UUID.toByteString
instance forall o. FromField (Subtype (Activity o)) where
fromField = fromField >=> \case
"Follow" -> pure Follow
"Accept" -> pure Accept
"Reject" -> pure Reject
"Create" -> pure Create
"Delete" -> pure Delete
"Announce"-> pure Announce
"Like" -> pure Like
"Undo" -> pure Undo
(str :: Text) -> fail ("Bad subtype of Activity: " <> show str)
instance forall o. ToField (Subtype (Activity o)) where
toField Follow = toField ("Follow" :: Text)
toField Accept = toField ("Accept" :: Text)
toField Reject = toField ("Reject" :: Text)
toField Create = toField ("Create" :: Text)
toField Delete = toField ("Delete" :: Text)
toField Announce = toField ("Announce" :: Text)
toField Like = toField ("Like" :: Text)
toField Undo = toField ("Undo" :: Text)
instance FromField (Subtype Actor) where
fromField = fromField >=> \case
"Person" -> pure Person
"Service" -> pure Service
"Application" -> pure Application
"Organization" -> pure Organization
"Group" -> pure Group
(str :: Text) -> fail ("Bad subtype of Actor: " <> show str)
instance ToField (Subtype Actor) where
toField Person = toField ("Person" :: Text)
toField Service = toField ("Service" :: Text)
toField Application = toField ("Application" :: Text)
toField Organization = toField ("Organization" :: Text)
toField Group = toField ("Group" :: Text)