2019-01-19 10:56:50 +09:00
|
|
|
{- 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
|
2019-04-21 19:58:57 +09:00
|
|
|
, getSharerInboxR
|
|
|
|
, getProjectInboxR
|
|
|
|
, postSharerInboxR
|
|
|
|
, postProjectInboxR
|
2019-03-22 14:17:54 +09:00
|
|
|
, getPublishR
|
2019-01-22 00:54:57 +09:00
|
|
|
, getOutboxR
|
2019-03-29 06:08:30 +09:00
|
|
|
, getOutboxItemR
|
2019-01-22 00:54:57 +09:00
|
|
|
, postOutboxR
|
2019-02-07 19:34:33 +09:00
|
|
|
, getActorKey1R
|
|
|
|
, getActorKey2R
|
2019-01-19 10:56:50 +09:00
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Prelude
|
|
|
|
|
|
|
|
import Control.Applicative ((<|>))
|
|
|
|
import Control.Concurrent.STM.TVar (readTVarIO, modifyTVar')
|
|
|
|
import Control.Exception (displayException)
|
2019-03-10 15:42:03 +09:00
|
|
|
import Control.Monad
|
2019-01-19 10:56:50 +09:00
|
|
|
import Control.Monad.IO.Class (liftIO)
|
2019-03-22 07:57:15 +09:00
|
|
|
import Control.Monad.Logger.CallStack
|
2019-01-19 10:56:50 +09:00
|
|
|
import Control.Monad.STM (atomically)
|
2019-03-10 15:42:03 +09:00
|
|
|
import Control.Monad.Trans.Except
|
2019-02-22 08:59:53 +09:00
|
|
|
import Control.Monad.Trans.Maybe
|
2019-01-19 10:56:50 +09:00
|
|
|
import Crypto.Error (CryptoFailable (..))
|
|
|
|
import Crypto.PubKey.Ed25519 (publicKey, signature, verify)
|
2019-01-22 00:54:57 +09:00
|
|
|
import Data.Aeson
|
2019-01-19 10:56:50 +09:00
|
|
|
import Data.Bifunctor (first, second)
|
2019-02-15 08:27:40 +09:00
|
|
|
import Data.Foldable (for_)
|
2019-01-19 10:56:50 +09:00
|
|
|
import Data.HashMap.Strict (HashMap)
|
2019-04-23 11:57:53 +09:00
|
|
|
import Data.List
|
2019-01-22 00:54:57 +09:00
|
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
2019-04-19 12:14:12 +09:00
|
|
|
import Data.Maybe
|
2019-02-07 19:34:33 +09:00
|
|
|
import Data.PEM (PEM (..))
|
2019-01-19 10:56:50 +09:00
|
|
|
import Data.Text (Text)
|
2019-04-23 11:57:53 +09:00
|
|
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8')
|
2019-01-19 10:56:50 +09:00
|
|
|
import Data.Text.Lazy.Encoding (decodeUtf8)
|
|
|
|
import Data.Time.Clock (UTCTime, getCurrentTime)
|
|
|
|
import Data.Time.Interval (TimeInterval, toTimeUnit)
|
|
|
|
import Data.Time.Units (Second)
|
2019-02-15 08:27:40 +09:00
|
|
|
import Database.Persist (Entity (..), getBy, insertBy, insert_)
|
2019-01-19 10:56:50 +09:00
|
|
|
import Network.HTTP.Client (Manager, HttpException, requestFromURI)
|
2019-01-19 11:57:58 +09:00
|
|
|
import Network.HTTP.Simple (httpJSONEither, getResponseBody, setRequestManager, addRequestHeader)
|
2019-01-22 00:54:57 +09:00
|
|
|
import Network.HTTP.Types.Header (hDate, hHost)
|
2019-04-23 11:57:53 +09:00
|
|
|
import Network.HTTP.Types.Status
|
2019-01-19 10:56:50 +09:00
|
|
|
import Text.Blaze.Html (Html)
|
2019-02-12 20:53:24 +09:00
|
|
|
import Text.Shakespeare.I18N (RenderMessage)
|
2019-01-19 10:56:50 +09:00
|
|
|
import UnliftIO.Exception (try)
|
2019-01-22 00:54:57 +09:00
|
|
|
import Yesod.Auth (requireAuth)
|
2019-02-12 20:53:24 +09:00
|
|
|
import Yesod.Core (ContentType, defaultLayout, whamlet, toHtml, HandlerSite)
|
2019-02-07 19:34:33 +09:00
|
|
|
import Yesod.Core.Content (TypedContent)
|
2019-01-19 10:56:50 +09:00
|
|
|
import Yesod.Core.Json (requireJsonBody)
|
|
|
|
import Yesod.Core.Handler
|
2019-02-12 20:53:24 +09:00
|
|
|
import Yesod.Form.Fields (Textarea (..), textField, textareaField)
|
|
|
|
import Yesod.Form.Functions
|
|
|
|
import Yesod.Form.Types
|
2019-01-22 00:54:57 +09:00
|
|
|
import Yesod.Persist.Core (runDB, get404)
|
2019-01-19 10:56:50 +09:00
|
|
|
|
|
|
|
import qualified Data.ByteString.Char8 as BC (unpack)
|
|
|
|
import qualified Data.CaseInsensitive as CI (mk)
|
2019-01-22 00:54:57 +09:00
|
|
|
import qualified Data.HashMap.Strict as M (lookup, insert, adjust, fromList)
|
2019-03-05 00:47:22 +09:00
|
|
|
import qualified Data.Text as T (pack, unpack, concat)
|
2019-01-22 00:54:57 +09:00
|
|
|
import qualified Data.Text.Lazy as TL (toStrict)
|
2019-03-14 11:30:36 +09:00
|
|
|
import qualified Data.Vector as V
|
2019-01-19 10:56:50 +09:00
|
|
|
import qualified Network.Wai as W (requestMethod, rawPathInfo, requestHeaders)
|
|
|
|
|
|
|
|
import Network.HTTP.Signature hiding (Algorithm (..))
|
2019-01-19 13:21:56 +09:00
|
|
|
import Yesod.HttpSignature (verifyRequestSignature)
|
2019-01-19 10:56:50 +09:00
|
|
|
|
|
|
|
import qualified Network.HTTP.Signature as S (Algorithm (..))
|
|
|
|
|
2019-04-23 11:57:53 +09:00
|
|
|
import Data.Aeson.Encode.Pretty
|
2019-03-22 07:57:15 +09:00
|
|
|
import Data.Aeson.Local
|
2019-03-10 02:12:43 +09:00
|
|
|
import Database.Persist.Local
|
2019-02-08 08:08:28 +09:00
|
|
|
import Network.FedURI
|
2019-01-22 00:54:57 +09:00
|
|
|
import Web.ActivityPub
|
2019-02-12 20:53:24 +09:00
|
|
|
import Yesod.Auth.Unverified
|
2019-03-23 11:05:30 +09:00
|
|
|
import Yesod.FedURI
|
2019-03-29 12:25:32 +09:00
|
|
|
import Yesod.Hashids
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-02-07 19:34:33 +09:00
|
|
|
import Vervis.ActorKey
|
2019-03-22 07:57:15 +09:00
|
|
|
import Vervis.Federation
|
2019-01-22 00:54:57 +09:00
|
|
|
import Vervis.Foundation
|
|
|
|
import Vervis.Model
|
2019-03-25 09:17:24 +09:00
|
|
|
import Vervis.Model.Ident
|
2019-03-10 02:12:43 +09:00
|
|
|
import Vervis.RemoteActorStore
|
2019-03-25 09:17:24 +09:00
|
|
|
import Vervis.Settings
|
2019-01-19 10:56:50 +09:00
|
|
|
|
|
|
|
getInboxR :: Handler Html
|
|
|
|
getInboxR = do
|
|
|
|
acts <- liftIO . readTVarIO =<< getsYesod appActivities
|
|
|
|
defaultLayout
|
|
|
|
[whamlet|
|
|
|
|
<p>
|
2019-04-21 19:58:57 +09:00
|
|
|
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.
|
2019-01-19 10:56:50 +09:00
|
|
|
<p>Last 10 activities posted:
|
|
|
|
<ul>
|
2019-04-23 11:57:53 +09:00
|
|
|
$forall ActivityReport time msg ctypes body <- acts
|
2019-01-19 10:56:50 +09:00
|
|
|
<li>
|
|
|
|
<div>#{show time}
|
2019-04-23 11:57:53 +09:00
|
|
|
<div>#{msg}
|
|
|
|
<div><code>#{intercalate " | " $ map BC.unpack ctypes}
|
|
|
|
<div><pre>#{decodeUtf8 body}
|
2019-01-19 10:56:50 +09:00
|
|
|
|]
|
|
|
|
|
2019-04-21 19:58:57 +09:00
|
|
|
getSharerInboxR :: ShrIdent -> Handler TypedContent
|
|
|
|
getSharerInboxR _ = error "TODO implement getSharerInboxR"
|
|
|
|
|
|
|
|
getProjectInboxR :: ShrIdent -> PrjIdent -> Handler TypedContent
|
|
|
|
getProjectInboxR _ _ = error "TODO implement getProjectInboxR"
|
|
|
|
|
2019-04-23 11:57:53 +09:00
|
|
|
postSharerInboxR :: ShrIdent -> Handler ()
|
|
|
|
postSharerInboxR shrRecip = do
|
2019-03-25 09:17:24 +09:00
|
|
|
federation <- getsYesod $ appFederation . appSettings
|
|
|
|
unless federation badMethod
|
2019-04-23 11:57:53 +09:00
|
|
|
contentTypes <- lookupHeaders "Content-Type"
|
|
|
|
body <- requireJsonBody
|
2019-01-19 10:56:50 +09:00
|
|
|
now <- liftIO getCurrentTime
|
2019-04-23 11:57:53 +09:00
|
|
|
result <- go now contentTypes body
|
|
|
|
recordActivity now result contentTypes body
|
|
|
|
case result of
|
|
|
|
Left _ -> sendResponseStatus badRequest400 ()
|
|
|
|
Right _ -> return ()
|
2019-01-19 10:56:50 +09:00
|
|
|
where
|
2019-04-23 11:57:53 +09:00
|
|
|
go now ctypes (WithValue raw (Doc hActivity activity)) = runExceptT $ do
|
|
|
|
verifyContentType
|
|
|
|
HttpSigVerResult result <-
|
|
|
|
ExceptT $
|
|
|
|
first (T.pack . displayException) <$>
|
|
|
|
verifyRequestSignature now
|
|
|
|
ActorDetail uSender iid _raid <- ExceptT $ pure $ first T.pack result
|
|
|
|
let (hSender, luSender) = f2l uSender
|
|
|
|
unless (hSender == hActivity) $
|
|
|
|
throwE $ T.concat
|
|
|
|
[ "Activity host <", hActivity
|
|
|
|
, "> doesn't match signature key host <", hSender, ">"
|
|
|
|
]
|
|
|
|
unless (activityActor activity == luSender) $
|
|
|
|
throwE $ T.concat
|
|
|
|
[ "Activity's actor <"
|
|
|
|
, renderFedURI $ l2f hActivity $ activityActor activity
|
|
|
|
, "> != Signature key's actor <", renderFedURI uSender, ">"
|
|
|
|
]
|
|
|
|
handleSharerInbox now shrRecip iid raw activity
|
|
|
|
where
|
|
|
|
verifyContentType =
|
|
|
|
case ctypes of
|
|
|
|
[] -> throwE "Content-Type not specified"
|
|
|
|
[x] | x == typeAS -> return ()
|
|
|
|
| x == typeAS2 -> return ()
|
|
|
|
| otherwise ->
|
|
|
|
throwE $ "Not a recognized AP Content-Type: " <>
|
|
|
|
case decodeUtf8' x of
|
|
|
|
Left _ -> T.pack (show x)
|
|
|
|
Right t -> t
|
|
|
|
_ -> throwE "More than one Content-Type specified"
|
|
|
|
where
|
|
|
|
typeAS = "application/activity+json"
|
|
|
|
typeAS2 =
|
|
|
|
"application/ld+json; \
|
|
|
|
\profile=\"https://www.w3.org/ns/activitystreams\""
|
|
|
|
recordActivity now result contentTypes body = do
|
2019-03-22 07:57:15 +09:00
|
|
|
acts <- getsYesod appActivities
|
|
|
|
liftIO $ atomically $ modifyTVar' acts $ \ vec ->
|
2019-04-23 11:57:53 +09:00
|
|
|
let msg = either id id result
|
|
|
|
formattedBody = encodePretty $ wvRaw body
|
|
|
|
item = ActivityReport now msg contentTypes formattedBody
|
|
|
|
vec' = item `V.cons` vec
|
2019-03-22 07:57:15 +09:00
|
|
|
in if V.length vec' > 10
|
|
|
|
then V.init vec'
|
|
|
|
else vec'
|
2019-04-21 19:58:57 +09:00
|
|
|
|
|
|
|
postProjectInboxR :: ShrIdent -> PrjIdent -> Handler ()
|
|
|
|
postProjectInboxR _ _ = error "TODO implement postProjectInboxR"
|
|
|
|
|
2019-02-12 20:53:24 +09:00
|
|
|
{-
|
2019-01-22 00:54:57 +09:00
|
|
|
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
|
2019-02-12 20:53:24 +09:00
|
|
|
-}
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-02-12 20:53:24 +09:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-04-19 12:14:12 +09:00
|
|
|
ticketField
|
|
|
|
:: (Route App -> LocalURI) -> Field Handler (Text, ShrIdent, PrjIdent, Int)
|
|
|
|
ticketField encodeRouteLocal = checkMMap toTicket fromTicket fedUriField
|
2019-01-22 00:54:57 +09:00
|
|
|
where
|
2019-04-19 12:14:12 +09:00
|
|
|
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
|
|
|
|
|
|
|
|
activityForm :: Form ((Text, ShrIdent, PrjIdent, Int), Maybe FedURI, Text)
|
|
|
|
activityForm 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" ""
|
2019-03-22 08:56:47 +09:00
|
|
|
defmsg = "Hi! I'm testing federation. Can you see my message? :)"
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-03-29 06:08:30 +09:00
|
|
|
activityWidget :: ShrIdent -> Widget -> Enctype -> Widget
|
|
|
|
activityWidget shr widget enctype =
|
2019-01-22 00:54:57 +09:00
|
|
|
[whamlet|
|
2019-02-12 20:53:24 +09:00
|
|
|
<p>
|
|
|
|
This is a federation test page. Provide a recepient actor URI and
|
|
|
|
message text, and a Create activity creating a new Note will be sent
|
|
|
|
to the destination server.
|
2019-03-29 06:08:30 +09:00
|
|
|
<form method=POST action=@{OutboxR shr} enctype=#{enctype}>
|
2019-01-22 00:54:57 +09:00
|
|
|
^{widget}
|
|
|
|
<input type=submit>
|
|
|
|
|]
|
|
|
|
|
2019-03-29 06:08:30 +09:00
|
|
|
getUserShrIdent :: Handler ShrIdent
|
|
|
|
getUserShrIdent = do
|
|
|
|
Entity _ p <- requireVerifiedAuth
|
|
|
|
s <- runDB $ get404 $ personIdent p
|
|
|
|
return $ sharerIdent s
|
|
|
|
|
2019-03-22 14:17:54 +09:00
|
|
|
getPublishR :: Handler Html
|
|
|
|
getPublishR = do
|
2019-03-29 06:08:30 +09:00
|
|
|
shr <- getUserShrIdent
|
2019-01-22 00:54:57 +09:00
|
|
|
((_result, widget), enctype) <- runFormPost activityForm
|
2019-03-29 06:08:30 +09:00
|
|
|
defaultLayout $ activityWidget shr widget enctype
|
2019-01-22 00:54:57 +09:00
|
|
|
|
2019-03-29 06:08:30 +09:00
|
|
|
getOutboxR :: ShrIdent -> Handler TypedContent
|
2019-03-22 14:17:54 +09:00
|
|
|
getOutboxR = error "Not implemented yet"
|
|
|
|
|
2019-03-29 12:25:32 +09:00
|
|
|
getOutboxItemR :: ShrIdent -> KeyHashid OutboxItem -> Handler TypedContent
|
2019-03-29 06:08:30 +09:00
|
|
|
getOutboxItemR = error "Not implemented yet"
|
|
|
|
|
|
|
|
postOutboxR :: ShrIdent -> Handler Html
|
2019-04-19 12:14:12 +09:00
|
|
|
postOutboxR shrAuthor = do
|
2019-03-25 09:17:24 +09:00
|
|
|
federation <- getsYesod $ appFederation . appSettings
|
|
|
|
unless federation badMethod
|
2019-01-22 00:54:57 +09:00
|
|
|
((result, widget), enctype) <- runFormPost activityForm
|
2019-04-19 12:14:12 +09:00
|
|
|
elmid <- runExceptT $ do
|
|
|
|
((hTicket, shrTicket, prj, num), muParent, msg) <-
|
|
|
|
case result of
|
|
|
|
FormMissing -> throwE "Field(s) missing"
|
|
|
|
FormFailure _l -> throwE "Invalid input, see below"
|
|
|
|
FormSuccess r -> return r
|
|
|
|
encodeRouteFed <- getEncodeRouteFed
|
|
|
|
encodeRouteLocal <- getEncodeRouteLocal
|
|
|
|
let encodeRecipRoute = l2f hTicket . encodeRouteLocal
|
|
|
|
uTicket = encodeRecipRoute $ TicketR shrTicket prj num
|
|
|
|
now <- liftIO getCurrentTime
|
|
|
|
let (hLocal, luAuthor) = f2l $ encodeRouteFed $ SharerR shrAuthor
|
|
|
|
recips =
|
|
|
|
[ ProjectR shrTicket prj
|
|
|
|
, TicketParticipantsR shrTicket prj num
|
|
|
|
, TicketTeamR shrTicket prj num
|
2019-04-18 19:38:01 +09:00
|
|
|
]
|
2019-04-19 12:14:12 +09:00
|
|
|
note = Note
|
|
|
|
{ noteId = Nothing
|
|
|
|
, noteAttrib = luAuthor
|
|
|
|
, noteAudience = Audience
|
|
|
|
{ audienceTo = map encodeRecipRoute recips
|
|
|
|
, audienceBto = []
|
|
|
|
, audienceCc = []
|
|
|
|
, audienceBcc = []
|
|
|
|
, audienceGeneral = []
|
|
|
|
}
|
|
|
|
, noteReplyTo = Just $ fromMaybe uTicket muParent
|
|
|
|
, noteContext = Just uTicket
|
|
|
|
, notePublished = Just now
|
|
|
|
, noteContent = msg
|
|
|
|
}
|
|
|
|
ExceptT $ handleOutboxNote hLocal note
|
|
|
|
case elmid of
|
|
|
|
Left err -> setMessage $ toHtml err
|
|
|
|
Right lmid -> do
|
|
|
|
lmkhid <- encodeKeyHashid lmid
|
|
|
|
renderUrl <- getUrlRender
|
|
|
|
let u = renderUrl $ MessageR shrAuthor lmkhid
|
|
|
|
setMessage $ toHtml $ "Message created! ID: " <> u
|
|
|
|
defaultLayout $ activityWidget shrAuthor widget enctype
|
2019-02-07 19:34:33 +09:00
|
|
|
|
|
|
|
getActorKey :: ((ActorKey, ActorKey, Bool) -> ActorKey) -> Route App -> Handler TypedContent
|
2019-03-20 21:01:10 +09:00
|
|
|
getActorKey choose route = selectRep $ provideAP $ do
|
2019-02-07 19:34:33 +09:00
|
|
|
actorKey <-
|
|
|
|
liftIO . fmap (actorKeyPublicBin . choose) . readTVarIO =<<
|
|
|
|
getsYesod appActorKeys
|
2019-03-23 11:05:30 +09:00
|
|
|
route2uri <- getEncodeRouteFed
|
2019-02-22 08:59:53 +09:00
|
|
|
let (host, id_) = f2l $ route2uri route
|
2019-03-20 21:01:10 +09:00
|
|
|
return $ Doc host PublicKey
|
|
|
|
{ publicKeyId = id_
|
|
|
|
, publicKeyExpires = Nothing
|
|
|
|
, publicKeyOwner = OwnerInstance
|
|
|
|
, publicKeyMaterial = actorKey
|
|
|
|
--, publicKeyAlgo = Just AlgorithmEd25519
|
|
|
|
}
|
2019-02-07 19:34:33 +09:00
|
|
|
|
|
|
|
getActorKey1R :: Handler TypedContent
|
|
|
|
getActorKey1R = getActorKey (\ (k1, _, _) -> k1) ActorKey1R
|
|
|
|
|
|
|
|
getActorKey2R :: Handler TypedContent
|
2019-03-06 10:49:55 +09:00
|
|
|
getActorKey2R = getActorKey (\ (_, k2, _) -> k2) ActorKey2R
|