1+ use :: entity:: content_delete_model as ContentDeleteModel ;
12use :: entity:: content_follow_model as ContentFollowModel ;
23use :: entity:: content_model as ContentModel ;
34use :: entity:: event_model as EventModel ;
@@ -34,17 +35,14 @@ impl Query {
3435 }
3536
3637 /// Return the list of identities that `caller` has followed (as
37- /// recorded by Follow events in the GRAPH collection).
38- ///
39- /// Unfollow (Delete) tombstones are not yet applied server-side, so a
40- /// previously-unfollowed identity still appears here .
38+ /// recorded by Follow events in the GRAPH collection), excluding any
39+ /// Follow event whose EventKey has been tombstoned by a Delete event.
40+ /// Follow → Unfollow → Follow-again resolves to "following" because the
41+ /// re-follow event has a fresh sequence and no Delete points at it .
4142 pub async fn list_followed_identities (
4243 db : & DbConn ,
4344 caller : & str ,
4445 ) -> Result < Vec < String > , DbErr > {
45- // Deduplicate because the same Follow content (by digest) may be
46- // referenced by multiple events — e.g. follow → unfollow → follow
47- // again all share one content row but produce distinct events.
4846 let rows = EventModel :: Entity :: find ( )
4947 . select_only ( )
5048 . column ( ContentFollowModel :: Column :: IdentityId )
@@ -57,8 +55,11 @@ impl Query {
5755 . to ( ContentFollowModel :: Column :: ContentId )
5856 . into ( ) ,
5957 )
58+ . join ( JoinType :: LeftJoin , delete_tombstone_join ( ) )
6059 . filter ( EventModel :: Column :: Collection . eq ( GRAPH_COLLECTION ) )
6160 . filter ( EventModel :: Column :: Identity . eq ( caller) )
61+ // ie. keep events where there is no deleted event (tombstone)
62+ . filter ( ContentDeleteModel :: Column :: ContentId . is_null ( ) )
6263 . into_tuple :: < String > ( )
6364 . all ( db)
6465 . await ?;
@@ -280,6 +281,56 @@ pub(crate) fn content_join() -> RelationDef {
280281 . into ( )
281282}
282283
284+ /// Relation joining an event to any `content_delete` row whose stored
285+ /// EventKey matches this event. Use as a LEFT JOIN so that a NULL partner
286+ /// means "not tombstoned".
287+ fn delete_tombstone_join ( ) -> RelationDef {
288+ EventModel :: Entity :: belongs_to ( ContentDeleteModel :: Entity )
289+ . from ( EventModel :: Column :: Collection )
290+ . to ( ContentDeleteModel :: Column :: EventKeyCollection )
291+ . on_condition ( |event_tbl, delete_tbl| {
292+ Condition :: all ( )
293+ . add (
294+ Expr :: col ( (
295+ event_tbl. clone ( ) ,
296+ EventModel :: Column :: Identity ,
297+ ) )
298+ . equals ( (
299+ delete_tbl. clone ( ) ,
300+ ContentDeleteModel :: Column :: EventKeyIdentity ,
301+ ) ) ,
302+ )
303+ . add (
304+ Expr :: col ( (
305+ event_tbl. clone ( ) ,
306+ EventModel :: Column :: PublicKeyType ,
307+ ) )
308+ . equals ( (
309+ delete_tbl. clone ( ) ,
310+ ContentDeleteModel :: Column :: EventKeyPublicKeyType ,
311+ ) ) ,
312+ )
313+ . add (
314+ Expr :: col ( (
315+ event_tbl. clone ( ) ,
316+ EventModel :: Column :: PublicKey ,
317+ ) )
318+ . equals ( (
319+ delete_tbl. clone ( ) ,
320+ ContentDeleteModel :: Column :: EventKeyPublicKey ,
321+ ) ) ,
322+ )
323+ . add (
324+ Expr :: col ( ( event_tbl, EventModel :: Column :: Sequence ) )
325+ . equals ( (
326+ delete_tbl,
327+ ContentDeleteModel :: Column :: EventKeySequence ,
328+ ) ) ,
329+ )
330+ } )
331+ . into ( )
332+ }
333+
283334#[ derive( Debug , FromQueryResult ) ]
284335pub struct AncestorRef {
285336 pub event_id : i64 ,
0 commit comments