Compare commits
	
		
			2 commits
		
	
	
		
			776e3d7e4e
			...
			4db2fe8fae
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4db2fe8fae | |||
| 622c5319dd | 
					 6 changed files with 135 additions and 98 deletions
				
			
		|  | @ -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.") | ||||||
|  |  | ||||||
|  | @ -52,6 +52,7 @@ library | ||||||
|     GeneralizedNewtypeDeriving |     GeneralizedNewtypeDeriving | ||||||
|     DeriveAnyClass |     DeriveAnyClass | ||||||
|     DerivingStrategies |     DerivingStrategies | ||||||
|  |     DuplicateRecordFields | ||||||
| 
 | 
 | ||||||
| executable fedi | executable fedi | ||||||
|   import: warnings |   import: warnings | ||||||
|  |  | ||||||
|  | @ -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" |  | ||||||
|     } |  | ||||||
|   ] |  | ||||||
| -} |  | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|       ] |       ] | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|       ] |       ] | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue