mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(relay): publish membership snapshot on provisioning (#1761)
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7
parent
bea507d8aa
commit
0950d392b7
@@ -260,6 +260,9 @@ mod tests {
|
||||
use tower::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use buzz_core::kind::KIND_NIP43_MEMBERSHIP_LIST;
|
||||
use buzz_db::event::EventQuery;
|
||||
|
||||
use crate::router::build_router;
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -324,6 +327,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|keys| keys.public_key().to_hex())
|
||||
.collect();
|
||||
config.require_relay_membership = true;
|
||||
|
||||
let pool = sqlx::PgPool::connect(TEST_DB_URL).await.ok()?;
|
||||
let db = buzz_db::Db::from_pool(pool.clone());
|
||||
@@ -512,6 +516,7 @@ mod tests {
|
||||
let body = serde_json::json!({
|
||||
"host": host,
|
||||
"initial_owner_pubkey": owner.public_key().to_hex(),
|
||||
"create_only": true,
|
||||
})
|
||||
.to_string();
|
||||
let url = format!("http://{INGRESS_HOST}/operator/communities");
|
||||
@@ -544,12 +549,37 @@ mod tests {
|
||||
.await
|
||||
.expect("lookup community")
|
||||
.expect("community exists");
|
||||
let owner_hex = owner.public_key().to_hex();
|
||||
let member = state
|
||||
.db
|
||||
.get_relay_member(community.id, &owner.public_key().to_hex())
|
||||
.get_relay_member(community.id, &owner_hex)
|
||||
.await
|
||||
.expect("lookup owner role")
|
||||
.expect("owner member exists");
|
||||
assert_eq!(member.role, "owner");
|
||||
|
||||
let membership_events = state
|
||||
.db
|
||||
.query_events(&EventQuery {
|
||||
kinds: Some(vec![KIND_NIP43_MEMBERSHIP_LIST as i32]),
|
||||
global_only: true,
|
||||
limit: Some(1),
|
||||
..EventQuery::for_community(community.id)
|
||||
})
|
||||
.await
|
||||
.expect("query membership snapshot");
|
||||
let snapshot = membership_events
|
||||
.first()
|
||||
.expect("membership snapshot published during provisioning");
|
||||
assert!(snapshot.event.tags.iter().any(|tag| {
|
||||
tag.as_slice()
|
||||
.first()
|
||||
.is_some_and(|value| value == "member")
|
||||
&& tag
|
||||
.as_slice()
|
||||
.get(1)
|
||||
.is_some_and(|value| value == &owner_hex)
|
||||
&& tag.as_slice().get(2).is_some_and(|value| value == "owner")
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use buzz_core::tenant::normalize_host;
|
||||
use buzz_core::tenant::{normalize_host, TenantContext};
|
||||
use url::{Host, Url};
|
||||
|
||||
use crate::state::AppState;
|
||||
@@ -201,6 +201,34 @@ pub(crate) fn normalize_candidate_host(host: &str) -> Result<String, String> {
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
/// Publish the event-backed membership view after owner persistence.
|
||||
///
|
||||
/// Provisioning is already committed when this runs. Publication therefore
|
||||
/// remains best-effort, matching every other membership mutation path: turning
|
||||
/// a stored success into an HTTP failure would make create-only retries report
|
||||
/// a misleading conflict while leaving clients without a repair path.
|
||||
async fn publish_membership_snapshot_if_required(
|
||||
state: &Arc<AppState>,
|
||||
community: buzz_core::CommunityId,
|
||||
host: &str,
|
||||
) {
|
||||
if !state.config.require_relay_membership {
|
||||
return;
|
||||
}
|
||||
|
||||
let tenant = TenantContext::resolved(community, host);
|
||||
if let Err(error) =
|
||||
crate::handlers::side_effects::publish_nip43_membership_list(&tenant, state).await
|
||||
{
|
||||
warn!(
|
||||
community = %community,
|
||||
host,
|
||||
error = %error,
|
||||
"community provisioned but NIP-43 membership snapshot publication failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate and execute a relay-operator community provisioning request.
|
||||
///
|
||||
/// The caller is an HTTP operator endpoint, not the Nostr event ingest path.
|
||||
@@ -267,6 +295,7 @@ pub async fn provision_community(
|
||||
owner = %owner_hex,
|
||||
"community created via operator endpoint"
|
||||
);
|
||||
publish_membership_snapshot_if_required(state, record.id, &record.host).await;
|
||||
return Ok(ProvisionCommunityResponse {
|
||||
community_id: record.id.to_string(),
|
||||
host: record.host,
|
||||
@@ -290,6 +319,7 @@ pub async fn provision_community(
|
||||
.bootstrap_owner(record.id, owner_hex)
|
||||
.await
|
||||
.map_err(|e| format!("community provisioned but owner bootstrap failed: {e}"))?;
|
||||
publish_membership_snapshot_if_required(state, record.id, &record.host).await;
|
||||
}
|
||||
|
||||
info!(
|
||||
|
||||
Reference in New Issue
Block a user