Compare commits

..

2 commits

Author SHA1 Message Date
me
4db2fe8fae introduce outbox page 2024-10-26 11:22:18 +03:00
me
622c5319dd use duplicate record fields 2024-10-26 10:41:36 +03:00
6 changed files with 135 additions and 98 deletions

View file

@ -41,8 +41,8 @@ mkFediApp :: IO Twain.Application
mkFediApp = do mkFediApp = do
detailsFile <- lookupEnv "FEDI_DETAILS" detailsFile <- lookupEnv "FEDI_DETAILS"
<&> maybe (error "missing FEDI_DETAILS") id <&> maybe (error "missing FEDI_DETAILS") id
details <- A.decodeFileStrict detailsFile details <- A.eitherDecodeFileStrict detailsFile
<&> maybe (error $ "could not read file '" <> detailsFile <> "'.") id <&> either (\err -> error $ "could not read file '" <> detailsFile <> "'.\n" <> err) id
pure $ foldr ($) pure $ foldr ($)
(Twain.notFound $ Twain.send $ Twain.text "Error: not found.") (Twain.notFound $ Twain.send $ Twain.text "Error: not found.")

View file

@ -52,6 +52,7 @@ library
GeneralizedNewtypeDeriving GeneralizedNewtypeDeriving
DeriveAnyClass DeriveAnyClass
DerivingStrategies DerivingStrategies
DuplicateRecordFields
executable fedi executable fedi
import: warnings import: warnings

View file

@ -1,5 +1,6 @@
module Fedi.Activity where module Fedi.Activity where
import Data.Maybe (listToMaybe)
import Data.Aeson qualified as A import Data.Aeson qualified as A
import Data.Text qualified as T import Data.Text qualified as T
import Fedi.Types import Fedi.Types
@ -8,7 +9,7 @@ import Data.Time (UTCTime)
data Activity data Activity
= Create = Create
{ createId :: ActivityUrl { id :: ActivityUrl
, actor :: ActorId , actor :: ActorId
, object :: Object , object :: Object
} }
@ -20,13 +21,13 @@ data Object
data Note data Note
= Note = Note
{ noteId :: NoteId { id :: NoteId
, notePublished :: UTCTime , published :: UTCTime
, noteActor :: ActorId , actor :: ActorId
, noteContent :: T.Text , content :: T.Text
, noteName :: Maybe String , name :: Maybe String
, noteUrl :: Maybe Url , url :: Maybe Url
, noteReplies :: Collection Unordered Note , replies :: Collection Unordered Note
} }
type NoteId = Url type NoteId = Url
@ -35,29 +36,37 @@ type Following = [Actor]
type Inbox = Collection Ordered Activity type Inbox = Collection Ordered Activity
type Outbox = Collection Unordered Activity type Outbox = Collection Unordered Activity
type OutboxPage = OrderedCollectionPage Activity
data Ordered data Ordered
data Unordered data Unordered
data OrderedCollectionPage a
= OrderedCollectionPage
{ id :: Url
, partOf :: Url
, orderedItems :: [a]
}
data Collection order a data Collection order a
= Collection = Collection
{ collectionSummary :: String { summary :: String
, collectionItems :: [a] , items :: [a]
} }
instance A.ToJSON Note where instance A.ToJSON Note where
toJSON note = toJSON note =
A.object $ A.object $
[ "type" A..= ("Note" :: String) [ "type" A..= ("Note" :: String)
, "id" A..= note.noteId , "id" A..= note.id
, "published" A..= note.notePublished , "published" A..= note.published
, "attributedTo" A..= note.noteActor , "attributedTo" A..= note.actor
, "content" A..= note.noteContent , "content" A..= note.content
, "name" A..= note.noteName , "name" A..= note.name
, "replies" A..= note.noteReplies , "replies" A..= note.replies
] ]
<> [ "name" A..= name | Just name <- [note.noteName] ] <> [ "name" A..= name | Just name <- [note.name] ]
<> [ "url" A..= url | Just url <- [note.noteUrl] ] <> [ "url" A..= url | Just url <- [note.url] ]
instance A.ToJSON Object where instance A.ToJSON Object where
toJSON = \case toJSON = \case
@ -71,7 +80,7 @@ instance A.ToJSON Activity where
[ "https://www.w3.org/ns/activitystreams" :: String [ "https://www.w3.org/ns/activitystreams" :: String
] ]
, "type" A..= ("Create" :: String) , "type" A..= ("Create" :: String)
, "id" A..= create.createId , "id" A..= create.id
, "actor" A..= create.actor , "actor" A..= create.actor
, "object" A..= create.object , "object" A..= create.object
] ]
@ -83,9 +92,11 @@ instance A.ToJSON a => A.ToJSON (Collection Ordered a) where
[ "https://www.w3.org/ns/activitystreams" :: String [ "https://www.w3.org/ns/activitystreams" :: String
] ]
, "type" A..= ("OrderedCollection" :: String) , "type" A..= ("OrderedCollection" :: String)
, "summary" A..= collection.collectionSummary , "summary" A..= collection.summary
, "totalItems" A..= length collection.collectionItems , "totalItems" A..= length collection.items
, "orderedItems" A..= collection.collectionItems , "orderedItems" A..= collection.items
, "first" A..= listToMaybe (take 1 collection.items)
, "last" A..= listToMaybe (take 1 $ reverse collection.items)
] ]
instance A.ToJSON a => A.ToJSON (Collection Unordered a) where instance A.ToJSON a => A.ToJSON (Collection Unordered a) where
@ -95,24 +106,20 @@ instance A.ToJSON a => A.ToJSON (Collection Unordered a) where
[ "https://www.w3.org/ns/activitystreams" :: String [ "https://www.w3.org/ns/activitystreams" :: String
] ]
, "type" A..= ("Collection" :: String) , "type" A..= ("Collection" :: String)
, "summary" A..= collection.collectionSummary , "summary" A..= collection.summary
, "totalItems" A..= length collection.collectionItems , "totalItems" A..= length collection.items
, "items" A..= collection.collectionItems , "items" A..= collection.items
, "first" A..= listToMaybe (take 1 collection.items)
, "last" A..= listToMaybe (take 1 $ reverse collection.items)
] ]
{- instance A.ToJSON a => A.ToJSON (OrderedCollectionPage a) where
"@context": "https://www.w3.org/ns/activitystreams", toJSON collection =
"summary": "Sally's notes", A.object
"type": "OrderedCollection", [ "@context" A..=
"totalItems": 2, [ "https://www.w3.org/ns/activitystreams" :: String
"orderedItems": [ ]
{ , "type" A..= ("OrderedCollectionPage" :: String)
"type": "Note", , "partOf" A..= collection.partOf
"name": "A Simple Note" , "orderedItems" A..= collection.orderedItems
}, ]
{
"type": "Note",
"name": "Another Simple Note"
}
]
-}

