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
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct InviteChannel {
21 pub id: String,
22 pub key: String,
23 pub epoch: u64,
24 pub name: String,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct CommunityInvite {
30 pub community_id: String,
31 pub name: String,
32 pub server_root_key: String,
33 #[serde(default)]
36 pub server_root_epoch: u64,
37 pub relays: Vec<String>,
38 pub channels: Vec<InviteChannel>,
39 #[serde(default)]
42 pub owner_attestation: Option<String>,
43}
44
45const MAX_INVITE_CHANNELS: usize = 256;
50
51impl CommunityInvite {
52 pub fn to_json(&self) -> Result<String, String> {
53 serde_json::to_string(self).map_err(|e| e.to_string())
54 }
55 pub fn from_json(json: &str) -> Result<Self, String> {
56 let mut inv: Self = serde_json::from_str(json).map_err(|e| e.to_string())?;
57 inv.relays = super::cap_relays(inv.relays);
60 Ok(inv)
61 }
62
63 pub fn validate(&self) -> Result<(), String> {
67 if self.channels.len() > MAX_INVITE_CHANNELS {
68 return Err(format!("invite declares too many channels ({})", self.channels.len()));
69 }
70 Ok(())
71 }
72}
73
74fn hex32(hex: &str) -> Result<[u8; 32], String> {
77 crate::simd::hex::hex_to_bytes_32_checked(hex)
78 .ok_or_else(|| format!("invalid or wrong-length 64-char hex ({} chars)", hex.len()))
79}
80
81pub fn build_invite(community: &Community) -> CommunityInvite {
86 CommunityInvite {
87 community_id: community.id.to_hex(),
88 name: community.name.clone(),
89 server_root_key: crate::simd::hex::bytes_to_hex_32(community.server_root_key.as_bytes()),
90 server_root_epoch: community.server_root_epoch.0,
91 relays: super::cap_relays(community.relays.clone()),
92 channels: community
93 .channels
94 .iter()
95 .map(|c| InviteChannel {
96 id: c.id.to_hex(),
97 key: crate::simd::hex::bytes_to_hex_32(c.key.as_bytes()),
98 epoch: c.epoch.0,
99 name: c.name.clone(),
100 })
101 .collect(),
102 owner_attestation: community.owner_attestation.clone(),
103 }
104}
105
106pub fn accept_invite(invite: &CommunityInvite) -> Result<Community, String> {
110 invite.validate()?;
111 let id = CommunityId(hex32(&invite.community_id)?);
112 let server_root_key = ServerRootKey(hex32(&invite.server_root_key)?);
113
114 let owner_attestation = invite.owner_attestation.as_ref().and_then(|att| {
117 super::owner::verify_owner_attestation(att, &invite.community_id)
118 .map(|_| att.clone())
119 });
120
121 let mut channels = Vec::with_capacity(invite.channels.len());
122 for ic in &invite.channels {
123 channels.push(Channel {
124 id: ChannelId(hex32(&ic.id)?),
125 key: ChannelKey(hex32(&ic.key)?),
126 epoch: Epoch(ic.epoch),
127 name: ic.name.clone(),
128 banned: Vec::new(),
130 protected: Vec::new(), roster: Default::default(),
131 epoch_keys: Vec::new(),
133 dissolved: false,
134 });
135 }
136
137 Ok(Community {
138 id,
139 server_root_key,
140 server_root_epoch: Epoch(invite.server_root_epoch),
141 name: invite.name.clone(),
142 description: None,
146 icon: None,
147 banner: None,
148 relays: super::cap_relays(invite.relays.clone()),
149 channels,
150 owner_attestation,
151 dissolved: false,
153 })
154}
155
156pub fn build_invite_rumor(community: &Community, my_pubkey: PublicKey) -> Result<UnsignedEvent, String> {
163 let json = build_invite(community).to_json()?;
164 Ok(EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE), json).build(my_pubkey))
165}
166
167pub fn parse_invite_rumor(kind: Kind, content: &str) -> Option<CommunityInvite> {
171 if kind != Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE) {
172 return None;
173 }
174 CommunityInvite::from_json(content).ok()
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::community::envelope::{open_message, seal_message};
181 use nostr_sdk::prelude::Keys;
182
183 #[test]
184 fn relay_cap_truncates_dedups_and_holds_at_every_boundary() {
185 let many: Vec<String> = vec![
187 "wss://a".into(), "wss://b".into(), "wss://a".into(), "wss://c".into(),
188 "wss://d".into(), "wss://e".into(), "wss://f".into(),
189 ];
190 let capped = super::super::cap_relays(many.clone());
191 assert_eq!(capped, vec!["wss://a", "wss://b", "wss://c", "wss://d", "wss://e"]);
192
193 let owner = Community::create("HQ", "general", many.clone());
195 assert!(owner.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
196
197 let mut bundle = build_invite(&owner);
200 bundle.relays = many; let json = bundle.to_json().unwrap();
202 let parsed = CommunityInvite::from_json(&json).unwrap();
203 assert!(parsed.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
204 let member = accept_invite(&parsed).unwrap();
205 assert!(member.relays.len() <= super::super::MAX_COMMUNITY_RELAYS);
206 }
207
208 #[test]
209 fn invite_round_trips_to_member_view() {
210 let owner = Community::create("HQ", "general", vec!["wss://r1".into(), "wss://r2".into()]);
211 let json = build_invite(&owner).to_json().unwrap();
212 let invite = CommunityInvite::from_json(&json).unwrap();
213 let member = accept_invite(&invite).unwrap();
214
215 assert_eq!(member.id, owner.id);
217 assert_eq!(member.name, "HQ");
218 assert_eq!(member.relays, owner.relays);
219 assert_eq!(member.server_root_key.as_bytes(), owner.server_root_key.as_bytes());
220 assert_eq!(member.channels.len(), 1);
221 assert_eq!(member.channels[0].id, owner.channels[0].id);
222 assert_eq!(member.channels[0].key.as_bytes(), owner.channels[0].key.as_bytes());
223 assert_eq!(member.channels[0].name, "general");
224 }
225
226 #[test]
227 fn invited_member_can_read_owner_messages() {
228 let owner = Community::create("HQ", "general", vec![]);
232 let owner_author = Keys::generate();
233 let chan = &owner.channels[0];
234 let sealed =
235 seal_message(&owner_author, &chan.key, &chan.id, chan.epoch, "welcome!", 1).unwrap();
236
237 let invite = CommunityInvite::from_json(&build_invite(&owner).to_json().unwrap()).unwrap();
238 let member = accept_invite(&invite).unwrap();
239 let mc = &member.channels[0];
240 let opened = open_message(&sealed, &mc.key, &mc.id, mc.epoch).unwrap();
241 assert_eq!(opened.content, "welcome!");
242 assert_eq!(opened.author, owner_author.public_key());
243 }
244
245 #[test]
246 fn malformed_invite_errors() {
247 let owner = Community::create("HQ", "general", vec![]);
248 let mut invite = build_invite(&owner);
249 invite.server_root_key = "zz".into(); assert!(accept_invite(&invite).is_err());
251 }
252
253 #[test]
254 fn accept_rejects_a_bundle_exceeding_the_caps() {
255 let owner = Community::create("HQ", "general", vec![]);
259 let mut over_relays = build_invite(&owner);
260 over_relays.relays = (0..100).map(|i| format!("wss://r{i}")).collect();
261 let member = accept_invite(&over_relays).expect("over-relay bundle truncates, not rejects");
262 assert!(member.relays.len() <= super::super::MAX_COMMUNITY_RELAYS, "relays truncated to cap");
263
264 let mut over_channels = build_invite(&owner);
265 over_channels.channels = (0..500)
266 .map(|i| InviteChannel { id: "aa".repeat(32), key: "bb".repeat(32), epoch: 0, name: format!("c{i}") })
267 .collect();
268 assert!(accept_invite(&over_channels).is_err(), "too many channels → rejected");
269 }
270
271 #[test]
272 fn accept_rejects_malformed_community_id() {
273 let owner = Community::create("HQ", "general", vec![]);
275 let mut bad = build_invite(&owner);
276 bad.community_id = "not-64-hex".into();
277 assert!(accept_invite(&bad).is_err());
278 }
279
280 #[test]
281 fn accept_drops_a_bogus_owner_attestation_but_still_joins() {
282 let owner = Community::create("HQ", "general", vec![]);
285 let mut inv = build_invite(&owner);
286 inv.owner_attestation = Some("not even an event".to_string());
287 let member = accept_invite(&inv).unwrap();
288 assert!(member.owner_attestation.is_none(), "an unverifiable attestation is dropped, not trusted");
289 }
290
291 #[test]
292 fn accept_an_empty_channel_bundle_is_graceful() {
293 let owner = Community::create("HQ", "general", vec![]);
294 let mut inv = build_invite(&owner);
295 inv.channels.clear();
296 let member = accept_invite(&inv).unwrap();
297 assert!(member.channels.is_empty(), "a 0-channel bundle accepts without panic");
298 }
299
300 #[test]
301 fn bundle_carries_read_keys() {
302 let owner = Community::create("HQ", "general", vec![]);
304 let json = build_invite(&owner).to_json().unwrap();
305 assert!(json.contains(&crate::simd::hex::bytes_to_hex_32(owner.server_root_key.as_bytes())));
306 assert!(json.contains(&crate::simd::hex::bytes_to_hex_32(owner.channels[0].key.as_bytes())));
307 }
308
309 #[test]
310 fn invite_rumor_round_trips() {
311 let owner = Community::create("HQ", "general", vec!["wss://r1".into()]);
312 let author = Keys::generate();
313 let rumor = build_invite_rumor(&owner, author.public_key()).unwrap();
314
315 assert_eq!(rumor.kind, Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE));
316 let parsed = parse_invite_rumor(rumor.kind, &rumor.content).expect("parses");
317 let member = accept_invite(&parsed).unwrap();
318 assert_eq!(member.id, owner.id);
319 assert_eq!(member.channels[0].key.as_bytes(), owner.channels[0].key.as_bytes());
320 }
321
322 #[test]
323 fn parse_invite_rumor_rejects_wrong_kind() {
324 let owner = Community::create("HQ", "general", vec![]);
327 let json = build_invite(&owner).to_json().unwrap();
328 assert!(parse_invite_rumor(Kind::Custom(14), &json).is_none());
329 assert!(parse_invite_rumor(Kind::Custom(event_kind::COMMUNITY_INVITE_BUNDLE), "not json").is_none());
331 }
332
333 #[tokio::test]
334 async fn full_carrier_chain_invitee_reads_owner_message() {
335 use crate::community::transport::memory::MemoryRelay;
339 use crate::community::send::{fetch_channel_messages, publish_message};
340
341 let owner = Community::create("HQ", "general", vec!["r1".into()]);
342 let author = Keys::generate();
343 let rumor = build_invite_rumor(&owner, author.public_key()).unwrap();
344
345 let invite = parse_invite_rumor(rumor.kind, &rumor.content).expect("invite");
347 let member = accept_invite(&invite).unwrap();
348
349 let relay = MemoryRelay::new();
351 let owner_author = Keys::generate();
352 publish_message(&relay, &owner, &owner.channels[0], &owner_author, "welcome aboard", 1)
353 .await
354 .unwrap();
355
356 let msgs = fetch_channel_messages(&relay, &member, &member.channels[0]).await.unwrap();
357 assert_eq!(msgs.len(), 1);
358 assert_eq!(msgs[0].content, "welcome aboard");
359 assert_eq!(msgs[0].author, owner_author.public_key());
360 }
361}