Skip to content

Commit 70461e0

Browse files
committed
(fix): following feed not taking into account tombstones
1 parent 592cf47 commit 70461e0

1 file changed

Lines changed: 58 additions & 7 deletions

File tree

services/server/src/service/feeds/feeds_repository.rs

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use ::entity::content_delete_model as ContentDeleteModel;
12
use ::entity::content_follow_model as ContentFollowModel;
23
use ::entity::content_model as ContentModel;
34
use ::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)]
284335
pub struct AncestorRef {
285336
pub event_id: i64,

0 commit comments

Comments
 (0)