View file

@ -5,12 +5,12 @@ import Fedi.Types
data Actor data Actor
= Actor = Actor
{ actorId :: Url { id :: Url
, actorName :: String , name :: String
, actorPreferredUsername :: String , preferredUsername :: String
, actorSummary :: String , summary :: String
, actorIcon :: Url , icon :: Url
, actorPublicKey :: PublicKey , publicKey :: PublicKey
} }
deriving Show deriving Show
@ -22,9 +22,9 @@ data ActorType
data PublicKey data PublicKey
= PublicKey = PublicKey
{ pkId :: Url { id :: Url
, pkOwner :: Url , owner :: Url
, pkPublicKeyPem :: Pem , publicKeyPem :: Pem
} }
deriving Show deriving Show
@ -34,16 +34,16 @@ makeActor details =
url = "https://" <> details.domain url = "https://" <> details.domain
actorUrl = url <> "/" <> details.username actorUrl = url <> "/" <> details.username
in Actor in Actor
{ actorId = actorUrl { id = actorUrl
, actorName = details.name , name = details.name
, actorPreferredUsername = details.username , preferredUsername = details.username
, actorSummary = details.summary , summary = details.summary
, actorIcon = details.icon , icon = details.icon
, actorPublicKey = , publicKey =
PublicKey PublicKey
{ pkId = actorUrl <> "#main-key" { id = actorUrl <> "#main-key"
, pkOwner = actorUrl , owner = actorUrl
, pkPublicKeyPem = details.publicPem , publicKeyPem = details.publicPem
} }
} }
@ -54,19 +54,19 @@ instance A.ToJSON Actor where
[ "https://www.w3.org/ns/activitystreams" :: String [ "https://www.w3.org/ns/activitystreams" :: String
, "https://w3id.org/security/v1" , "https://w3id.org/security/v1"
] ]
, "id" A..= actor.actorId , "id" A..= actor.id
, "type" A..= Person , "type" A..= Person
, "name" A..= actor.actorName , "name" A..= actor.name
, "preferredUsername" A..= actor.actorPreferredUsername , "preferredUsername" A..= actor.preferredUsername
, "summary" A..= actor.actorSummary , "summary" A..= actor.summary
, "icon" A..= A.object , "icon" A..= A.object
[ "type" A..= ("Image" :: String) [ "type" A..= ("Image" :: String)
, "mediaType" A..= ("image/png" :: String) , "mediaType" A..= ("image/png" :: String)
, "url" A..= actor.actorIcon , "url" A..= actor.icon
] ]
, "inbox" A..= (actor.actorId <> "/inbox") , "inbox" A..= (actor.id <> "/inbox")
, "outbox" A..= (actor.actorId <> "/outbox") , "outbox" A..= (actor.id <> "/outbox")
, "publicKey" A..= actor.actorPublicKey , "publicKey" A..= actor.publicKey
] ]
instance A.ToJSON ActorType where instance A.ToJSON ActorType where
@ -75,7 +75,7 @@ instance A.ToJSON ActorType where
instance A.ToJSON PublicKey where instance A.ToJSON PublicKey where
toJSON pk = toJSON pk =
A.object A.object
[ "id" A..= pk.pkId [ "id" A..= pk.id
, "owner" A..= pk.pkOwner , "owner" A..= pk.owner
, "publicKeyPem" A..= pk.pkPublicKeyPem , "publicKeyPem" A..= pk.publicKeyPem
] ]

