{- This file is part of Vervis.
 -
 - Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

module Vervis.Handler.Inbox
    ( getInboxR
    , getSharerInboxR
    , getProjectInboxR
    , postSharerInboxR
    , postProjectInboxR
    , getPublishR
    , getSharerOutboxR
    , getSharerOutboxItemR
    , postSharerOutboxR
    , getProjectOutboxR
    , getProjectOutboxItemR
    , getActorKey1R
    , getActorKey2R
    , getNotificationsR
    , postNotificationsR
    )
where

import Control.Applicative ((<|>))
import Control.Concurrent.STM.TVar (readTVarIO, modifyTVar')
import Control.Exception hiding (Handler)
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger.CallStack
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Crypto.Error (CryptoFailable (..))
import Crypto.PubKey.Ed25519 (publicKey, signature, verify)
import Data.Aeson
import Data.Aeson.Encode.Pretty
import Data.Bifunctor
import Data.Bitraversable
import Data.Foldable (for_)
import Data.HashMap.Strict (HashMap)
import Data.List
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe
import Data.PEM (PEM (..))
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8')
import Data.Text.Lazy.Encoding (decodeUtf8)
import Data.Time.Clock
import Data.Time.Interval (TimeInterval, toTimeUnit)
import Data.Time.Units (Second)
import Data.Traversable
import Database.Persist
import Network.HTTP.Client (Manager, HttpException, requestFromURI)
import Network.HTTP.Simple (httpJSONEither, getResponseBody, setRequestManager, addRequestHeader)
import Network.HTTP.Types.Header (hDate, hHost)
import Network.HTTP.Types.Status
import Text.Blaze.Html (Html, preEscapedToHtml)
import Text.Blaze.Html.Renderer.Text
import Text.HTML.SanitizeXSS
import Text.Shakespeare.I18N (RenderMessage)
import UnliftIO.Exception (try)
import Yesod.Auth (requireAuth)
import Yesod.Core
import Yesod.Core.Json (requireJsonBody)
import Yesod.Core.Handler
import Yesod.Form.Fields
import Yesod.Form.Functions
import Yesod.Form.Types
import Yesod.Persist.Core

import qualified Data.ByteString.Char8 as BC (unpack)
import qualified Data.CaseInsensitive as CI (mk)
import qualified Data.HashMap.Strict as M (lookup, insert, adjust, fromList)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL (toStrict)
import qualified Data.Text.Lazy.Builder as TLB
import qualified Data.Vector as V
import qualified Database.Esqueleto as E
import qualified Network.Wai as W (requestMethod, rawPathInfo, requestHeaders)

import Network.HTTP.Signature hiding (Algorithm (..))
import Yesod.HttpSignature (verifyRequestSignature)

import qualified Network.HTTP.Signature as S (Algorithm (..))

import Database.Persist.JSON
import Network.FedURI
import Web.ActivityPub
import Yesod.ActivityPub
import Yesod.Auth.Unverified
import Yesod.FedURI
import Yesod.Hashids
import Yesod.RenderSource

import qualified Data.Aeson.Encode.Pretty.ToEncoding as AEP

import Data.Aeson.Local
import Data.Either.Local
import Data.EventTime.Local
import Data.Paginate.Local
import Data.Time.Clock.Local
import Database.Persist.Local
import Yesod.Persist.Local

import Vervis.ActivityPub
import Vervis.ActorKey
import Vervis.API
import Vervis.Federation
import Vervis.Federation.Auth
import Vervis.Foundation
import Vervis.Model hiding (Ticket)
import Vervis.Model.Ident
import Vervis.Paginate
import Vervis.RemoteActorStore
import Yesod.RenderSource
import Vervis.Settings

getInboxR :: Handler Html
getInboxR = do
    acts <-
        liftIO . readTVarIO . snd =<< maybe notFound return =<< getsYesod appActivities
    defaultLayout
        [whamlet|
            <p>
              Welcome to the ActivityPub inbox test page! Activities received
              by this Vervis instance are listed here for testing and
              debugging. To test, go to another Vervis instance and publish
              something that supports federation (currently, only ticket
              comments), either through the regular UI or via the /publish
              page, and then come back here to see the result. Activities that
              aren't understood or their processing fails get listed here too,
              with a report of what exactly happened.
            <p>Last 10 activities posted:
            <ul>
              $forall ActivityReport time msg ctypes body <- acts
                <li>
                  <div>#{show time}
                  <div>#{msg}
                  <div><code>#{intercalate " | " $ map BC.unpack ctypes}
                  <div><pre>#{decodeUtf8 body}
        |]

getInbox :: Route App -> AppDB InboxId -> Handler TypedContent
getInbox here getInboxId = do
    (total, pages, mpage) <- runDB $ do
        ibid <- getInboxId
        getPageAndNavCount
            (countItems ibid)
            (\ off lim -> map adaptItem <$> getItems ibid off lim)
    encodeRouteLocal <- getEncodeRouteLocal
    encodeRoutePageLocal <- getEncodeRoutePageLocal
    let pageUrl = encodeRoutePageLocal here
    host <- getsYesod $ appInstanceHost . appSettings
    selectRep $
        case mpage of
            Nothing -> do
                provideAP $ pure $ Doc host $ Collection
                    { collectionId         = encodeRouteLocal here
                    , collectionType       = CollectionTypeOrdered
                    , collectionTotalItems = Just total
                    , collectionCurrent    = Nothing
                    , collectionFirst      = Just $ pageUrl 1
                    , collectionLast       = Just $ pageUrl pages
                    , collectionItems      = [] :: [Text]
                    }
                provideRep (redirectFirstPage here :: Handler Html)
            Just (items, navModel) -> do
                let current = nmCurrent navModel
                provideAP $ pure $ Doc host $ CollectionPage
                    { collectionPageId         = pageUrl current
                    , collectionPageType       = CollectionPageTypeOrdered
                    , collectionPageTotalItems = Nothing
                    , collectionPageCurrent    = Just $ pageUrl current
                    , collectionPageFirst      = Just $ pageUrl 1
                    , collectionPageLast       = Just $ pageUrl pages
                    , collectionPagePartOf     = encodeRouteLocal here
                    , collectionPagePrev       =
                        if current > 1
                            then Just $ pageUrl $ current - 1
                            else Nothing
                    , collectionPageNext       =
                        if current < pages
                            then Just $ pageUrl $ current + 1
                            else Nothing
                    , collectionPageStartIndex = Nothing
                    , collectionPageItems      = map fromEither items
                    }
                provideRep $
                    let pageNav = navWidget navModel
                    in  defaultLayout $(widgetFile "person/inbox")
    where
    countItems ibid =
        (+) <$> count [InboxItemLocalInbox ==. ibid]
            <*> count [InboxItemRemoteInbox ==. ibid]
    getItems ibid off lim =
        E.select $ E.from $
            \ (ib `E.LeftOuterJoin` (ibl `E.InnerJoin` ob) `E.LeftOuterJoin` (ibr `E.InnerJoin` ract)) -> do
                E.on $ ibr E.?. InboxItemRemoteActivity E.==. ract E.?. RemoteActivityId
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibr E.?. InboxItemRemoteItem
                E.on $ ibl E.?. InboxItemLocalActivity E.==. ob E.?. OutboxItemId
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibl E.?. InboxItemLocalItem
                E.where_
                    $ ( E.isNothing (ibr E.?. InboxItemRemoteInbox) E.||.
                        ibr E.?. InboxItemRemoteInbox E.==. E.just (E.val ibid)
                      )
                    E.&&.
                      ( E.isNothing (ibl E.?. InboxItemLocalInbox) E.||.
                        ibl E.?. InboxItemLocalInbox E.==. E.just (E.val ibid)
                      )
                E.orderBy [E.desc $ ib E.^. InboxItemId]
                E.offset $ fromIntegral off
                E.limit $ fromIntegral lim
                return
                    ( ib E.^. InboxItemId
                    , ob E.?. OutboxItemActivity
                    , ract E.?. RemoteActivityContent
                    )
    adaptItem (E.Value ibid, E.Value mact, E.Value mobj) =
        case (mact, mobj) of
            (Nothing, Nothing) ->
                error $
                    "InboxItem #" ++ show ibid ++ " neither local nor remote"
            (Just _, Just _) ->
                error $ "InboxItem #" ++ show ibid ++ " both local and remote"
            (Just act, Nothing) -> Left $ persistJSONValue act
            (Nothing, Just obj) -> Right $ persistJSONValue obj

getSharerInboxR :: ShrIdent -> Handler TypedContent
getSharerInboxR shr = getInbox here getInboxId
    where
    here = SharerInboxR shr
    getInboxId = do
        sid <- getKeyBy404 $ UniqueSharer shr
        p <- getValBy404 $ UniquePersonIdent sid
        return $ personInbox p

getProjectInboxR :: ShrIdent -> PrjIdent -> Handler TypedContent
getProjectInboxR shr prj = getInbox here getInboxId
    where
    here = ProjectInboxR shr prj
    getInboxId = do
        sid <- getKeyBy404 $ UniqueSharer shr
        j <- getValBy404 $ UniqueProject prj sid
        return $ projectInbox j

postSharerInboxR :: ShrIdent -> Handler ()
postSharerInboxR shrRecip = do
    federation <- getsYesod $ appFederation . appSettings
    unless federation badMethod
    contentTypes <- lookupHeaders "Content-Type"
    now <- liftIO getCurrentTime
    result <- runExceptT $ do
        (auth, body) <- authenticateActivity now
        (actbObject body,) <$> handleSharerInbox now shrRecip auth body
    recordActivity now result contentTypes
    case result of
        Left _ -> sendResponseStatus badRequest400 ()
        Right _ -> return ()

recordActivity now result contentTypes = do
    macts <- getsYesod appActivities
    for_ macts $ \ (size, acts) ->
        liftIO $ atomically $ modifyTVar' acts $ \ vec ->
            let (msg, body) =
                    case result of
                        Left t -> (t, "{?}")
                        Right (o, t) -> (t, encodePretty o)
                item = ActivityReport now msg contentTypes body
                vec' = item `V.cons` vec
            in  if V.length vec' > size
                    then V.init vec'
                    else vec'

postProjectInboxR :: ShrIdent -> PrjIdent -> Handler ()
postProjectInboxR shrRecip prjRecip = do
    federation <- getsYesod $ appFederation . appSettings
    unless federation badMethod
    contentTypes <- lookupHeaders "Content-Type"
    now <- liftIO getCurrentTime
    result <- runExceptT $ do
        (auth, body) <- authenticateActivity now
        (actbObject body,) <$>
            handleProjectInbox now shrRecip prjRecip auth body
    recordActivity now result contentTypes
    case result of
        Left _ -> sendResponseStatus badRequest400 ()
        Right _ -> return ()

{-
jsonField :: (FromJSON a, ToJSON a) => Field Handler a
jsonField = checkMMap fromTextarea toTextarea textareaField
    where
    toTextarea = Textarea . TL.toStrict . encodePrettyToLazyText
    fromTextarea = return . first T.pack . eitherDecodeStrict' . encodeUtf8 . unTextarea
-}

fedUriField
    :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m FedURI
fedUriField = Field
    { fieldParse = parseHelper $ \ t ->
        case parseFedURI t of
            Left e  -> Left $ MsgInvalidUrl $ T.pack e <> ": " <> t
            Right u -> Right u
    , fieldView = \theId name attrs val isReq ->
        [whamlet|<input ##{theId} name=#{name} *{attrs} type=url :isReq:required value=#{either id renderFedURI val}>|]
    , fieldEnctype = UrlEncoded
    }

ticketField
    :: (Route App -> LocalURI) -> Field Handler (Text, ShrIdent, PrjIdent, Int)
ticketField encodeRouteLocal = checkMMap toTicket fromTicket fedUriField
    where
    toTicket uTicket = runExceptT $ do
        let (hTicket, luTicket) = f2l uTicket
        route <-
            case decodeRouteLocal luTicket of
                Nothing -> throwE ("Not a valid route" :: Text)
                Just r -> return r
        case route of
            TicketR shr prj num -> return (hTicket, shr, prj, num)
            _ -> throwE "Not a ticket route"
    fromTicket (h, shr, prj, num) =
        l2f h $ encodeRouteLocal $ TicketR shr prj num

projectField
    :: (Route App -> LocalURI) -> Field Handler (Text, ShrIdent, PrjIdent)
projectField encodeRouteLocal = checkMMap toProject fromProject fedUriField
    where
    toProject u = runExceptT $ do
        let (h, lu) = f2l u
        route <-
            case decodeRouteLocal lu of
                Nothing -> throwE ("Not a valid route" :: Text)
                Just r -> return r
        case route of
            ProjectR shr prj -> return (h, shr, prj)
            _ -> throwE "Not a project route"
    fromProject (h, shr, prj) = l2f h $ encodeRouteLocal $ ProjectR shr prj

publishCommentForm
    :: Form ((Text, ShrIdent, PrjIdent, Int), Maybe FedURI, Text)
publishCommentForm html = do
    enc <- getEncodeRouteLocal
    flip renderDivs html $ (,,)
        <$> areq (ticketField enc) "Ticket"      (Just deft)
        <*> aopt fedUriField       "Replying to" (Just $ Just defp)
        <*> areq textField         "Message"     (Just defmsg)
    where
    deft = ("forge.angeley.es", text2shr "fr33", text2prj "sandbox", 1)
    defp = FedURI "forge.angeley.es" "/s/fr33/m/2f1a7" ""
    defmsg = "Hi! I'm testing federation. Can you see my message? :)"

openTicketForm
    :: Form ((Text, ShrIdent, PrjIdent), TextHtml, TextPandocMarkdown)
openTicketForm html = do
    enc <- getEncodeRouteLocal
    flip renderDivs html $ (,,)
        <$> areq (projectField enc) "Project"     (Just defj)
        <*> ( TextHtml . sanitizeBalance <$>
              areq textField        "Title"       (Just deft)
            )
        <*> ( TextPandocMarkdown . T.filter (/= '\r') . unTextarea <$>
              areq textareaField    "Description" (Just defd)
            )
    where
    defj = ("forge.angeley.es", text2shr "fr33", text2prj "sandbox")
    deft = "Time slows down when tasting coconut ice-cream"
    defd = "Is that slow-motion effect intentional? :)"

activityWidget :: ShrIdent -> Widget -> Enctype -> Widget -> Enctype -> Widget
activityWidget shr widget1 enctype1 widget2 enctype2 =
    [whamlet|
        <h1>Publish a ticket comment
        <form method=POST action=@{SharerOutboxR shr} enctype=#{enctype1}>
          ^{widget1}
          <input type=submit>

        <h1>Open a new ticket
        <form method=POST action=@{SharerOutboxR shr} enctype=#{enctype2}>
          ^{widget2}
          <input type=submit>
    |]

getUserShrIdent :: Handler ShrIdent
getUserShrIdent = do
    Entity _ p <- requireVerifiedAuth
    s <- runDB $ get404 $ personIdent p
    return $ sharerIdent s

getPublishR :: Handler Html
getPublishR = do
    shr <- getUserShrIdent
    ((_result1, widget1), enctype1) <-
        runFormPost $ identifyForm "f1" publishCommentForm
    ((_result2, widget2), enctype2) <-
        runFormPost $ identifyForm "f2" openTicketForm
    defaultLayout $ activityWidget shr widget1 enctype1 widget2 enctype2

getOutbox :: Route App -> AppDB OutboxId -> Handler TypedContent
getOutbox here getObid = do
    (total, pages, mpage) <- runDB $ do
        obid <- getObid
        let countAllItems = count [OutboxItemOutbox ==. obid]
            selectItems off lim = selectList [OutboxItemOutbox ==. obid] [Desc OutboxItemId, OffsetBy off, LimitTo lim]
        getPageAndNavCount countAllItems selectItems
    encodeRouteLocal <- getEncodeRouteLocal
    encodeRoutePageLocal <- getEncodeRoutePageLocal
    let pageUrl = encodeRoutePageLocal here
    host <- getsYesod $ appInstanceHost . appSettings
    selectRep $
        case mpage of
            Nothing -> do
                provideAP $ pure $ Doc host $ Collection
                    { collectionId         = encodeRouteLocal here
                    , collectionType       = CollectionTypeOrdered
                    , collectionTotalItems = Just total
                    , collectionCurrent    = Nothing
                    , collectionFirst      = Just $ pageUrl 1
                    , collectionLast       = Just $ pageUrl pages
                    , collectionItems      = [] :: [Text]
                    }
                provideRep (redirectFirstPage here :: Handler Html)
            Just (items, navModel) -> do
                let current = nmCurrent navModel
                provideAP $ pure $ Doc host $ CollectionPage
                    { collectionPageId         = pageUrl current
                    , collectionPageType       = CollectionPageTypeOrdered
                    , collectionPageTotalItems = Nothing
                    , collectionPageCurrent    = Just $ pageUrl current
                    , collectionPageFirst      = Just $ pageUrl 1
                    , collectionPageLast       = Just $ pageUrl pages
                    , collectionPagePartOf     = encodeRouteLocal here
                    , collectionPagePrev       =
                        if current > 1
                            then Just $ pageUrl $ current - 1
                            else Nothing
                    , collectionPageNext       =
                        if current < pages
                            then Just $ pageUrl $ current + 1
                            else Nothing
                    , collectionPageStartIndex = Nothing
                    , collectionPageItems      = map (persistJSONValue . outboxItemActivity . entityVal) items
                    }
                provideRep $ do
                    let pageNav = navWidget navModel
                    now <- liftIO getCurrentTime
                    let showTime =
                            showEventTime .
                            intervalToEventTime .
                            FriendlyConvert .
                            diffUTCTime now
                    defaultLayout $(widgetFile "person/outbox")

getOutboxItem
    :: Route App
    -> AppDB OutboxId
    -> KeyHashid OutboxItem
    -> Handler TypedContent
getOutboxItem here getObid obikhid = do
    obiid <- decodeKeyHashid404 obikhid
    Doc h act <- runDB $ do
        obid <- getObid
        obi <- get404 obiid
        unless (outboxItemOutbox obi == obid) notFound
        return $ persistJSONValue $ outboxItemActivity obi
    provideHtmlAndAP' h act $ redirect (here, [("prettyjson", "true")])

getSharerOutboxR :: ShrIdent -> Handler TypedContent
getSharerOutboxR shr = getOutbox here getObid
    where
    here = SharerOutboxR shr
    getObid = do
        sid <- getKeyBy404 $ UniqueSharer shr
        p <- getValBy404 $ UniquePersonIdent sid
        return $ personOutbox p

getSharerOutboxItemR :: ShrIdent -> KeyHashid OutboxItem -> Handler TypedContent
getSharerOutboxItemR shr obikhid = getOutboxItem here getObid obikhid
    where
    here = SharerOutboxItemR shr obikhid
    getObid = do
        sid <- getKeyBy404 $ UniqueSharer shr
        p <- getValBy404 $ UniquePersonIdent sid
        return $ personOutbox p

postSharerOutboxR :: ShrIdent -> Handler Html
postSharerOutboxR shrAuthor = do
    federation <- getsYesod $ appFederation . appSettings
    unless federation badMethod

    ((result1, widget1), enctype1) <-
        runFormPost $ identifyForm "f1" publishCommentForm
    ((result2, widget2), enctype2) <-
        runFormPost $ identifyForm "f2" openTicketForm
    let result = Left <$> result1 <|> Right <$> result2

    eid <- runExceptT $ do
        input <-
            case result of
                FormMissing -> throwE "Field(s) missing"
                FormFailure _l -> throwE "Invalid input, see below"
                FormSuccess r -> return r
        bitraverse publishComment openTicket input
    case eid of
        Left err -> setMessage $ toHtml err
        Right id_ ->
            case id_ of
                Left lmid -> do
                    lmkhid <- encodeKeyHashid lmid
                    renderUrl <- getUrlRender
                    let u = renderUrl $ MessageR shrAuthor lmkhid
                    setMessage $ toHtml $ "Message created! ID: " <> u
                Right _obiid ->
                    setMessage "Ticket offer published!"
    defaultLayout $ activityWidget shrAuthor widget1 enctype1 widget2 enctype2
    where
    publishComment ((hTicket, shrTicket, prj, num), muParent, msg) = do
        encodeRouteFed <- getEncodeRouteHome
        encodeRouteLocal <- getEncodeRouteLocal
        let msg' = T.filter (/= '\r') msg
        contentHtml <- ExceptT . pure $ renderPandocMarkdown msg'
        let encodeRecipRoute = l2f hTicket . encodeRouteLocal
            uTicket = encodeRecipRoute $ TicketR shrTicket prj num
            (hLocal, luAuthor) = f2l $ encodeRouteFed $ SharerR shrAuthor
            collections =
                [ ProjectFollowersR shrTicket prj
                , TicketParticipantsR shrTicket prj num
                , TicketTeamR shrTicket prj num
                ]
            recips = ProjectR shrTicket prj : collections
            note = Note
                { noteId        = Nothing
                , noteAttrib    = luAuthor
                , noteAudience  = Audience
                    { audienceTo        = map encodeRecipRoute recips
                    , audienceBto       = []
                    , audienceCc        = []
                    , audienceBcc       = []
                    , audienceGeneral   = []
                    , audienceNonActors = map encodeRecipRoute collections
                    }
                , noteReplyTo   = Just $ fromMaybe uTicket muParent
                , noteContext   = Just uTicket
                , notePublished = Nothing
                , noteSource    = msg'
                , noteContent   = contentHtml
                }
        ExceptT $ createNoteC hLocal note
    openTicket ((h, shr, prj), TextHtml title, TextPandocMarkdown desc) = do
        encodeRouteLocal <- getEncodeRouteLocal
        encodeRouteFed <- getEncodeRouteFed
        local <- hostIsLocal h
        descHtml <- ExceptT . pure $ renderPandocMarkdown desc
        summary <-
            TextHtml . TL.toStrict . renderHtml <$>
                withUrlRenderer
                    [hamlet|
                        <p>
                          <a href=@{SharerR shrAuthor}>
                            #{shr2text shrAuthor}
                          \ offered a ticket to project #
                          $if local
                            <a href=@{ProjectR shr prj}>
                              ./s/#{shr2text shr}/p/#{prj2text prj}
                          $else
                            <a href=#{renderFedURI $ encodeRouteFed h $ ProjectR shr prj}>
                              #{h}/s/#{shr2text shr}/p/#{prj2text prj}
                          : #{preEscapedToHtml title}.
                    |]
        let recipsA = [ProjectR shr prj]
            recipsC = [ProjectTeamR shr prj, ProjectFollowersR shr prj]
            ticket = Ticket
                { ticketLocal        = Nothing
                , ticketAttributedTo = encodeRouteLocal $ SharerR shrAuthor
                , ticketPublished    = Nothing
                , ticketUpdated      = Nothing
                , ticketName         = Nothing
                , ticketSummary      = TextHtml title
                , ticketContent      = TextHtml descHtml
                , ticketSource       = TextPandocMarkdown desc
                , ticketAssignedTo   = Nothing
                , ticketIsResolved   = False
                , ticketDependsOn    = []
                , ticketDependedBy   = []
                }
            offer = Offer
                { offerObject = ticket
                , offerTarget = encodeRouteFed h $ ProjectR shr prj
                }
            audience = Audience
                { audienceTo        =
                    map (encodeRouteFed h) $ recipsA ++ recipsC
                , audienceBto       = []
                , audienceCc        = []
                , audienceBcc       = []
                , audienceGeneral   = []
                , audienceNonActors = map (encodeRouteFed h) recipsC
                }
        ExceptT $ offerTicketC shrAuthor summary audience offer

getProjectOutboxR :: ShrIdent -> PrjIdent -> Handler TypedContent
getProjectOutboxR shr prj = getOutbox here getObid
    where
    here = ProjectOutboxR shr prj
    getObid = do
        sid <- getKeyBy404 $ UniqueSharer shr
        j <- getValBy404 $ UniqueProject prj sid
        return $ projectOutbox j

getProjectOutboxItemR
    :: ShrIdent -> PrjIdent -> KeyHashid OutboxItem -> Handler TypedContent
getProjectOutboxItemR shr prj obikhid = getOutboxItem here getObid obikhid
    where
    here = ProjectOutboxItemR shr prj obikhid
    getObid = do
        sid <- getKeyBy404 $ UniqueSharer shr
        j <- getValBy404 $ UniqueProject prj sid
        return $ projectOutbox j

getActorKey :: ((ActorKey, ActorKey, Bool) -> ActorKey) -> Route App -> Handler TypedContent
getActorKey choose route = do
    actorKey <-
        liftIO . fmap (actorKeyPublicBin . choose) . readTVarIO =<<
        getsYesod appActorKeys
    encodeRouteLocal <- getEncodeRouteLocal
    let key = PublicKey
            { publicKeyId       = encodeRouteLocal route
            , publicKeyExpires  = Nothing
            , publicKeyOwner    = OwnerInstance
            , publicKeyMaterial = actorKey
            }
    provideHtmlAndAP key $ redirect (route, [("prettyjson", "true")])

getActorKey1R :: Handler TypedContent
getActorKey1R = getActorKey (\ (k1, _, _) -> k1) ActorKey1R

getActorKey2R :: Handler TypedContent
getActorKey2R = getActorKey (\ (_, k2, _) -> k2) ActorKey2R

notificationForm :: Maybe (Maybe (InboxItemId, Bool)) -> Form (Maybe (InboxItemId, Bool))
notificationForm defs = renderDivs $ mk
    <$> aopt hiddenField (name "Inbox Item ID#") (fmap fst <$> defs)
    <*> aopt hiddenField (name "New unread flag") (fmap snd <$> defs)
    where
    name t = FieldSettings "" Nothing Nothing (Just t) []
    mk Nothing     Nothing       = Nothing
    mk (Just ibid) (Just unread) = Just (ibid, unread)
    mk _           _             = error "Missing hidden field?"

getNotificationsR :: ShrIdent -> Handler Html
getNotificationsR shr = do
    items <- runDB $ do
        sid <- getKeyBy404 $ UniqueSharer shr
        p <- getValBy404 $ UniquePersonIdent sid
        let ibid = personInbox p
        map adaptItem <$> getItems ibid
    notifications <- for items $ \ (ibid, activity) -> do
        ((_result, widget), enctype) <-
            runFormPost $ notificationForm $ Just $ Just (ibid, False)
        return (activity, widget, enctype)
    ((_result, widgetAll), enctypeAll) <-
        runFormPost $ notificationForm $ Just Nothing
    defaultLayout $(widgetFile "person/notifications")
    where
    getItems ibid =
        E.select $ E.from $
            \ (ib `E.LeftOuterJoin` (ibl `E.InnerJoin` ob) `E.LeftOuterJoin` (ibr `E.InnerJoin` ract)) -> do
                E.on $ ibr E.?. InboxItemRemoteActivity E.==. ract E.?. RemoteActivityId
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibr E.?. InboxItemRemoteItem
                E.on $ ibl E.?. InboxItemLocalActivity E.==. ob E.?. OutboxItemId
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibl E.?. InboxItemLocalItem
                E.where_
                    $ ( E.isNothing (ibr E.?. InboxItemRemoteInbox) E.||.
                        ibr E.?. InboxItemRemoteInbox E.==. E.just (E.val ibid)
                      )
                    E.&&.
                      ( E.isNothing (ibl E.?. InboxItemLocalInbox) E.||.
                        ibl E.?. InboxItemLocalInbox E.==. E.just (E.val ibid)
                      )
                    E.&&.
                      ib E.^. InboxItemUnread E.==. E.val True
                E.orderBy [E.desc $ ib E.^. InboxItemId]
                return
                    ( ib E.^. InboxItemId
                    , ob E.?. OutboxItemActivity
                    , ract E.?. RemoteActivityContent
                    )
    adaptItem (E.Value ibid, E.Value mact, E.Value mobj) =
        case (mact, mobj) of
            (Nothing, Nothing) ->
                error $
                    "InboxItem #" ++ show ibid ++ " neither local nor remote"
            (Just _, Just _) ->
                error $ "InboxItem #" ++ show ibid ++ " both local and remote"
            (Just act, Nothing) -> (ibid, Left $ persistJSONValue act)
            (Nothing, Just obj) -> (ibid, Right $ persistJSONValue obj)

postNotificationsR :: ShrIdent -> Handler Html
postNotificationsR shr = do
    ((result, _widget), _enctype) <- runFormPost $ notificationForm Nothing
    case result of
        FormSuccess mitem -> do
            (multi, markedUnread) <- runDB $ do
                sid <- getKeyBy404 $ UniqueSharer shr
                p <- getValBy404 $ UniquePersonIdent sid
                let ibid = personInbox p
                case mitem of
                    Nothing -> do
                        ibiids <- map E.unValue <$> getItems ibid
                        updateWhere
                            [InboxItemId <-. ibiids]
                            [InboxItemUnread =. False]
                        return (True, False)
                    Just (ibiid, unread) -> do
                        mibl <- getValBy $ UniqueInboxItemLocalItem ibiid
                        mibr <- getValBy $ UniqueInboxItemRemoteItem ibiid
                        mib <-
                            requireEitherM
                                mibl
                                mibr
                                "Unused InboxItem"
                                "InboxItem used more than once"
                        let samePid =
                                case mib of
                                    Left ibl ->
                                        inboxItemLocalInbox ibl == ibid
                                    Right ibr ->
                                        inboxItemRemoteInbox ibr == ibid
                        if samePid
                            then do
                                update ibiid [InboxItemUnread =. unread]
                                return (False, unread)
                            else
                                permissionDenied
                                    "Notification belongs to different user"
            setMessage $
                if multi
                    then "Items marked as read."
                    else if markedUnread
                        then "Item marked as unread."
                        else "Item marked as read."
        FormMissing -> do
            setMessage "Field(s) missing"
        FormFailure l -> do
            setMessage $ toHtml $ "Marking as read failed:" <> T.pack (show l)
    redirect $ NotificationsR shr
    where
    getItems ibid =
        E.select $ E.from $
            \ (ib `E.LeftOuterJoin` ibl `E.LeftOuterJoin` ibr) -> do
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibr E.?. InboxItemRemoteItem
                E.on $ E.just (ib E.^. InboxItemId) E.==. ibl E.?. InboxItemLocalItem
                E.where_
                    $ ( E.isNothing (ibr E.?. InboxItemRemoteInbox) E.||.
                        ibr E.?. InboxItemRemoteInbox E.==. E.just (E.val ibid)
                      )
                    E.&&.
                      ( E.isNothing (ibl E.?. InboxItemLocalInbox) E.||.
                        ibl E.?. InboxItemLocalInbox E.==. E.just (E.val ibid)
                      )
                    E.&&.
                      ib E.^. InboxItemUnread E.==. E.val True
                return $ ib E.^. InboxItemId
    -- TODO copied from Vervis.Federation, put this in 1 place
    requireEitherM
        :: MonadIO m => Maybe a -> Maybe b -> String -> String -> m (Either a b)
    requireEitherM mx my f t =
        case requireEither mx my of
            Left b    -> liftIO $ throwIO $ userError $ if b then t else f
            Right exy -> return exy