Eventually-decentralized project hosting and management platform

[[ 🗃 ^WvWbo vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Changes]

Clone

HTTPS: darcs clone https://vervis.peers.community/repos/WvWbo

SSH: darcs clone USERNAME@vervis.peers.community:WvWbo

Tags

TODO

src / Vervis / Federation /

Offer.hs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
{- This file is part of Vervis.
 -
 - Written in 2019, 2020, 2022, 2023 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/>.
 -}

{-# LANGUAGE RankNTypes #-}

module Vervis.Federation.Offer
    ( --sharerAcceptF

    --, sharerRejectF

      personFollowF
    , deckFollowF
    , loomFollowF
    , repoFollowF

    , personUndoF
    , deckUndoF
    , loomUndoF
    , repoUndoF
    )
where

import Control.Applicative
import Control.Exception hiding (Handler)
import Control.Monad
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Data.Aeson
import Data.Bifunctor
import Data.Bitraversable
import Data.ByteString (ByteString)
import Data.Either
import Data.Foldable
import Data.Function
import Data.List (nub, union)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe
import Data.Text (Text)
import Data.Time.Calendar
import Data.Time.Clock
import Data.Traversable
import Database.Persist
import Database.Persist.Sql
import Text.Blaze.Html (preEscapedToHtml)
import Text.Blaze.Html.Renderer.Text
import Yesod.Core hiding (logError, logWarn, logInfo, logDebug)
import Yesod.Core.Handler
import Yesod.Persist.Core

import qualified Data.List.NonEmpty as NE
import qualified Data.List.Ordered as LO
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL

import Database.Persist.JSON
import Network.FedURI
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite

import qualified Web.ActivityPub as AP

import Control.Monad.Trans.Except.Local
import Data.Tuple.Local
import Database.Persist.Local
import Yesod.Persist.Local

import Vervis.Access
import Vervis.ActivityPub
import Vervis.Actor
import Vervis.Cloth
import Vervis.Data.Actor
import Vervis.FedURI
import Vervis.Federation.Auth
import Vervis.Federation.Util
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Vervis.Model.Ticket
import Vervis.Persist.Actor
import Vervis.Recipient
import Vervis.Ticket
import Vervis.Web.Delivery

{-
sharerAcceptF
    :: KeyHashid Person
    -> UTCTime
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (LocalRecipientSet, ByteString)
    -> LocalURI
    -> Accept URIMode
    -> ExceptT Text Handler Text
sharerAcceptF recipHash now author body mfwd luAccept (Accept (ObjURI hOffer luOffer) mresult) = do
    error "sharerAcceptF temporarily disabled"



    mres <- lift $ runDB $ do
        Entity pidRecip recip <- do
            sid <- getKeyBy404 $ UniqueSharer shr
            getBy404 $ UniquePersonIdent sid
        mractid <- insertToInbox now author body (personInbox recip) luAccept True
        for mractid $ \ ractid -> do
            mv <- runMaybeT $ asum
                [ insertFollow pidRecip (personOutbox recip) ractid
                , updateTicket pidRecip (personOutbox recip) ractid
                , insertDep mfwd (personInbox recip) ractid
                ]
            for mv $ bitraverse pure $ traverse $ \ ((localRecips, sig), collections) -> do
                let sieve = makeRecipientSet [] collections
                remoteRecips <-
                    insertRemoteActivityToLocalInboxes
                        False ractid $
                            localRecipSieve'
                                sieve False False localRecips
                (sig,) <$> deliverRemoteDB_S (actbBL body) ractid (personIdent recip) sig remoteRecips
    case mres of
        Nothing -> return "Activity already in my inbox"
        Just Nothing -> return "Activity inserted to my inbox"
        Just (Just (t, mfwd)) -> do
            for_ mfwd $ \ (sig, remotes) -> do
                forkWorker "sharerAcceptF inbox-forwarding" $
                    deliverRemoteHTTP_S now shr (actbBL body) sig remotes
            return t
    where
    insertFollow pidRecip obidRecip ractidAccept = do
        guard =<< hostIsLocal hOffer
        route <- MaybeT . pure $ decodeRouteLocal luOffer
        obiid <-
            case route of
                SharerOutboxItemR shr' obikhid
                    | shr == shr' -> decodeKeyHashidM obikhid
                _ -> MaybeT $ pure Nothing
        obi <- MaybeT $ get obiid
        guard $ outboxItemOutbox obi == obidRecip
        Entity frrid frr <- MaybeT $ getBy $ UniqueFollowRemoteRequestActivity obiid
        guard $ followRemoteRequestPerson frr == pidRecip
        let originalRecip =
                case followRemoteRequestRecip frr of
                    Nothing -> followRemoteRequestTarget frr
                    Just u -> u
        guard $ originalRecip == remoteAuthorURI author
        lift $ delete frrid
        lift $ insert_ FollowRemote
            { followRemotePerson = pidRecip
            , followRemoteRecip  = remoteAuthorId author
            , followRemoteTarget = followRemoteRequestTarget frr
            , followRemotePublic = followRemoteRequestPublic frr
            , followRemoteFollow = followRemoteRequestActivity frr
            , followRemoteAccept = ractidAccept
            }
        return ("Accept received for my follow request", Nothing)
    updateTicket pidRecip obidRecip ractidAccept = do
        guard =<< hostIsLocal hOffer
        route <- MaybeT . pure $ decodeRouteLocal luOffer
        obiid <-
            case route of
                SharerOutboxItemR shr' obikhid
                    | shr == shr' -> decodeKeyHashidM obikhid
                _ -> MaybeT $ pure Nothing
        obi <- MaybeT $ get obiid
        guard $ outboxItemOutbox obi == obidRecip
        Entity talid tal <- MaybeT $ getBy $ UniqueTicketAuthorLocalOpen obiid
        guard $ ticketAuthorLocalAuthor tal == pidRecip
        Entity tprid tpr <- MaybeT $ getBy $ UniqueTicketProjectRemote talid
        guard $ remoteAuthorId author == ticketProjectRemoteTracker tpr
        _tpraid <- MaybeT $ insertUnique TicketProjectRemoteAccept
            { ticketProjectRemoteAcceptTicket   = tprid
            , ticketProjectRemoteAcceptActivity = ractidAccept
            , ticketProjectRemoteAcceptAccept   = True
            , ticketProjectRemoteAcceptResult   = mresult
            }
        return ("Accept received for my ticket", Nothing)
    insertDep msig ibidRecip ractidAccept = do
        luResult <- MaybeT $ pure mresult
        hl <- hostIsLocal hOffer
        ibiidOffer <-
            if hl
                then do
                    route <- MaybeT . pure $ decodeRouteLocal luOffer
                    obiid <-
                        case route of
                            SharerOutboxItemR shr' obikhid -> do
                                obiid <- decodeKeyHashidM obikhid
                                obi <- MaybeT $ get obiid
                                p <- do
                                    sid <- MaybeT $ getKeyBy $ UniqueSharer shr'
                                    MaybeT $ getValBy $ UniquePersonIdent sid
                                guard $ personOutbox p == outboxItemOutbox obi
                                return obiid
                            _ -> MaybeT $ pure Nothing
                    inboxItemLocalItem <$>
                        MaybeT (getValBy $ UniqueInboxItemLocal ibidRecip obiid)
                else do
                    iid <- MaybeT $ getKeyBy $ UniqueInstance hOffer
                    roid <- MaybeT $ getKeyBy $ UniqueRemoteObject iid luOffer
                    ractid <- MaybeT $ getKeyBy $ UniqueRemoteActivity roid
                    inboxItemRemoteItem <$>
                        MaybeT (getValBy $ UniqueInboxItemRemote ibidRecip ractid)
        Entity tdoid tdo <-
            MaybeT $ getBy $ UniqueTicketDependencyOffer ibiidOffer
        let ltidChild = ticketDependencyOfferChild tdo
        child <- lift $ getWorkItem ltidChild
        (talid, patch) <-
            case child of
                WorkItemSharerTicket shr' t p | shr == shr' -> return (t, p)
                _ -> MaybeT $ pure Nothing
        lift $ do
            delete tdoid
            roidResult <-
                let iid = remoteAuthorInstance author
                in  either entityKey id <$>
                        insertBy' (RemoteObject iid luResult)
            insert_ RemoteTicketDependency
                { remoteTicketDependencyIdent  = roidResult
                , remoteTicketDependencyChild  = ltidChild
                , remoteTicketDependencyAccept = ractidAccept
                }
        talkhid <- encodeKeyHashid talid
        let collections =
                [ let coll =
                        if patch
                            then LocalPersonCollectionSharerProposalFollowers
                            else LocalPersonCollectionSharerTicketFollowers
                  in  coll shr talkhid
                ]
        return
            ( "Inserted remote reverse ticket dep"
            , (,collections) <$> msig
            )
-}

{-
sharerRejectF
    :: KeyHashid Person
    -> UTCTime
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (LocalRecipientSet, ByteString)
    -> LocalURI
    -> Reject URIMode
    -> ExceptT Text Handler Text
sharerRejectF recipHash now author body mfwd luReject (Reject (ObjURI hOffer luOffer)) = do
    error "sharerRejectF temporarily disabled"





    lift $ runDB $ do
        Entity pidRecip recip <- do
            sid <- getKeyBy404 $ UniqueSharer shr
            getBy404 $ UniquePersonIdent sid
        mractid <- insertToInbox now author body (personInbox recip) luReject True
        encodeRouteLocal <- getEncodeRouteLocal
        let me = localUriPath $ encodeRouteLocal $ SharerR shr
        case mractid of
            Nothing -> return $ "Activity already exists in inbox of " <> me
            Just ractid -> do
                mv <- deleteFollow pidRecip (personOutbox recip)
                case mv of
                    Nothing ->
                        return $ "Activity inserted to inbox of " <> me
                    Just () ->
                        return $ "Reject received for follow request by " <> me
    where
    deleteFollow pidRecip obidRecip = runMaybeT $ do
        guard =<< hostIsLocal hOffer
        route <- MaybeT . pure $ decodeRouteLocal luOffer
        obiid <-
            case route of
                SharerOutboxItemR shr' obikhid
                    | shr == shr' -> decodeKeyHashidM obikhid
                _ -> MaybeT $ pure Nothing
        obi <- MaybeT $ get obiid
        guard $ outboxItemOutbox obi == obidRecip
        Entity frrid frr <- MaybeT $ getBy $ UniqueFollowRemoteRequestActivity obiid
        guard $ followRemoteRequestPerson frr == pidRecip
        let originalRecip =
                case followRemoteRequestRecip frr of
                    Nothing -> followRemoteRequestTarget frr
                    Just u -> u
        guard $ originalRecip == remoteAuthorURI author
        lift $ delete frrid
-}

followF
    :: (PersistRecordBackend r SqlBackend, ToBackendKey SqlBackend r)
    => (Route App -> ExceptT Text Handler a)
    -> (r -> ActorId)
    -> Bool
    -> (Key r -> Actor -> a -> ExceptT Text AppDB FollowerSetId)
    -> (a -> AppDB RecipientRoutes)
    -> (forall f. f r -> LocalActorBy f)
    -> (a -> Handler [Aud URIMode])
    -> UTCTime
    -> KeyHashid r
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
followF parseFollowee grabActor unread getFollowee getSieve makeLocalActor makeAudience now recipHash author body mfwd luFollow (AP.Follow uObject _ hide) = (,Nothing) <$> do

    -- Check input
    recipID <- decodeKeyHashid404 recipHash
    followee <- nameExceptT "Follow object" $ do
        route <- do
            routeOrRemote <- parseFedURIOld uObject
            case routeOrRemote of
                Left route -> pure route
                Right _ -> throwE "Remote, so definitely not me/mine"
        parseFollowee route
    verifyNothingE
        (AP.activityCapability $ actbActivity body)
        "Capability not needed"

    maybeHttp <- runDBExcept $ do

        -- Find recipient actor in DB, returning 404 if doesn't exist because
        -- we're in the actor's inbox post handler
        recip <- lift $ get404 recipID
        let recipActorID = grabActor recip
        recipActor <- lift $ getJust recipActorID

        -- Insert the Follow to actor's inbox
        mractid <- lift $ insertToInbox now author body (actorInbox recipActor) luFollow unread
        for mractid $ \ followID -> do

            -- Find followee in DB
            followerSetID <- getFollowee recipID recipActor followee

            -- Verify not already following us
            let followerID = remoteAuthorId author
            maybeFollow <-
                lift $ getBy $ UniqueRemoteFollow followerID followerSetID
            verifyNothingE maybeFollow "You're already following this object"

            -- Forward the Follow activity to relevant local stages, and
            -- schedule delivery for unavailable remote members of them
            maybeHttpFwdFollow <- lift $ for mfwd $ \ (localRecips, sig) -> do
                sieve <- getSieve followee
                forwardActivityDB
                    (actbBL body) localRecips sig recipActorID
                    (makeLocalActor recipHash) sieve followID

            -- Record the new follow in DB
            acceptID <-
                lift $ insertEmptyOutboxItem (actorOutbox recipActor) now
            lift $ insert_ $ RemoteFollow followerID followerSetID (not hide) followID acceptID

            -- Prepare an Accept activity and insert to actor's outbox
            (actionAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
                lift $ prepareAccept followee
            _luAccept <- lift $ updateOutboxItem (makeLocalActor recipID) acceptID actionAccept

            -- Deliver the Accept to local recipients, and schedule delivery
            -- for unavailable remote recipients
            deliverHttpAccept <-
                deliverActivityDB
                    (makeLocalActor recipHash) recipActorID
                    localRecipsAccept remoteRecipsAccept fwdHostsAccept
                    acceptID actionAccept

            -- Return instructions for HTTP inbox-forwarding of the Follow
            -- activity, and for HTTP delivery of the Accept activity to
            -- remote recipients
            return (maybeHttpFwdFollow, deliverHttpAccept)

    -- Launch asynchronous HTTP forwarding of the Follow activity and HTTP
    -- delivery of the Accept activity
    case maybeHttp of
        Nothing ->
            return "I already have this activity in my inbox, doing nothing"
        Just (maybeHttpFwdFollow, deliverHttpAccept) -> do
            for_ maybeHttpFwdFollow $ forkWorker "followF inbox-forwarding"
            forkWorker "followF Accept HTTP delivery" deliverHttpAccept
            return $
                case maybeHttpFwdFollow of
                    Nothing -> "Recorded follow, no inbox-forwarding to do"
                    Just _ ->
                        "Recorded follow and ran inbox-forwarding of the Follow"

    where

    prepareAccept followee = do
        encodeRouteHome <- getEncodeRouteHome

        ra <- getJust $ remoteAuthorId author

        let ObjURI hAuthor luAuthor = remoteAuthorURI author

            audSender =
                AudRemote hAuthor
                    [luAuthor]
                    (maybeToList $ remoteActorFollowers ra)

        audsRecip <- lift $ makeAudience followee

        let (recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
                collectAudience $ audSender : audsRecip

            recips = map encodeRouteHome audLocal ++ audRemote
            action = AP.Action
                { AP.actionCapability = Nothing
                , AP.actionSummary    = Nothing
                , AP.actionAudience   = AP.Audience recips [] [] [] [] []
                , AP.actionFulfills   = []
                , AP.actionSpecific   = AP.AcceptActivity AP.Accept
                    { AP.acceptObject   = ObjURI hAuthor luFollow
                    , AP.acceptResult   = Nothing
                    }
                }

        return (action, recipientSet, remoteActors, fwdHosts)

{-
followF
    :: (Route App -> Maybe a)
    -> Route App
    -> (a -> AppDB (Maybe b))
    -> (b -> InboxId)
    -> (b -> OutboxId)
    -> (b -> FollowerSetId)
    -> (KeyHashid OutboxItem -> Route App)
    -> UTCTime
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (LocalRecipientSet, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler Text
followF
    objRoute recipRoute getRecip recipInbox recipOutbox recipFollowers outboxItemRoute
    now author body mfwd luFollow (AP.Follow (ObjURI hObj luObj) _mcontext hide) = do
        mobj <- do
            local <- hostIsLocal hObj
            return $
                if local
                    then objRoute =<< decodeRouteLocal luObj
                    else Nothing
        case mobj of
            Nothing -> return "Follow object unrelated to me, ignoring activity"
            Just obj -> do
                emsg <- lift $ runDB $ do
                    mrecip <- getRecip obj
                    case mrecip of
                        Nothing -> return $ Left "Follow object not found, ignoring activity"
                        Just recip -> do
                            newItem <- insertToInbox luFollow $ recipInbox recip
                            case newItem of
                                Nothing -> return $ Left "Activity already exists in inbox, not using"
                                Just ractid -> do
                                    let raidAuthor = remoteAuthorId author
                                    ra <- getJust raidAuthor
                                    ro <- getJust $ remoteActorIdent ra
                                    (obiid, doc) <-
                                        insertAcceptToOutbox
                                            ra
                                            luFollow
                                            (recipOutbox recip)
                                    newFollow <- insertFollow ractid obiid $ recipFollowers recip
                                    if newFollow
                                        then Right <$> do
                                            let raInfo = RemoteRecipient raidAuthor (remoteObjectIdent ro) (remoteActorInbox ra) (remoteActorErrorSince ra)
                                                iidAuthor = remoteAuthorInstance author
                                                hAuthor = objUriAuthority $ remoteAuthorURI author
                                                hostSection = ((iidAuthor, hAuthor), raInfo :| [])
                                            (obiid, doc,) <$> deliverRemoteDB [] obiid [] [hostSection]
                                        else do
                                            delete obiid
                                            return $ Left "You're already a follower of me"
                case emsg of
                    Left msg -> return msg
                    Right (obiid, doc, remotesHttp) -> do
                        forkWorker "followF: Accept delivery" $
                            deliverRemoteHttp' [] obiid doc remotesHttp
                        return "Follow request accepted"
    where
    insertToInbox luFollow ibidRecip = do
        let iidAuthor = remoteAuthorInstance author
        roid <-
            either entityKey id <$> insertBy' (RemoteObject iidAuthor luFollow)
        let jsonObj = persistJSONFromBL $ actbBL body
            ract = RemoteActivity roid jsonObj now
        ractid <- either entityKey id <$> insertBy' ract
        ibiid <- insert $ InboxItem True
        mibrid <- insertUnique $ InboxItemRemote ibidRecip ractid ibiid
        case mibrid of
            Nothing -> do
                delete ibiid
                return Nothing
            Just _ -> return $ Just ractid

    insertFollow ractid obiidA fsid = do
        let raid = remoteAuthorId author
        mrfid <- insertUnique $ RemoteFollow raid fsid (not hide) ractid obiidA
        return $ isJust mrfid

    insertAcceptToOutbox ra luFollow obidRecip = do
        now <- liftIO getCurrentTime
        let uAuthor@(ObjURI hAuthor luAuthor) = remoteAuthorURI author
        encodeRouteLocal <- getEncodeRouteLocal
        hLocal <- asksSite siteInstanceHost
        let recipPath = localUriPath $ encodeRouteLocal recipRoute
        summary <-
            TextHtml . TL.toStrict . renderHtml <$>
                withUrlRenderer
                    [hamlet|
                        <p>
                          <a href="#{renderObjURI uAuthor}">
                            $maybe name <- remoteActorName ra
                              #{name}
                            $nothing
                              #{renderAuthority hAuthor}#{localUriPath luAuthor}
                          \'s follow request accepted by #
                          <a href=@{recipRoute}>
                            #{renderAuthority hLocal}#{recipPath}
                          .
                    |]
        let accept luAct = Doc hLocal Activity
                { activityId       = luAct
                , activityActor    = encodeRouteLocal recipRoute
                , activityCapability = Nothing
                , activitySummary  = Just summary
                , activityAudience = Audience [uAuthor] [] [] [] [] []
                , activitySpecific = AcceptActivity Accept
                    { acceptObject = ObjURI hAuthor luFollow
                    , acceptResult = Nothing
                    }
                }
        obiid <- insert OutboxItem
            { outboxItemOutbox    = obidRecip
            , outboxItemActivity  = persistJSONObjectFromDoc $ accept Nothing
            , outboxItemPublished = now
            }
        obikhid <- encodeKeyHashid obiid
        let luAct = encodeRouteLocal $ outboxItemRoute obikhid
            doc = accept $ Just luAct
        update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
        return (obiid, doc)
-}

personFollowF
    :: UTCTime
    -> KeyHashid Person
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
personFollowF now recipPersonHash =
    followF
        (\case
            PersonR p | p == recipPersonHash -> pure ()
            _ -> throwE "Asking to follow someone else"
        )
        personActor
        True
        (\ _recipPersonID recipPersonActor () ->
            pure $ actorFollowers recipPersonActor
        )
        (\ () -> pure $ makeRecipientSet [] [])
        LocalActorPerson
        (\ () -> pure [])
        now
        recipPersonHash

deckFollowF
    :: UTCTime
    -> KeyHashid Deck
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
deckFollowF now recipDeckHash =
    followF
        (\case
            DeckR d | d == recipDeckHash -> pure Nothing
            TicketR d t | d == recipDeckHash ->
                Just <$> decodeKeyHashidE t "Invalid task keyhashid"
            _ -> throwE "Asking to follow someone else"
        )
        deckActor
        False
        (\ recipDeckID recipDeckActor maybeTaskID ->
            case maybeTaskID of
                Nothing -> pure $ actorFollowers recipDeckActor
                Just taskID -> do
                    maybeTicket <- lift $ getTicket recipDeckID taskID
                    (_deck, _task, Entity _ ticket, _author, _resolve) <-
                        fromMaybeE maybeTicket "I don't have this ticket in DB"
                    return $ ticketFollowers ticket
        )
        (\ _ -> pure $ makeRecipientSet [] [])
        LocalActorDeck
        (\ _ -> pure [])
        now
        recipDeckHash

loomFollowF
    :: UTCTime
    -> KeyHashid Loom
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
loomFollowF now recipLoomHash =
    followF
        (\case
            LoomR l | l == recipLoomHash -> pure Nothing
            ClothR l c | l == recipLoomHash ->
                Just <$> decodeKeyHashidE c "Invalid cloth keyhashid"
            _ -> throwE "Asking to follow someone else"
        )
        loomActor
        False
        (\ recipLoomID recipLoomActor maybeClothID ->
            case maybeClothID of
                Nothing -> pure $ actorFollowers recipLoomActor
                Just clothID -> do
                    maybeCloth <- lift $ getCloth recipLoomID clothID
                    (_loom, _cloth, Entity _ ticket, _author, _resolve, _merge) <-
                        fromMaybeE maybeCloth "I don't have this MR in DB"
                    return $ ticketFollowers ticket
        )
        (\ _ -> pure $ makeRecipientSet [] [])
        LocalActorLoom
        (\ _ -> pure [])
        now
        recipLoomHash

repoFollowF
    :: UTCTime
    -> KeyHashid Repo
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Follow URIMode
    -> ExceptT Text Handler (Text, Maybe (ExceptT Text Worker Text))
repoFollowF now recipRepoHash =
    followF
        (\case
            RepoR r | r == recipRepoHash -> pure ()
            _ -> throwE "Asking to follow someone else"
        )
        repoActor
        False
        (\ _recipRepoID recipRepoActor () ->
            pure $ actorFollowers recipRepoActor
        )
        (\ () -> pure $ makeRecipientSet [] [])
        LocalActorRepo
        (\ () -> pure [])
        now
        recipRepoHash

personUndoF
    :: UTCTime
    -> KeyHashid Person
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Undo URIMode
    -> ExceptT Text Handler Text
personUndoF now recipPersonHash author body mfwd luUndo (AP.Undo uObject) = do

    -- Check input
    recipPersonID <- decodeKeyHashid404 recipPersonHash
    undone <-
        first (\ (actor, _, item) -> (actor, item)) <$>
            parseActivityURI uObject

    -- Verify the capability URI, if provided, is one of:
    --   * Outbox item URI of a local actor, i.e. a local activity
    --   * A remote URI
    maybeCapability <-
        for (AP.activityCapability $ actbActivity body) $ \ uCap ->
            nameExceptT "Undo capability" $
                first (\ (actor, _, item) -> (actor, item)) <$>
                    parseActivityURI uCap

    maybeHttp <- runDBExcept $ do

        -- Find recipient person in DB, returning 404 if doesn't exist because we're
        -- in the person's inbox post handler
        (recipPersonActorID, recipPersonActor) <- lift $ do
            person <- get404 recipPersonID
            let actorID = personActor person
            (actorID,) <$> getJust actorID

        -- Insert the Undo to person's inbox
        mractid <- lift $ insertToInbox now author body (actorInbox recipPersonActor) luUndo False
        for mractid $ \ undoID -> do

            maybeUndo <- runMaybeT $ do

                -- Find the undone activity in our DB
                undoneDB <- MaybeT $ getActivity undone

                let followers = actorFollowers recipPersonActor
                MaybeT $ lift $ runMaybeT $ tryUnfollow followers undoneDB

            for maybeUndo $ \ (remoteFollowID, followerID) -> do

                (sieve, acceptAudience) <- do
                    (audSenderOnly, _audSenderAndFollowers) <- do
                        ra <- lift $ getJust $ remoteAuthorId author
                        let ObjURI hAuthor luAuthor = remoteAuthorURI author
                        return
                            ( AudRemote hAuthor [luAuthor] []
                            , AudRemote hAuthor
                                [luAuthor]
                                (maybeToList $ remoteActorFollowers ra)
                            )
                    unless (followerID == remoteAuthorId author) $
                        throwE "Trying to undo someone else's Follow"
                    lift $ delete remoteFollowID
                    return
                        ( makeRecipientSet [] []
                        , [audSenderOnly]
                        )

                -- Forward the Undo activity to relevant local stages, and
                -- schedule delivery for unavailable remote members of them
                maybeHttpFwdUndo <- lift $ for mfwd $ \ (localRecips, sig) ->
                    forwardActivityDB
                        (actbBL body) localRecips sig recipPersonActorID
                        (LocalActorPerson recipPersonHash) sieve undoID


                -- Prepare an Accept activity and insert to person's outbox
                acceptID <- lift $ insertEmptyOutboxItem (actorOutbox recipPersonActor) now
                (actionAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
                    lift . lift $ prepareAccept acceptAudience
                _luAccept <- lift $ updateOutboxItem (LocalActorPerson recipPersonID) acceptID actionAccept

                -- Deliver the Accept to local recipients, and schedule delivery
                -- for unavailable remote recipients
                deliverHttpAccept <-
                    deliverActivityDB
                        (LocalActorPerson recipPersonHash) recipPersonActorID
                        localRecipsAccept remoteRecipsAccept fwdHostsAccept
                        acceptID actionAccept

                -- Return instructions for HTTP inbox-forwarding of the Undo
                -- activity, and for HTTP delivery of the Accept activity to
                -- remote recipients
                return (maybeHttpFwdUndo, deliverHttpAccept)

    -- Launch asynchronous HTTP forwarding of the Undo activity and HTTP
    -- delivery of the Accept activity
    case maybeHttp of
        Nothing -> return "I already have this activity in my inbox, doing nothing"
        Just Nothing -> return "Unrelated to me, just inserted to inbox"
        Just (Just (maybeHttpFwdUndo, deliverHttpAccept)) -> do
            forkWorker "personUndoF Accept HTTP delivery" deliverHttpAccept
            case maybeHttpFwdUndo of
                Nothing -> return "Undid, no inbox-forwarding to do"
                Just forwardHttpUndo -> do
                    forkWorker "personUndoF inbox-forwarding" forwardHttpUndo
                    return "Undid and ran inbox-forwarding of the Undo"

    where

    tryUnfollow _               (Left _)                 = mzero
    tryUnfollow personFollowersID (Right remoteActivityID) = do
        Entity remoteFollowID remoteFollow <-
            MaybeT $ getBy $ UniqueRemoteFollowFollow remoteActivityID
        let followerID = remoteFollowActor remoteFollow
            followerSetID = remoteFollowTarget remoteFollow
        guard $ followerSetID == personFollowersID
        return (remoteFollowID, followerID)

    prepareAccept audience = do
        encodeRouteHome <- getEncodeRouteHome

        let ObjURI hAuthor _ = remoteAuthorURI author

            (recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
                collectAudience audience

            recips = map encodeRouteHome audLocal ++ audRemote
            action = AP.Action
                { AP.actionCapability = Nothing
                , AP.actionSummary    = Nothing
                , AP.actionAudience   = AP.Audience recips [] [] [] [] []
                , AP.actionFulfills   = []
                , AP.actionSpecific   = AP.AcceptActivity AP.Accept
                    { AP.acceptObject   = ObjURI hAuthor luUndo
                    , AP.acceptResult   = Nothing
                    }
                }

        return (action, recipientSet, remoteActors, fwdHosts)

deckUndoF
    :: UTCTime
    -> KeyHashid Deck
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Undo URIMode
    -> ExceptT Text Handler Text
deckUndoF now recipDeckHash author body mfwd luUndo (AP.Undo uObject) = do

    -- Check input
    recipDeckID <- decodeKeyHashid404 recipDeckHash
    undone <-
        first (\ (actor, _, item) -> (actor, item)) <$>
            parseActivityURI uObject

    -- Verify the capability URI, if provided, is one of:
    --   * Outbox item URI of a local actor, i.e. a local activity
    --   * A remote URI
    maybeCapability <-
        for (AP.activityCapability $ actbActivity body) $ \ uCap ->
            nameExceptT "Undo capability" $
                first (\ (actor, _, item) -> (actor, item)) <$>
                    parseActivityURI uCap

    maybeHttp <- runDBExcept $ do

        -- Find recipient deck in DB, returning 404 if doesn't exist because we're
        -- in the deck's inbox post handler
        (recipDeckActorID, recipDeckActor) <- lift $ do
            deck <- get404 recipDeckID
            let actorID = deckActor deck
            (actorID,) <$> getJust actorID

        -- Insert the Undo to deck's inbox
        mractid <- lift $ insertToInbox now author body (actorInbox recipDeckActor) luUndo False
        for mractid $ \ undoID -> do

            -- Find the undone activity in our DB
            undoneDB <- do
                a <- getActivity undone
                fromMaybeE a "Can't find undone in DB"

            (sieve, acceptAudience) <- do
                maybeUndo <- do
                    let followers = actorFollowers recipDeckActor
                    lift $ runMaybeT $
                        Left <$> tryUnfollow recipDeckID followers undoneDB <|>
                        Right <$> tryUnresolve recipDeckID undoneDB
                undo <- fromMaybeE maybeUndo "Undone activity isn't a Follow or Resolve related to me"
                (audSenderOnly, audSenderAndFollowers) <- do
                    ra <- lift $ getJust $ remoteAuthorId author
                    let ObjURI hAuthor luAuthor = remoteAuthorURI author
                    return
                        ( AudRemote hAuthor [luAuthor] []
                        , AudRemote hAuthor
                            [luAuthor]
                            (maybeToList $ remoteActorFollowers ra)
                        )
                case undo of
                    Left (remoteFollowID, followerID) -> do
                        unless (followerID == remoteAuthorId author) $
                            throwE "Trying to undo someone else's Follow"
                        lift $ delete remoteFollowID
                        return
                            ( makeRecipientSet [] []
                            , [audSenderOnly]
                            )
                    Right (deleteFromDB, taskID) -> do

                        -- Verify the sender is authorized by the deck to unresolve a ticket
                        capability <- do
                            cap <-
                                fromMaybeE
                                    maybeCapability
                                    "Asking to unresolve ticket but no capability provided"
                            case cap of
                                Left c -> pure c
                                Right _ -> throwE "Capability is a remote URI, i.e. not authored by me"
                        verifyCapability
                            capability
                            (Right $ remoteAuthorId author)
                            (GrantResourceDeck recipDeckID)

                        lift deleteFromDB

                        taskHash <- encodeKeyHashid taskID
                        return
                            ( makeRecipientSet
                                [LocalActorDeck recipDeckHash]
                                [ LocalStageDeckFollowers recipDeckHash
                                , LocalStageTicketFollowers recipDeckHash taskHash
                                ]
                            , [ AudLocal
                                    []
                                    [ LocalStageDeckFollowers recipDeckHash
                                    , LocalStageTicketFollowers recipDeckHash taskHash
                                    ]
                              , audSenderAndFollowers
                              ]
                            )

            -- Forward the Undo activity to relevant local stages, and
            -- schedule delivery for unavailable remote members of them
            maybeHttpFwdUndo <- lift $ for mfwd $ \ (localRecips, sig) ->
                forwardActivityDB
                    (actbBL body) localRecips sig recipDeckActorID
                    (LocalActorDeck recipDeckHash) sieve undoID


            -- Prepare an Accept activity and insert to deck's outbox
            acceptID <- lift $ insertEmptyOutboxItem (actorOutbox recipDeckActor) now
            (actionAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
                lift . lift $ prepareAccept acceptAudience
            _luAccept <- lift $ updateOutboxItem (LocalActorDeck recipDeckID) acceptID actionAccept

            -- Deliver the Accept to local recipients, and schedule delivery
            -- for unavailable remote recipients
            deliverHttpAccept <-
                deliverActivityDB
                    (LocalActorDeck recipDeckHash) recipDeckActorID
                    localRecipsAccept remoteRecipsAccept fwdHostsAccept
                    acceptID actionAccept

            -- Return instructions for HTTP inbox-forwarding of the Undo
            -- activity, and for HTTP delivery of the Accept activity to
            -- remote recipients
            return (maybeHttpFwdUndo, deliverHttpAccept)

    -- Launch asynchronous HTTP forwarding of the Undo activity and HTTP
    -- delivery of the Accept activity
    case maybeHttp of
        Nothing -> return "I already have this activity in my inbox, doing nothing"
        Just (maybeHttpFwdUndo, deliverHttpAccept) -> do
            forkWorker "deckUndoF Accept HTTP delivery" deliverHttpAccept
            case maybeHttpFwdUndo of
                Nothing -> return "Undid, no inbox-forwarding to do"
                Just forwardHttpUndo -> do
                    forkWorker "deckUndoF inbox-forwarding" forwardHttpUndo
                    return "Undid and ran inbox-forwarding of the Undo"

    where

    tryUnfollow _      _               (Left _)                 = mzero
    tryUnfollow deckID deckFollowersID (Right remoteActivityID) = do
        Entity remoteFollowID remoteFollow <-
            MaybeT $ getBy $ UniqueRemoteFollowFollow remoteActivityID
        let followerID = remoteFollowActor remoteFollow
            followerSetID = remoteFollowTarget remoteFollow
        if followerSetID == deckFollowersID
            then pure ()
            else do
                ticketID <-
                    MaybeT $ getKeyBy $ UniqueTicketFollowers followerSetID
                TicketDeck _ d <-
                    MaybeT $ getValBy $ UniqueTicketDeck ticketID
                guard $ d == deckID
        return (remoteFollowID, followerID)

    tryUnresolve deckID undone = do
        (deleteFromDB, ticketID) <- findTicket undone
        Entity taskID (TicketDeck _ d) <-
            MaybeT $ getBy $ UniqueTicketDeck ticketID
        guard $ d == deckID
        return (deleteFromDB, taskID)
        where
        findTicket (Left (_actorByKey, _actorEntity, itemID)) = do
            Entity resolveLocalID resolveLocal <-
                MaybeT $ getBy $ UniqueTicketResolveLocalActivity itemID
            let resolveID = ticketResolveLocalTicket resolveLocal
            resolve <- lift $ getJust resolveID
            let ticketID = ticketResolveTicket resolve
            return
                ( delete resolveLocalID >> delete resolveID
                , ticketID
                )
        findTicket (Right remoteActivityID) = do
            Entity resolveRemoteID resolveRemote <-
                MaybeT $ getBy $
                    UniqueTicketResolveRemoteActivity remoteActivityID
            let resolveID = ticketResolveRemoteTicket resolveRemote
            resolve <- lift $ getJust resolveID
            let ticketID = ticketResolveTicket resolve
            return
                ( delete resolveRemoteID >> delete resolveID
                , ticketID
                )

    prepareAccept audience = do
        encodeRouteHome <- getEncodeRouteHome

        let ObjURI hAuthor _ = remoteAuthorURI author

            (recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
                collectAudience audience

            recips = map encodeRouteHome audLocal ++ audRemote
            action = AP.Action
                { AP.actionCapability = Nothing
                , AP.actionSummary    = Nothing
                , AP.actionAudience   = AP.Audience recips [] [] [] [] []
                , AP.actionFulfills   = []
                , AP.actionSpecific   = AP.AcceptActivity AP.Accept
                    { AP.acceptObject   = ObjURI hAuthor luUndo
                    , AP.acceptResult   = Nothing
                    }
                }

        return (action, recipientSet, remoteActors, fwdHosts)

loomUndoF
    :: UTCTime
    -> KeyHashid Loom
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Undo URIMode
    -> ExceptT Text Handler Text
loomUndoF now recipLoomHash author body mfwd luUndo (AP.Undo uObject) = do

    -- Check input
    recipLoomID <- decodeKeyHashid404 recipLoomHash
    undone <-
        first (\ (actor, _, item) -> (actor, item)) <$>
            parseActivityURI uObject

    -- Verify the capability URI, if provided, is one of:
    --   * Outbox item URI of a local actor, i.e. a local activity
    --   * A remote URI
    maybeCapability <-
        for (AP.activityCapability $ actbActivity body) $ \ uCap ->
            nameExceptT "Undo capability" $
                first (\ (actor, _, item) -> (actor, item)) <$>
                    parseActivityURI uCap

    maybeHttp <- runDBExcept $ do

        -- Find recipient loom in DB, returning 404 if doesn't exist because we're
        -- in the loom's inbox post handler
        (recipLoomActorID, recipLoomActor) <- lift $ do
            loom <- get404 recipLoomID
            let actorID = loomActor loom
            (actorID,) <$> getJust actorID

        -- Insert the Undo to loom's inbox
        mractid <- lift $ insertToInbox now author body (actorInbox recipLoomActor) luUndo False
        for mractid $ \ undoID -> do

            -- Find the undone activity in our DB
            undoneDB <- do
                a <- getActivity undone
                fromMaybeE a "Can't find undone in DB"

            (sieve, acceptAudience) <- do
                maybeUndo <- do
                    let followers = actorFollowers recipLoomActor
                    lift $ runMaybeT $
                        Left <$> tryUnfollow recipLoomID followers undoneDB <|>
                        Right <$> tryUnresolve recipLoomID undoneDB
                undo <- fromMaybeE maybeUndo "Undone activity isn't a Follow or Resolve related to me"
                (audSenderOnly, audSenderAndFollowers) <- do
                    ra <- lift $ getJust $ remoteAuthorId author
                    let ObjURI hAuthor luAuthor = remoteAuthorURI author
                    return
                        ( AudRemote hAuthor [luAuthor] []
                        , AudRemote hAuthor
                            [luAuthor]
                            (maybeToList $ remoteActorFollowers ra)
                        )
                case undo of
                    Left (remoteFollowID, followerID) -> do
                        unless (followerID == remoteAuthorId author) $
                            throwE "Trying to undo someone else's Follow"
                        lift $ delete remoteFollowID
                        return
                            ( makeRecipientSet [] []
                            , [audSenderOnly]
                            )
                    Right (deleteFromDB, clothID) -> do

                        -- Verify the sender is authorized by the loom to unresolve a MR
                        capability <- do
                            cap <-
                                fromMaybeE
                                    maybeCapability
                                    "Asking to unresolve MR but no capability provided"
                            case cap of
                                Left c -> pure c
                                Right _ -> throwE "Capability is a remote URI, i.e. not authored by me"
                        verifyCapability
                            capability
                            (Right $ remoteAuthorId author)
                            (GrantResourceLoom recipLoomID)

                        lift deleteFromDB

                        clothHash <- encodeKeyHashid clothID
                        return
                            ( makeRecipientSet
                                [LocalActorLoom recipLoomHash]
                                [ LocalStageLoomFollowers recipLoomHash
                                , LocalStageClothFollowers recipLoomHash clothHash
                                ]
                            , [ AudLocal
                                    []
                                    [ LocalStageLoomFollowers recipLoomHash
                                    , LocalStageClothFollowers recipLoomHash clothHash
                                    ]
                              , audSenderAndFollowers
                              ]
                            )

            -- Forward the Undo activity to relevant local stages, and
            -- schedule delivery for unavailable remote members of them
            maybeHttpFwdUndo <- lift $ for mfwd $ \ (localRecips, sig) ->
                forwardActivityDB
                    (actbBL body) localRecips sig recipLoomActorID
                    (LocalActorLoom recipLoomHash) sieve undoID


            -- Prepare an Accept activity and insert to loom's outbox
            acceptID <- lift $ insertEmptyOutboxItem (actorOutbox recipLoomActor) now
            (actionAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
                lift . lift $ prepareAccept acceptAudience
            _luAccept <- lift $ updateOutboxItem (LocalActorLoom recipLoomID) acceptID actionAccept

            -- Deliver the Accept to local recipients, and schedule delivery
            -- for unavailable remote recipients
            deliverHttpAccept <-
                deliverActivityDB
                    (LocalActorLoom recipLoomHash) recipLoomActorID
                    localRecipsAccept remoteRecipsAccept fwdHostsAccept
                    acceptID actionAccept

            -- Return instructions for HTTP inbox-forwarding of the Undo
            -- activity, and for HTTP delivery of the Accept activity to
            -- remote recipients
            return (maybeHttpFwdUndo, deliverHttpAccept)

    -- Launch asynchronous HTTP forwarding of the Undo activity and HTTP
    -- delivery of the Accept activity
    case maybeHttp of
        Nothing -> return "I already have this activity in my inbox, doing nothing"
        Just (maybeHttpFwdUndo, deliverHttpAccept) -> do
            forkWorker "loomUndoF Accept HTTP delivery" deliverHttpAccept
            case maybeHttpFwdUndo of
                Nothing -> return "Undid, no inbox-forwarding to do"
                Just forwardHttpUndo -> do
                    forkWorker "loomUndoF inbox-forwarding" forwardHttpUndo
                    return "Undid and ran inbox-forwarding of the Undo"

    where

    tryUnfollow _      _               (Left _)                 = mzero
    tryUnfollow loomID loomFollowersID (Right remoteActivityID) = do
        Entity remoteFollowID remoteFollow <-
            MaybeT $ getBy $ UniqueRemoteFollowFollow remoteActivityID
        let followerID = remoteFollowActor remoteFollow
            followerSetID = remoteFollowTarget remoteFollow
        if followerSetID == loomFollowersID
            then pure ()
            else do
                ticketID <-
                    MaybeT $ getKeyBy $ UniqueTicketFollowers followerSetID
                TicketLoom _ l _ <-
                    MaybeT $ getValBy $ UniqueTicketLoom ticketID
                guard $ l == loomID
        return (remoteFollowID, followerID)

    tryUnresolve loomID undone = do
        (deleteFromDB, ticketID) <- findTicket undone
        Entity clothID (TicketLoom _ l _) <-
            MaybeT $ getBy $ UniqueTicketLoom ticketID
        guard $ l == loomID
        return (deleteFromDB, clothID)
        where
        findTicket (Left (_actorByKey, _actorEntity, itemID)) = do
            Entity resolveLocalID resolveLocal <-
                MaybeT $ getBy $ UniqueTicketResolveLocalActivity itemID
            let resolveID = ticketResolveLocalTicket resolveLocal
            resolve <- lift $ getJust resolveID
            let ticketID = ticketResolveTicket resolve
            return
                ( delete resolveLocalID >> delete resolveID
                , ticketID
                )
        findTicket (Right remoteActivityID) = do
            Entity resolveRemoteID resolveRemote <-
                MaybeT $ getBy $
                    UniqueTicketResolveRemoteActivity remoteActivityID
            let resolveID = ticketResolveRemoteTicket resolveRemote
            resolve <- lift $ getJust resolveID
            let ticketID = ticketResolveTicket resolve
            return
                ( delete resolveRemoteID >> delete resolveID
                , ticketID
                )

    prepareAccept audience = do
        encodeRouteHome <- getEncodeRouteHome

        let ObjURI hAuthor _ = remoteAuthorURI author

            (recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
                collectAudience audience

            recips = map encodeRouteHome audLocal ++ audRemote
            action = AP.Action
                { AP.actionCapability = Nothing
                , AP.actionSummary    = Nothing
                , AP.actionAudience   = AP.Audience recips [] [] [] [] []
                , AP.actionFulfills   = []
                , AP.actionSpecific   = AP.AcceptActivity AP.Accept
                    { AP.acceptObject   = ObjURI hAuthor luUndo
                    , AP.acceptResult   = Nothing
                    }
                }

        return (action, recipientSet, remoteActors, fwdHosts)

repoUndoF
    :: UTCTime
    -> KeyHashid Repo
    -> RemoteAuthor
    -> ActivityBody
    -> Maybe (RecipientRoutes, ByteString)
    -> LocalURI
    -> AP.Undo URIMode
    -> ExceptT Text Handler Text
repoUndoF now recipRepoHash author body mfwd luUndo (AP.Undo uObject) = do

    -- Check input
    recipRepoID <- decodeKeyHashid404 recipRepoHash
    undone <-
        first (\ (actor, _, item) -> (actor, item)) <$>
            parseActivityURI uObject

    -- Verify the capability URI, if provided, is one of:
    --   * Outbox item URI of a local actor, i.e. a local activity
    --   * A remote URI
    maybeCapability <-
        for (AP.activityCapability $ actbActivity body) $ \ uCap ->
            nameExceptT "Undo capability" $
                first (\ (actor, _, item) -> (actor, item)) <$>
                    parseActivityURI uCap

    maybeHttp <- runDBExcept $ do

        -- Find recipient repo in DB, returning 404 if doesn't exist because we're
        -- in the repo's inbox post handler
        (recipRepoActorID, recipRepoActor) <- lift $ do
            repo <- get404 recipRepoID
            let actorID = repoActor repo
            (actorID,) <$> getJust actorID

        -- Insert the Undo to repo's inbox
        mractid <- lift $ insertToInbox now author body (actorInbox recipRepoActor) luUndo False
        for mractid $ \ undoID -> do

            -- Find the undone activity in our DB
            undoneDB <- do
                a <- getActivity undone
                fromMaybeE a "Can't find undone in DB"

            (sieve, acceptAudience) <- do
                (remoteFollowID, followerID) <- do
                    maybeUndo <- do
                        let followers = actorFollowers recipRepoActor
                        lift $ runMaybeT $ tryUnfollow followers undoneDB
                    fromMaybeE maybeUndo "Undone activity isn't a Follow related to me"
                (audSenderOnly, _audSenderAndFollowers) <- do
                    ra <- lift $ getJust $ remoteAuthorId author
                    let ObjURI hAuthor luAuthor = remoteAuthorURI author
                    return
                        ( AudRemote hAuthor [luAuthor] []
                        , AudRemote hAuthor
                            [luAuthor]
                            (maybeToList $ remoteActorFollowers ra)
                        )
                unless (followerID == remoteAuthorId author) $
                    throwE "Trying to undo someone else's Follow"
                lift $ delete remoteFollowID
                return
                    ( makeRecipientSet [] []
                    , [audSenderOnly]
                    )

            -- Forward the Undo activity to relevant local stages, and
            -- schedule delivery for unavailable remote members of them
            maybeHttpFwdUndo <- lift $ for mfwd $ \ (localRecips, sig) ->
                forwardActivityDB
                    (actbBL body) localRecips sig recipRepoActorID
                    (LocalActorRepo recipRepoHash) sieve undoID


            -- Prepare an Accept activity and insert to repo's outbox
            acceptID <- lift $ insertEmptyOutboxItem (actorOutbox recipRepoActor) now
            (actionAccept, localRecipsAccept, remoteRecipsAccept, fwdHostsAccept) <-
                lift . lift $ prepareAccept acceptAudience
            _luAccept <- lift $ updateOutboxItem (LocalActorRepo recipRepoID) acceptID actionAccept

            -- Deliver the Accept to local recipients, and schedule delivery
            -- for unavailable remote recipients
            deliverHttpAccept <-
                deliverActivityDB
                    (LocalActorRepo recipRepoHash) recipRepoActorID
                    localRecipsAccept remoteRecipsAccept fwdHostsAccept
                    acceptID actionAccept

            -- Return instructions for HTTP inbox-forwarding of the Undo
            -- activity, and for HTTP delivery of the Accept activity to
            -- remote recipients
            return (maybeHttpFwdUndo, deliverHttpAccept)

    -- Launch asynchronous HTTP forwarding of the Undo activity and HTTP
    -- delivery of the Accept activity
    case maybeHttp of
        Nothing -> return "I already have this activity in my inbox, doing nothing"
        Just (maybeHttpFwdUndo, deliverHttpAccept) -> do
            forkWorker "repoUndoF Accept HTTP delivery" deliverHttpAccept
            case maybeHttpFwdUndo of
                Nothing -> return "Undid, no inbox-forwarding to do"
                Just forwardHttpUndo -> do
                    forkWorker "repoUndoF inbox-forwarding" forwardHttpUndo
                    return "Undid and ran inbox-forwarding of the Undo"

    where

    tryUnfollow _               (Left _)                 = mzero
    tryUnfollow repoFollowersID (Right remoteActivityID) = do
        Entity remoteFollowID remoteFollow <-
            MaybeT $ getBy $ UniqueRemoteFollowFollow remoteActivityID
        let followerID = remoteFollowActor remoteFollow
            followerSetID = remoteFollowTarget remoteFollow
        guard $ followerSetID == repoFollowersID
        return (remoteFollowID, followerID)

    prepareAccept audience = do
        encodeRouteHome <- getEncodeRouteHome

        let ObjURI hAuthor _ = remoteAuthorURI author

            (recipientSet, remoteActors, fwdHosts, audLocal, audRemote) =
                collectAudience audience

            recips = map encodeRouteHome audLocal ++ audRemote
            action = AP.Action
                { AP.actionCapability = Nothing
                , AP.actionSummary    = Nothing
                , AP.actionAudience   = AP.Audience recips [] [] [] [] []
                , AP.actionFulfills   = []
                , AP.actionSpecific   = AP.AcceptActivity AP.Accept
                    { AP.acceptObject   = ObjURI hAuthor luUndo
                    , AP.acceptResult   = Nothing
                    }
                }

        return (action, recipientSet, remoteActors, fwdHosts)
[See repo JSON]