1use serde::{Deserialize, Serialize};
13
14use super::{Channel, ChannelId, ChannelKey, Community, CommunityId, Epoch, ServerRootKey};
15use crate::stored_event::event_kind;
16use nostr_sdk::prelude::{EventBuilder, Kind, PublicKey, UnsignedEvent};
17
18fn is_zero_u64(n: &u64) -> bool {
21 *n == 0
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct InviteChannel {
27 pub id: String,
28 pub key: String,
29 #[serde(default, skip_serializing_if = "is_zero_u64")]
30 pub epoch: u64,
31 pub name: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct CommunityInvite {
37 pub community_id: String,
38 pub name: String,
39 pub server_root_key: String,
40 #[serde(default, skip_serializing_if = "is_zero_u64")]
43 pub server_root_epoch: u64,
44 pub relays: Vec<String>,
45 pub channels: Vec<InviteChannel>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub owner_attestation: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub icon: Option<super::CommunityImage>,
56}
57
58const MAX_INVITE_CHANNELS: usize = 256;
63
64impl CommunityInvite {
65 pub fn to_json(&self) -> Result<String, String> {
66 serde_json::to_string(self).map_err(|e| e.to_string())
67 }
68 pub fn from_json(json: &str) -> Result<Self, String> {
69 let mut inv: Self = serde_json::from_str(json).map_err(|e| e.to_string())?;
70 inv.relays = super::cap_relays(inv.relays);
73 Ok(inv)
74 }
75
76 pub fn validate(&self) -> Result<(), String> {
80 if self.channels.len() > MAX_INVITE_CHANNELS {
81 return Err(format!("invite declares too many channels ({})", self.channels.len()));
82 }
83 Ok(())
84 }
85}
86
87fn hex32(hex: &str) -> Result<[u8; 32], String> {
90 crate::simd::hex::hex_to_bytes_32_checked(hex)
91 .ok_or_else(|| format!("invalid or wrong-length 64-char hex ({} chars)", hex.len()))
92}
93
94pub fn build_invite(community: &Community) -> CommunityInvite {
99 CommunityInvite {
100 community_id: community.id.to_hex(),
101 name: community.name.clone(),
102 server_root_key: crate::simd::hex::bytes_to_hex_32(community.server_root_key.as_bytes()),
103 server_root_epoch: community.server_root_epoch.0,
104 relays: super::cap_relays(community.relays.clone()),
105 channels: community
106 .channels
107 .iter()
108 .map(|c| InviteChannel {
109 id: c.id.to_hex(),
110 key: crate::simd::hex::bytes_to_hex_32(c.key.as_bytes()),
111 epoch: c.epoch.0,
112 name: c.name.clone(),
113 })
114 .collect(),
115 owner_attestation: community.owner_attestation.clone(),
116 icon: community.icon.clone(),
117 }
118}
119
120pub fn accept_invite(invite: &CommunityInvite) -> Result<Community, String> {
124 invite.validate()?;
125 let id = CommunityId(hex32(&invite.community_id)?);
126 let server_root_key = ServerRootKey(hex32(&invite.server_root_key)?);
127
128 let owner_attestation = invite.owner_attestation.as_ref().and_then(|att| {
131 super::owner::verify_owner_attestation(att, &invite.community_id)
132 .map(|_| att.clone())
133 });
134
135 let mut channels = Vec::with_capacity(invite.channels.len());
136 for ic in &invite.channels {
137 channels.push(Channel {
138 id: ChannelId(hex32(&ic.id)?),
139 key: ChannelKey(hex32(&ic.key)?),
140 epoch: Epoch(ic.epoch),
141 name: ic.name.clone(),
142 banned: Vec::new(),
144 protected: Vec::new(), roster: Default::default(),
145 epoch_keys: Vec::new(),
147 dissolved: false,
148 });
149 }
150
151 Ok(Community {
152 id,
153 server_root_key,
154 server_root_epoch: Epoch(invite.server_root_epoch),
155 name: invite.name.clone(),
156 description: None,
159 icon: invite.icon.clone(),
160 banner: None,
161 relays: super::cap_relays(invite.relays.clone()),
162 channels,
163 owner_attestation,
164 dissolved: false,
166 })
167}
168
169pub fn build_invite_rumor(community: &Community, my_pubkey: PublicKey) -> Result<UnsignedEvent, String> {
176 let json = build_invite(community).to_json()?;
177 Ok(EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE), json).build(my_pubkey))
178}
179
180pub fn parse_invite_rumor(kind: Kind, content: &str) -> Option<CommunityInvite> {
184 if kind != Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE) {
185 return None;
186 }
187 CommunityInvite::from_json(content).ok()
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::community::envelope::{open_message, seal_message};
194 use nostr_sdk::prelude::Keys;
195
196 #[test]
197 fn relay_cap_truncates_dedups_and_holds_at_every_boundary() {
198 let many: Vec<String> = vec![
200 "wss://a".into(), "wss://b".into(), "wss://a".into(), "wss://c".into(),
201 "wss://d".into(), "wss://e".into(), "wss://f".into(),
202 ];
203 let capped = super::super::cap_relays(many.clone());
204 assert_eq!(capped, vec!["wss://a", "wss://b", "wss://c", "wss://d", "wss://e"]);
205
206 let owner = Community::create("HQ", "general", many.clone());
208 assert!(owner.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
209
210 let mut bundle = build_invite(&owner);
213 bundle.relays = many; let json = bundle.to_json().unwrap();
215 let parsed = CommunityInvite::from_json(&json).unwrap();
216 assert!(parsed.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
217 let member = accept_invite(&parsed).unwrap();
218 assert!(member.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
219 }
220
221 #[test]
222 fn invite_round_trips_to_member_view() {
223 let owner = Community::create("HQ", "general", vec!["wss://r1".into(), "wss://r2".into()]);
224 let json = build_invite(&owner).to_json().unwrap();
225 let invite = CommunityInvite::from_json(&json).unwrap();
226 let member = accept_invite(&invite).unwrap();
227
228 assert_eq!(member.id, owner.id);
230 assert_eq!(member.name, "HQ");
231 assert_eq!(member.relays, owner.relays);
232 assert_eq!(member.server_root_key.as_bytes(), owner.server_root_key.as_bytes());
233 assert_eq!(member.channels.len(), 1);
234 assert_eq!(member.channels[0].id, owner.channels[0].id);
235 assert_eq!(member.channels[0].key.as_bytes(), owner.channels[0].key.as_bytes());
236 assert_eq!(member.channels[0].name, "general");
237 }
238
239 #[test]
240 fn invited_member_can_read_owner_messages() {
241 let owner = Community::create("HQ", "general", vec![]);
245 let owner_author = Keys::generate();
246 let chan = &owner.channels[0];
247 let sealed =
248 seal_message(&owner_author, &chan.key, &chan.id, chan.epoch, "welcome!", 1).unwrap();
249
250 let invite = CommunityInvite::from_json(&build_invite(&owner).to_json().unwrap()).unwrap();
251 let member = accept_invite(&invite).unwrap();
252 let mc = &member.channels[0];
253 let opened = open_message(&sealed, &mc.key, &mc.id, mc.epoch).unwrap();
254 assert_eq!(opened.content, "welcome!");
255 assert_eq!(opened.author, owner_author.public_key());
256 }
257
258 #[test]
259 fn malformed_invite_errors() {
260 let owner = Community::create("HQ", "general", vec![]);
261 let mut invite = build_invite(&owner);
262 invite.server_root_key = "zz".into(); assert!(accept_invite(&invite).is_err());
264 }
265
266 #[test]
267 fn accept_rejects_a_bundle_exceeding_the_caps() {
268 let owner = Community::create("HQ", "general", vec![]);
272 let mut over_relays = build_invite(&owner);
273 over_relays.relays = (0..100).map(|i| format!("wss://r{i}")).collect();
274 let member = accept_invite(&over_relays).expect("over-relay bundle truncates, not rejects");
275 assert!(member.relays.len() <= super::super::MAX_COMMUNITY_RELAYS, "relays truncated to cap");
276
277 let mut over_channels = build_invite(&owner);
278 over_channels.channels = (0..500)
279 .map(|i| InviteChannel { id: "aa".repeat(32), key: "bb".repeat(32), epoch: 0, name: format!("c{i}") })
280 .collect();
281 assert!(accept_invite(&over_channels).is_err(), "too many channels → rejected");
282 }
283
284 #[test]
285 fn accept_rejects_malformed_community_id() {
286 let owner = Community::create("HQ", "general", vec![]);
288 let mut bad = build_invite(&owner);
289 bad.community_id = "not-64-hex".into();
290 assert!(accept_invite(&bad).is_err());
291 }
292
293 #[test]
294 fn accept_drops_a_bogus_owner_attestation_but_still_joins() {
295 let owner = Community::create("HQ", "general", vec![]);
298 let mut inv = build_invite(&owner);
299 inv.owner_attestation = Some("not even an event".to_string());
300 let member = accept_invite(&inv).unwrap();
301 assert!(member.owner_attestation.is_none(), "an unverifiable attestation is dropped, not trusted");
302 }
303
304 #[test]
305 fn accept_an_empty_channel_bundle_is_graceful() {
306 let owner = Community::create("HQ", "general", vec![]);
307 let mut inv = build_invite(&owner);
308 inv.channels.clear();
309 let member = accept_invite(&inv).unwrap();
310 assert!(member.channels.is_empty(), "a 0-channel bundle accepts without panic");
311 }
312
313 #[test]
314 fn bundle_carries_read_keys() {
315 let owner = Community::create("HQ", "general", vec![]);
317 let json = build_invite(&owner).to_json().unwrap();
318 assert!(json.contains(&crate::simd::hex::bytes_to_hex_32(owner.server_root_key.as_bytes())));
319 assert!(json.contains(&crate::simd::hex::bytes_to_hex_32(owner.channels[0].key.as_bytes())));
320 }
321
322 #[test]
323 fn invite_rumor_round_trips() {
324 let owner = Community::create("HQ", "general", vec!["wss://r1".into()]);
325 let author = Keys::generate();
326 let rumor = build_invite_rumor(&owner, author.public_key()).unwrap();
327
328 assert_eq!(rumor.kind, Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE));
329 let parsed = parse_invite_rumor(rumor.kind, &rumor.content).expect("parses");
330 let member = accept_invite(&parsed).unwrap();
331 assert_eq!(member.id, owner.id);
332 assert_eq!(member.channels[0].key.as_bytes(), owner.channels[0].key.as_bytes());
333 }
334
335 #[test]
336 fn parse_invite_rumor_rejects_wrong_kind() {
337 let owner = Community::create("HQ", "general", vec![]);
340 let json = build_invite(&owner).to_json().unwrap();
341 assert!(parse_invite_rumor(Kind::Custom(14), &json).is_none());
342 assert!(parse_invite_rumor(Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE), "not json").is_none());
344 }
345
346 #[tokio::test]
347 async fn full_carrier_chain_invitee_reads_owner_message() {
348 use crate::community::transport::memory::MemoryRelay;
352 use crate::community::send::{fetch_channel_messages, publish_message};
353
354 let owner = Community::create("HQ", "general", vec!["r1".into()]);
355 let author = Keys::generate();
356 let rumor = build_invite_rumor(&owner, author.public_key()).unwrap();
357
358 let invite = parse_invite_rumor(rumor.kind, &rumor.content).expect("invite");
360 let member = accept_invite(&invite).unwrap();
361
362 let relay = MemoryRelay::new();
364 let owner_author = Keys::generate();
365 publish_message(&relay, &owner, &owner.channels[0], &owner_author, "welcome aboard", 1)
366 .await
367 .unwrap();
368
369 let msgs = fetch_channel_messages(&relay, &member, &member.channels[0]).await.unwrap();
370 assert_eq!(msgs.len(), 1);
371 assert_eq!(msgs[0].content, "welcome aboard");
372 assert_eq!(msgs[0].author, owner_author.public_key());
373 }
374}