View file

@ -18,7 +18,7 @@ routes details =
handleUser details handleUser details
, Twain.get (matchOutbox details) do , Twain.get (matchOutbox details) do
handleOutbox details handleOutbox details []
, Twain.get matchWebfinger do , Twain.get matchWebfinger do
handleWebfinger details handleWebfinger details
@ -49,7 +49,7 @@ handleWebfinger :: UserDetails -> Twain.ResponderM b
handleWebfinger details = do handleWebfinger details = do
resource <- Twain.param "resource" resource <- Twain.param "resource"
let webfinger = makeWebfinger details let webfinger = makeWebfinger details
if resource == ppSubject webfinger.wfSubject if resource == ppSubject webfinger.subject
then do then do
Twain.send $ jsonLD (A.encode webfinger) Twain.send $ jsonLD (A.encode webfinger)
else do else do
@ -61,7 +61,36 @@ matchOutbox :: UserDetails -> Twain.PathPattern
matchOutbox details = matchOutbox details =
fromString ("/" <> details.username <> "/outbox") fromString ("/" <> details.username <> "/outbox")
handleOutbox :: UserDetails -> Twain.ResponderM b handleOutbox :: UserDetails -> [Activity] -> Twain.ResponderM b
handleOutbox details = do handleOutbox details items = do
let content = Collection { collectionSummary = details.username <> "'s notes", collectionItems = [] } :: Outbox isPage <- Twain.queryParamMaybe "page"
Twain.send $ jsonLD (A.encode content) let
outboxUrl =
"https://"
<> details.domain
<> "/"
<> details.username
<> "/outbox"
let
response =
case isPage of
Just True ->
let
content :: OutboxPage
content =
OrderedCollectionPage
{ id = outboxUrl <> "?page=true"
, partOf = outboxUrl
, orderedItems = items
}
in A.encode content
_ ->
let
content :: Outbox
content =
Collection
{ summary = details.username <> "'s notes"
, items = items
}
in A.encode content
Twain.send $ jsonLD response

View file

@ -6,27 +6,27 @@ import Fedi.Types
data Webfinger data Webfinger
= Webfinger = Webfinger
{ wfSubject :: Subject { subject :: Subject
, wfLinks :: [Link] , links :: [Link]
} }
deriving Show deriving Show
data Subject data Subject
= Subject = Subject
{ subjectUsername :: Username { username :: Username
, subjectDomain :: Domain , domain :: Domain
} }
deriving Show deriving Show
ppSubject :: Subject -> String ppSubject :: Subject -> String
ppSubject subject = ppSubject subject =
"acct:" <> subject.subjectUsername <> "@" <> subject.subjectDomain "acct:" <> subject.username <> "@" <> subject.domain
data Link data Link
= Link = Link
{ linkRel :: Rel { rel :: Rel
, linkType :: LinkType , type_ :: LinkType
, linkHref :: Url , href :: Url
} }
deriving Show deriving Show
@ -36,15 +36,15 @@ makeWebfinger details =
url = "https://" <> details.domain url = "https://" <> details.domain
in in
Webfinger Webfinger
{ wfSubject = Subject { subject = Subject
{ subjectUsername = details.username { username = details.username
, subjectDomain = details.domain , domain = details.domain
} }
, wfLinks = , links =
[ Link [ Link
{ linkRel = Self { rel = Self
, linkType = ActivityJson , type_ = ActivityJson
, linkHref = url <> "/" <> details.username , href = url <> "/" <> details.username
} }
] ]
} }
@ -55,8 +55,8 @@ makeWebfinger details =
instance A.ToJSON Webfinger where instance A.ToJSON Webfinger where
toJSON webfinger = toJSON webfinger =
A.object A.object
[ "subject" A..= webfinger.wfSubject [ "subject" A..= webfinger.subject
, "links" A..= webfinger.wfLinks , "links" A..= webfinger.links
] ]
instance A.ToJSON Subject where instance A.ToJSON Subject where
@ -66,7 +66,7 @@ instance A.ToJSON Subject where
instance A.ToJSON Link where instance A.ToJSON Link where
toJSON link = toJSON link =
A.object A.object
[ "rel" A..= link.linkRel [ "rel" A..= link.rel
, "type" A..= link.linkType , "type" A..= link.type_
, "href" A..= link.linkHref , "href" A..= link.href
] ]