mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(relay): resubscribe agents when an archived channel is restored (#1187)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
d389bf0b85
commit
6780ea21e4
@@ -1165,6 +1165,39 @@ async fn handle_edit_metadata(event: &Event, state: &Arc<AppState>) -> anyhow::R
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Resubscribe connected agents after restore: archiving evicts their
|
||||
// live subscriptions (CLOSED "channel access revoked") and unarchive
|
||||
// otherwise emits no signal that makes a connected agent resubscribe.
|
||||
// We reuse the member_added notification (44100) purely as a resubscribe
|
||||
// trigger — no membership actually changed here — because it flows on the
|
||||
// agent's always-live global membership subscription, the same path
|
||||
// remove/re-add uses to recover. Humans self-heal via the re-emitted
|
||||
// kind:39000 discovery, so this is intentionally agent-scoped.
|
||||
//
|
||||
// Known limitation: emit_membership_notification builds a created_at=now
|
||||
// event with no nonce, and insert_event skips fan-out on a duplicate id.
|
||||
// Four sub-second toggles (archive->unarchive->archive->unarchive) on the
|
||||
// same channel by the same actor could collide ids and skip a fan-out.
|
||||
// Not reachable in practice — unarchive has a single human-driven caller;
|
||||
// the reaper only auto-archives — so we don't engineer around it.
|
||||
for member in state.db.get_members(channel_id).await? {
|
||||
if let Err(e) = emit_membership_notification(
|
||||
state,
|
||||
channel_id,
|
||||
&member.pubkey,
|
||||
&actor_bytes,
|
||||
KIND_MEMBER_ADDED_NOTIFICATION,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
channel = %channel_id,
|
||||
error = %e,
|
||||
"post-unarchive resubscribe notification failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {} // ignore invalid values
|
||||
}
|
||||
|
||||
@@ -846,6 +846,79 @@ async fn test_nip29_put_user_default_policy_allows() {
|
||||
ws.disconnect().await.expect("disconnect");
|
||||
}
|
||||
|
||||
/// Restoring an archived channel must re-signal connected members so their
|
||||
/// agents resubscribe. Archive evicts live channel subscriptions; unarchive
|
||||
/// emits a kind:44100 member_added notification per member on the always-live
|
||||
/// global membership feed, which is the resubscribe trigger remove/re-add uses.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_unarchive_emits_member_added_notification() {
|
||||
let url = relay_url();
|
||||
|
||||
let owner_keys = Keys::generate();
|
||||
let owner_pubkey_hex = owner_keys.public_key().to_hex();
|
||||
|
||||
// Creating the channel makes the owner its sole member.
|
||||
let channel_id = create_test_channel(&owner_keys).await;
|
||||
|
||||
let mut ws = BuzzTestClient::connect(&url, &owner_keys)
|
||||
.await
|
||||
.expect("connect as owner");
|
||||
|
||||
// Subscribe to the global membership feed (kind:44100 addressed to the owner).
|
||||
// This is a global, non-channel-scoped subscription, so archive's
|
||||
// channel-scoped eviction leaves it intact across the archive→unarchive cycle.
|
||||
let sid = sub_id("membership-feed");
|
||||
let membership_filter = Filter::new().kind(Kind::Custom(44100)).custom_tags(
|
||||
SingleLetterTag::lowercase(Alphabet::P),
|
||||
[owner_pubkey_hex.as_str()],
|
||||
);
|
||||
ws.subscribe(&sid, vec![membership_filter])
|
||||
.await
|
||||
.expect("subscribe to membership feed");
|
||||
ws.collect_until_eose(&sid, Duration::from_secs(5))
|
||||
.await
|
||||
.expect("membership feed EOSE");
|
||||
|
||||
// Archive, then unarchive, the channel via kind:9002 edit-metadata.
|
||||
for archived in ["true", "false"] {
|
||||
let event = EventBuilder::new(Kind::Custom(9002), "")
|
||||
.tags([
|
||||
Tag::parse(["h", &channel_id]).unwrap(),
|
||||
Tag::parse(["archived", archived]).unwrap(),
|
||||
])
|
||||
.sign_with_keys(&owner_keys)
|
||||
.unwrap();
|
||||
let ok = ws.send_event(event).await.expect("send kind 9002");
|
||||
assert!(ok.accepted, "edit-metadata rejected: {}", ok.message);
|
||||
}
|
||||
|
||||
// The unarchive must fan out a 44100 to the owner. Loop past any other
|
||||
// events delivered on the connection until we see it (or time out).
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let remaining = deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.filter(|d| !d.is_zero())
|
||||
.expect("timed out waiting for member_added notification");
|
||||
if let RelayMessage::Event { event, .. } = ws
|
||||
.recv_event(remaining)
|
||||
.await
|
||||
.expect("recv membership notification")
|
||||
{
|
||||
if event.kind == Kind::Custom(44100) {
|
||||
let content: serde_json::Value =
|
||||
serde_json::from_str(&event.content).expect("parse notification content");
|
||||
assert_eq!(content["type"], "member_added");
|
||||
assert_eq!(content["channel_id"], channel_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.disconnect().await.expect("disconnect");
|
||||
}
|
||||
|
||||
/// NIP-29 kind 9000 (PUT_USER): "nobody" policy blocks a third party from adding the agent.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
|
||||
Reference in New Issue
Block a user