1use thiserror::Error;
24
25use crate::event::{
26 Alphabet, Coordinate, Event, EventBuilder, EventId, EventIdError, Kind, SingleLetterTag, Tag,
27 TagKind,
28};
29use crate::key::{PublicKey, PublicKeyError};
30use crate::types::{Url, UrlError};
31
32pub const KIND_GROUP_PUT_USER: Kind = Kind::GROUP_PUT_USER;
34pub const KIND_GROUP_REMOVE_USER: Kind = Kind::GROUP_REMOVE_USER;
36pub const KIND_GROUP_EDIT_METADATA: Kind = Kind::GROUP_EDIT_METADATA;
38pub const KIND_GROUP_DELETE_EVENT: Kind = Kind::GROUP_DELETE_EVENT;
40pub const KIND_GROUP_CREATE: Kind = Kind::GROUP_CREATE;
42pub const KIND_GROUP_DELETE: Kind = Kind::GROUP_DELETE;
44pub const KIND_GROUP_CREATE_INVITE: Kind = Kind::GROUP_CREATE_INVITE;
46pub const KIND_GROUP_JOIN_REQUEST: Kind = Kind::GROUP_JOIN_REQUEST;
48pub const KIND_GROUP_LEAVE_REQUEST: Kind = Kind::GROUP_LEAVE_REQUEST;
50pub const KIND_GROUP_METADATA: Kind = Kind::GROUP_METADATA;
52pub const KIND_GROUP_ADMINS: Kind = Kind::GROUP_ADMINS;
54pub const KIND_GROUP_MEMBERS: Kind = Kind::GROUP_MEMBERS;
56pub const KIND_GROUP_ROLES: Kind = Kind::GROUP_ROLES;
58
59const H_TAG: &str = "h";
60const PREVIOUS_TAG: &str = "previous";
61const NAME_TAG: &str = "name";
62const PICTURE_TAG: &str = "picture";
63const ABOUT_TAG: &str = "about";
64const PRIVATE_TAG: &str = "private";
65const RESTRICTED_TAG: &str = "restricted";
66const HIDDEN_TAG: &str = "hidden";
67const CLOSED_TAG: &str = "closed";
68const ROLE_TAG: &str = "role";
69const CODE_TAG: &str = "code";
70
71pub const RELAY_LOCAL_GROUP: &str = "_";
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
78pub struct GroupId {
79 pub host: String,
81 pub id: String,
83}
84
85impl GroupId {
86 #[must_use]
89 pub fn new(host: impl Into<String>, id: Option<String>) -> Self {
90 Self {
91 host: host.into(),
92 id: id.unwrap_or_else(|| RELAY_LOCAL_GROUP.to_owned()),
93 }
94 }
95
96 #[must_use]
100 pub fn to_wire(&self) -> String {
101 if self.id == RELAY_LOCAL_GROUP {
102 self.host.clone()
103 } else {
104 format!("{}'{}", self.host, self.id)
105 }
106 }
107
108 pub fn parse(input: &str) -> Result<Self, GroupIdError> {
115 let (host, id) = input.split_once('\'').map_or_else(
116 || (input.to_owned(), None),
117 |(h, i)| (h.to_owned(), Some(i.to_owned())),
118 );
119 if host.is_empty() {
120 return Err(GroupIdError::EmptyHost);
121 }
122 if let Some(id_str) = &id
123 && !is_valid_id(id_str)
124 {
125 return Err(GroupIdError::InvalidId(id_str.clone()));
126 }
127 Ok(Self::new(host, id))
128 }
129}
130
131fn is_valid_id(id: &str) -> bool {
132 !id.is_empty()
133 && id
134 .bytes()
135 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
136}
137
138#[derive(Debug, Error)]
140#[non_exhaustive]
141pub enum GroupIdError {
142 #[error("group id missing host portion")]
144 EmptyHost,
145 #[error("group id `{0}` contains invalid characters (allowed: [a-z0-9-_])")]
147 InvalidId(String),
148}
149
150pub type PreviousReferences = Vec<String>;
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct GroupRef {
156 pub id: String,
158 pub previous: PreviousReferences,
160}
161
162impl GroupRef {
163 #[must_use]
165 pub fn new(id: impl Into<String>) -> Self {
166 Self {
167 id: id.into(),
168 previous: Vec::new(),
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct JoinRequest {
176 pub reason: String,
178 pub group: GroupRef,
180 pub code: Option<String>,
182 pub extra_tags: Vec<Tag>,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct LeaveRequest {
189 pub reason: String,
191 pub group: GroupRef,
193 pub extra_tags: Vec<Tag>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum ModerationKind {
201 PutUser {
203 pubkey: PublicKey,
205 roles: Vec<String>,
207 },
208 RemoveUser(PublicKey),
210 EditMetadata(GroupMetadataPatch),
212 DeleteEvent(EventId),
214 CreateGroup,
216 DeleteGroup,
218 CreateInvite {
220 code: String,
222 },
223 Custom {
225 kind: Kind,
227 tags: Vec<Tag>,
230 },
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
236pub struct GroupFlags(u8);
237
238impl GroupFlags {
239 pub const PRIVATE: Self = Self(0b0001);
241 pub const RESTRICTED: Self = Self(0b0010);
243 pub const HIDDEN: Self = Self(0b0100);
245 pub const CLOSED: Self = Self(0b1000);
247
248 #[must_use]
250 pub const fn empty() -> Self {
251 Self(0)
252 }
253
254 #[must_use]
256 pub const fn contains(self, other: Self) -> bool {
257 (self.0 & other.0) == other.0
258 }
259}
260
261impl std::ops::BitOr for GroupFlags {
262 type Output = Self;
263
264 fn bitor(self, rhs: Self) -> Self {
265 Self(self.0 | rhs.0)
266 }
267}
268
269impl std::ops::BitOrAssign for GroupFlags {
270 fn bitor_assign(&mut self, rhs: Self) {
271 self.0 |= rhs.0;
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Default)]
277pub struct GroupMetadataPatch {
278 pub name: Option<String>,
280 pub picture: Option<Url>,
282 pub about: Option<String>,
284 pub flags: GroupFlags,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct ModerationAction {
292 pub reason: String,
294 pub group: GroupRef,
296 pub action: ModerationKind,
298 pub extra_tags: Vec<Tag>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Default)]
304pub struct GroupMetadata {
305 pub identifier: String,
307 pub name: Option<String>,
309 pub picture: Option<Url>,
311 pub about: Option<String>,
313 pub flags: GroupFlags,
315 pub extra_tags: Vec<Tag>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct GroupAdminEntry {
322 pub pubkey: PublicKey,
324 pub roles: Vec<String>,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct GroupAdmins {
331 pub identifier: String,
333 pub content: String,
335 pub admins: Vec<GroupAdminEntry>,
337 pub extra_tags: Vec<Tag>,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct GroupMembers {
344 pub identifier: String,
346 pub content: String,
348 pub members: Vec<PublicKey>,
350 pub extra_tags: Vec<Tag>,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct GroupRole {
357 pub name: String,
359 pub description: Option<String>,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct GroupRoles {
366 pub identifier: String,
368 pub content: String,
370 pub roles: Vec<GroupRole>,
372 pub extra_tags: Vec<Tag>,
374}
375
376#[derive(Debug, Error)]
378#[non_exhaustive]
379pub enum GroupError {
380 #[error("unexpected kind for NIP-29 event: {}", .0.as_u16())]
382 WrongKind(Kind),
383 #[error("NIP-29 event missing required `h` tag")]
385 MissingGroupRef,
386 #[error("NIP-29 state event missing required `d` tag")]
388 MissingIdentifier,
389 #[error("NIP-29 moderation event missing required `p` tag")]
391 MissingPubkey,
392 #[error("NIP-29 delete-event missing required `e` tag")]
394 MissingEvent,
395 #[error("NIP-29 create-invite missing required `code` tag")]
397 MissingCode,
398 #[error(transparent)]
400 InvalidGroupId(#[from] GroupIdError),
401 #[error(transparent)]
403 InvalidPublicKey(#[from] PublicKeyError),
404 #[error(transparent)]
406 InvalidUrl(#[from] UrlError),
407 #[error(transparent)]
409 InvalidEventId(#[from] EventIdError),
410}
411
412fn h_tag_value(event: &Event) -> Option<&str> {
413 event
414 .tags
415 .iter()
416 .find(|tag| tag.name() == H_TAG)
417 .and_then(|tag| tag.get(1))
418}
419
420fn d_tag_value(event: &Event) -> Option<&str> {
421 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::D));
422 event.tags.find_first(&head).and_then(|tag| tag.get(1))
423}
424
425fn previous_from_event(event: &Event) -> PreviousReferences {
426 event
427 .tags
428 .iter()
429 .find(|tag| tag.name() == PREVIOUS_TAG)
430 .map_or_else(Vec::new, |tag| {
431 tag.values().iter().skip(1).cloned().collect()
432 })
433}
434
435fn group_ref_tag(group: &GroupRef) -> Vec<Tag> {
436 let mut tags = vec![Tag::with(&TagKind::from_wire(H_TAG), [group.id.clone()])];
437 if !group.previous.is_empty() {
438 let mut cols = vec![PREVIOUS_TAG.to_owned()];
439 cols.extend(group.previous.iter().cloned());
440 if let Ok(tag) = Tag::new(cols) {
441 tags.push(tag);
442 }
443 }
444 tags
445}
446
447fn metadata_apply_flags(builder: &mut EventBuilder, patch: &GroupMetadataPatch) {
448 if let Some(name) = &patch.name {
449 *builder = builder
450 .clone()
451 .tag(Tag::with(&TagKind::from_wire(NAME_TAG), [name.clone()]));
452 }
453 if let Some(picture) = &patch.picture {
454 *builder = builder.clone().tag(Tag::with(
455 &TagKind::from_wire(PICTURE_TAG),
456 [picture.as_str().to_owned()],
457 ));
458 }
459 if let Some(about) = &patch.about {
460 *builder = builder
461 .clone()
462 .tag(Tag::with(&TagKind::from_wire(ABOUT_TAG), [about.clone()]));
463 }
464 for (bit, name) in [
465 (GroupFlags::PRIVATE, PRIVATE_TAG),
466 (GroupFlags::RESTRICTED, RESTRICTED_TAG),
467 (GroupFlags::HIDDEN, HIDDEN_TAG),
468 (GroupFlags::CLOSED, CLOSED_TAG),
469 ] {
470 if patch.flags.contains(bit) {
471 *builder = builder
472 .clone()
473 .tag(Tag::with(&TagKind::from_wire(name), Vec::<String>::new()));
474 }
475 }
476}
477
478impl JoinRequest {
479 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
485 if event.kind != KIND_GROUP_JOIN_REQUEST {
486 return Err(GroupError::WrongKind(event.kind));
487 }
488 let id = h_tag_value(event)
489 .ok_or(GroupError::MissingGroupRef)?
490 .to_owned();
491 let mut code: Option<String> = None;
492 let mut extra_tags: Vec<Tag> = Vec::new();
493 for tag in &event.tags {
494 match tag.name() {
495 H_TAG | PREVIOUS_TAG => {}
496 CODE_TAG => code = tag.get(1).map(str::to_owned),
497 _ => extra_tags.push(tag.clone()),
498 }
499 }
500 Ok(Self {
501 reason: event.content.clone(),
502 group: GroupRef {
503 id,
504 previous: previous_from_event(event),
505 },
506 code,
507 extra_tags,
508 })
509 }
510}
511
512impl LeaveRequest {
513 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
519 if event.kind != KIND_GROUP_LEAVE_REQUEST {
520 return Err(GroupError::WrongKind(event.kind));
521 }
522 let id = h_tag_value(event)
523 .ok_or(GroupError::MissingGroupRef)?
524 .to_owned();
525 let mut extra_tags: Vec<Tag> = Vec::new();
526 for tag in &event.tags {
527 match tag.name() {
528 H_TAG | PREVIOUS_TAG => {}
529 _ => extra_tags.push(tag.clone()),
530 }
531 }
532 Ok(Self {
533 reason: event.content.clone(),
534 group: GroupRef {
535 id,
536 previous: previous_from_event(event),
537 },
538 extra_tags,
539 })
540 }
541}
542
543impl ModerationAction {
544 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
550 let kind = event.kind;
551 if !(9_000..=9_020).contains(&kind.as_u16()) {
552 return Err(GroupError::WrongKind(kind));
553 }
554 let group_id = h_tag_value(event)
555 .ok_or(GroupError::MissingGroupRef)?
556 .to_owned();
557 let action = parse_moderation_kind(event)?;
558 let mut extra_tags: Vec<Tag> = Vec::new();
559 for tag in &event.tags {
560 if !is_canonical_moderation_tag(tag, &action) {
561 extra_tags.push(tag.clone());
562 }
563 }
564 Ok(Self {
565 reason: event.content.clone(),
566 group: GroupRef {
567 id: group_id,
568 previous: previous_from_event(event),
569 },
570 action,
571 extra_tags,
572 })
573 }
574}
575
576fn is_canonical_moderation_tag(tag: &Tag, action: &ModerationKind) -> bool {
577 matches!(
578 (tag.name(), action),
579 (H_TAG | PREVIOUS_TAG, _)
580 | (
581 "p",
582 ModerationKind::PutUser { .. } | ModerationKind::RemoveUser(_)
583 )
584 | ("e", ModerationKind::DeleteEvent(_))
585 | (CODE_TAG, ModerationKind::CreateInvite { .. })
586 | (
587 NAME_TAG
588 | PICTURE_TAG
589 | ABOUT_TAG
590 | PRIVATE_TAG
591 | RESTRICTED_TAG
592 | HIDDEN_TAG
593 | CLOSED_TAG,
594 ModerationKind::EditMetadata(_)
595 )
596 )
597}
598
599fn parse_moderation_kind(event: &Event) -> Result<ModerationKind, GroupError> {
600 match event.kind {
601 KIND_GROUP_PUT_USER => {
602 let tag = event
603 .tags
604 .iter()
605 .find(|t| matches!(t.kind(), TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P))
606 .ok_or(GroupError::MissingPubkey)?;
607 let pk_hex = tag.get(1).ok_or(GroupError::MissingPubkey)?;
608 let pubkey = PublicKey::parse(pk_hex)?;
609 let roles = tag.values().iter().skip(2).cloned().collect();
610 Ok(ModerationKind::PutUser { pubkey, roles })
611 }
612 KIND_GROUP_REMOVE_USER => {
613 let tag = event
614 .tags
615 .iter()
616 .find(|t| matches!(t.kind(), TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P))
617 .ok_or(GroupError::MissingPubkey)?;
618 let pk_hex = tag.get(1).ok_or(GroupError::MissingPubkey)?;
619 Ok(ModerationKind::RemoveUser(PublicKey::parse(pk_hex)?))
620 }
621 KIND_GROUP_EDIT_METADATA => Ok(ModerationKind::EditMetadata(parse_metadata_patch(event)?)),
622 KIND_GROUP_DELETE_EVENT => {
623 let tag = event
624 .tags
625 .iter()
626 .find(|t| matches!(t.kind(), TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E))
627 .ok_or(GroupError::MissingEvent)?;
628 let id_hex = tag.get(1).ok_or(GroupError::MissingEvent)?;
629 Ok(ModerationKind::DeleteEvent(EventId::parse(id_hex)?))
630 }
631 KIND_GROUP_CREATE => Ok(ModerationKind::CreateGroup),
632 KIND_GROUP_DELETE => Ok(ModerationKind::DeleteGroup),
633 KIND_GROUP_CREATE_INVITE => {
634 let code = event
635 .tags
636 .iter()
637 .find(|t| t.name() == CODE_TAG)
638 .and_then(|t| t.get(1))
639 .ok_or(GroupError::MissingCode)?
640 .to_owned();
641 Ok(ModerationKind::CreateInvite { code })
642 }
643 other => Ok(ModerationKind::Custom {
644 kind: other,
645 tags: event
646 .tags
647 .iter()
648 .filter(|t| t.name() != H_TAG && t.name() != PREVIOUS_TAG)
649 .cloned()
650 .collect(),
651 }),
652 }
653}
654
655fn parse_metadata_patch(event: &Event) -> Result<GroupMetadataPatch, GroupError> {
656 let mut out = GroupMetadataPatch::default();
657 for tag in &event.tags {
658 match tag.name() {
659 NAME_TAG => out.name = tag.get(1).map(str::to_owned),
660 PICTURE_TAG => {
661 if let Some(raw) = tag.get(1) {
662 out.picture = Some(Url::parse(raw)?);
663 }
664 }
665 ABOUT_TAG => out.about = tag.get(1).map(str::to_owned),
666 PRIVATE_TAG => out.flags |= GroupFlags::PRIVATE,
667 RESTRICTED_TAG => out.flags |= GroupFlags::RESTRICTED,
668 HIDDEN_TAG => out.flags |= GroupFlags::HIDDEN,
669 CLOSED_TAG => out.flags |= GroupFlags::CLOSED,
670 _ => {}
671 }
672 }
673 Ok(out)
674}
675
676impl GroupMetadata {
677 #[must_use]
679 pub fn coordinate(&self, author: PublicKey) -> Coordinate {
680 Coordinate::new(KIND_GROUP_METADATA, author, self.identifier.clone())
681 }
682
683 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
689 if event.kind != KIND_GROUP_METADATA {
690 return Err(GroupError::WrongKind(event.kind));
691 }
692 let identifier = d_tag_value(event)
693 .ok_or(GroupError::MissingIdentifier)?
694 .to_owned();
695 let patch = parse_metadata_patch(event)?;
696 let extra_tags = event
697 .tags
698 .iter()
699 .filter(|tag| {
700 let name = tag.name();
701 name != "d"
702 && name != NAME_TAG
703 && name != PICTURE_TAG
704 && name != ABOUT_TAG
705 && name != PRIVATE_TAG
706 && name != RESTRICTED_TAG
707 && name != HIDDEN_TAG
708 && name != CLOSED_TAG
709 })
710 .cloned()
711 .collect();
712 Ok(Self {
713 identifier,
714 name: patch.name,
715 picture: patch.picture,
716 about: patch.about,
717 flags: patch.flags,
718 extra_tags,
719 })
720 }
721}
722
723impl GroupAdmins {
724 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
730 if event.kind != KIND_GROUP_ADMINS {
731 return Err(GroupError::WrongKind(event.kind));
732 }
733 let identifier = d_tag_value(event)
734 .ok_or(GroupError::MissingIdentifier)?
735 .to_owned();
736 let mut admins: Vec<GroupAdminEntry> = Vec::new();
737 let mut extra_tags: Vec<Tag> = Vec::new();
738 for tag in &event.tags {
739 match tag.kind() {
740 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
741 let pk_hex = tag.get(1).ok_or(GroupError::MissingPubkey)?;
742 let pubkey = PublicKey::parse(pk_hex)?;
743 let roles = tag.values().iter().skip(2).cloned().collect();
744 admins.push(GroupAdminEntry { pubkey, roles });
745 }
746 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {}
747 _ => extra_tags.push(tag.clone()),
748 }
749 }
750 Ok(Self {
751 identifier,
752 content: event.content.clone(),
753 admins,
754 extra_tags,
755 })
756 }
757}
758
759impl GroupMembers {
760 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
766 if event.kind != KIND_GROUP_MEMBERS {
767 return Err(GroupError::WrongKind(event.kind));
768 }
769 let identifier = d_tag_value(event)
770 .ok_or(GroupError::MissingIdentifier)?
771 .to_owned();
772 let mut members: Vec<PublicKey> = Vec::new();
773 let mut extra_tags: Vec<Tag> = Vec::new();
774 for tag in &event.tags {
775 match tag.kind() {
776 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
777 let pk_hex = tag.get(1).ok_or(GroupError::MissingPubkey)?;
778 members.push(PublicKey::parse(pk_hex)?);
779 }
780 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {}
781 _ => extra_tags.push(tag.clone()),
782 }
783 }
784 Ok(Self {
785 identifier,
786 content: event.content.clone(),
787 members,
788 extra_tags,
789 })
790 }
791}
792
793impl GroupRoles {
794 pub fn from_event(event: &Event) -> Result<Self, GroupError> {
800 if event.kind != KIND_GROUP_ROLES {
801 return Err(GroupError::WrongKind(event.kind));
802 }
803 let identifier = d_tag_value(event)
804 .ok_or(GroupError::MissingIdentifier)?
805 .to_owned();
806 let mut roles: Vec<GroupRole> = Vec::new();
807 let mut extra_tags: Vec<Tag> = Vec::new();
808 for tag in &event.tags {
809 match tag.name() {
810 ROLE_TAG => {
811 let name = tag.get(1).ok_or(GroupError::MissingPubkey)?.to_owned();
812 let description = tag.get(2).map(str::to_owned);
813 roles.push(GroupRole { name, description });
814 }
815 "d" => {}
816 _ => extra_tags.push(tag.clone()),
817 }
818 }
819 Ok(Self {
820 identifier,
821 content: event.content.clone(),
822 roles,
823 extra_tags,
824 })
825 }
826}
827
828impl EventBuilder {
829 #[must_use]
831 pub fn group_join_request(req: &JoinRequest) -> Self {
832 let mut builder = Self::new(KIND_GROUP_JOIN_REQUEST, req.reason.clone());
833 for tag in group_ref_tag(&req.group) {
834 builder = builder.tag(tag);
835 }
836 if let Some(code) = &req.code {
837 builder = builder.tag(Tag::with(&TagKind::from_wire(CODE_TAG), [code.clone()]));
838 }
839 for tag in &req.extra_tags {
840 builder = builder.tag(tag.clone());
841 }
842 builder
843 }
844
845 #[must_use]
847 pub fn group_leave_request(req: &LeaveRequest) -> Self {
848 let mut builder = Self::new(KIND_GROUP_LEAVE_REQUEST, req.reason.clone());
849 for tag in group_ref_tag(&req.group) {
850 builder = builder.tag(tag);
851 }
852 for tag in &req.extra_tags {
853 builder = builder.tag(tag.clone());
854 }
855 builder
856 }
857
858 #[must_use]
860 pub fn group_moderation(action: &ModerationAction) -> Self {
861 let kind = match &action.action {
862 ModerationKind::PutUser { .. } => KIND_GROUP_PUT_USER,
863 ModerationKind::RemoveUser(_) => KIND_GROUP_REMOVE_USER,
864 ModerationKind::EditMetadata(_) => KIND_GROUP_EDIT_METADATA,
865 ModerationKind::DeleteEvent(_) => KIND_GROUP_DELETE_EVENT,
866 ModerationKind::CreateGroup => KIND_GROUP_CREATE,
867 ModerationKind::DeleteGroup => KIND_GROUP_DELETE,
868 ModerationKind::CreateInvite { .. } => KIND_GROUP_CREATE_INVITE,
869 ModerationKind::Custom { kind, .. } => *kind,
870 };
871 let mut builder = Self::new(kind, action.reason.clone());
872 for tag in group_ref_tag(&action.group) {
873 builder = builder.tag(tag);
874 }
875 match &action.action {
876 ModerationKind::PutUser { pubkey, roles } => {
877 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
878 let mut cols = vec![pubkey.to_hex()];
879 cols.extend(roles.iter().cloned());
880 builder = builder.tag(Tag::with(&head, cols));
881 }
882 ModerationKind::RemoveUser(pubkey) => {
883 builder = builder.tag(Tag::p(*pubkey));
884 }
885 ModerationKind::EditMetadata(patch) => metadata_apply_flags(&mut builder, patch),
886 ModerationKind::DeleteEvent(id) => builder = builder.tag(Tag::e(*id)),
887 ModerationKind::CreateGroup | ModerationKind::DeleteGroup => {}
888 ModerationKind::CreateInvite { code } => {
889 builder = builder.tag(Tag::with(&TagKind::from_wire(CODE_TAG), [code.clone()]));
890 }
891 ModerationKind::Custom { tags, .. } => {
892 for tag in tags {
893 builder = builder.tag(tag.clone());
894 }
895 }
896 }
897 for tag in &action.extra_tags {
898 builder = builder.tag(tag.clone());
899 }
900 builder
901 }
902
903 #[must_use]
905 pub fn group_metadata(metadata: &GroupMetadata) -> Self {
906 let mut builder = Self::new(KIND_GROUP_METADATA, "");
907 builder = builder.tag(Tag::d(&metadata.identifier));
908 let patch = GroupMetadataPatch {
909 name: metadata.name.clone(),
910 picture: metadata.picture.clone(),
911 about: metadata.about.clone(),
912 flags: metadata.flags,
913 };
914 metadata_apply_flags(&mut builder, &patch);
915 for tag in &metadata.extra_tags {
916 builder = builder.tag(tag.clone());
917 }
918 builder
919 }
920
921 #[must_use]
923 pub fn group_admins(admins: &GroupAdmins) -> Self {
924 let mut builder = Self::new(KIND_GROUP_ADMINS, admins.content.clone());
925 builder = builder.tag(Tag::d(&admins.identifier));
926 for entry in &admins.admins {
927 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
928 let mut cols = vec![entry.pubkey.to_hex()];
929 cols.extend(entry.roles.iter().cloned());
930 builder = builder.tag(Tag::with(&head, cols));
931 }
932 for tag in &admins.extra_tags {
933 builder = builder.tag(tag.clone());
934 }
935 builder
936 }
937
938 #[must_use]
940 pub fn group_members(members: &GroupMembers) -> Self {
941 let mut builder = Self::new(KIND_GROUP_MEMBERS, members.content.clone());
942 builder = builder.tag(Tag::d(&members.identifier));
943 for pubkey in &members.members {
944 builder = builder.tag(Tag::p(*pubkey));
945 }
946 for tag in &members.extra_tags {
947 builder = builder.tag(tag.clone());
948 }
949 builder
950 }
951
952 #[must_use]
954 pub fn group_roles(roles: &GroupRoles) -> Self {
955 let mut builder = Self::new(KIND_GROUP_ROLES, roles.content.clone());
956 builder = builder.tag(Tag::d(&roles.identifier));
957 for role in &roles.roles {
958 let head = TagKind::from_wire(ROLE_TAG);
959 let cols = role.description.as_ref().map_or_else(
960 || vec![role.name.clone()],
961 |desc| vec![role.name.clone(), desc.clone()],
962 );
963 builder = builder.tag(Tag::with(&head, cols));
964 }
965 for tag in &roles.extra_tags {
966 builder = builder.tag(tag.clone());
967 }
968 builder
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use crate::Keys;
976
977 fn keys() -> Keys {
978 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
979 }
980
981 #[test]
982 fn group_id_parse_and_render() {
983 let g = GroupId::parse("groups.example.com'pizzalovers").unwrap();
984 assert_eq!(g.host, "groups.example.com");
985 assert_eq!(g.id, "pizzalovers");
986 assert_eq!(g.to_wire(), "groups.example.com'pizzalovers");
987
988 let local = GroupId::parse("groups.example.com").unwrap();
989 assert_eq!(local.id, RELAY_LOCAL_GROUP);
990 assert_eq!(local.to_wire(), "groups.example.com");
991 }
992
993 #[test]
994 fn group_id_invalid_chars() {
995 assert!(matches!(
996 GroupId::parse("groups.example.com'BAD ID"),
997 Err(GroupIdError::InvalidId(_))
998 ));
999 }
1000
1001 #[test]
1002 fn join_request_round_trip() {
1003 let req = JoinRequest {
1004 reason: "please".into(),
1005 group: GroupRef::new("pizzalovers"),
1006 code: Some("invite-1".into()),
1007 extra_tags: Vec::new(),
1008 };
1009 let event = EventBuilder::group_join_request(&req)
1010 .sign_with_keys(&keys())
1011 .unwrap();
1012 let parsed = JoinRequest::from_event(&event).unwrap();
1013 assert_eq!(parsed, req);
1014 }
1015
1016 #[test]
1017 fn put_user_moderation_round_trip() {
1018 let action = ModerationAction {
1019 reason: "promotion".into(),
1020 group: GroupRef::new("pizzalovers"),
1021 action: ModerationKind::PutUser {
1022 pubkey: *keys().public_key(),
1023 roles: vec!["ceo".into()],
1024 },
1025 extra_tags: Vec::new(),
1026 };
1027 let event = EventBuilder::group_moderation(&action)
1028 .sign_with_keys(&keys())
1029 .unwrap();
1030 let parsed = ModerationAction::from_event(&event).unwrap();
1031 assert_eq!(parsed.group.id, "pizzalovers");
1032 match parsed.action {
1033 ModerationKind::PutUser { pubkey, roles } => {
1034 assert_eq!(pubkey, *keys().public_key());
1035 assert_eq!(roles, vec!["ceo".to_owned()]);
1036 }
1037 other => panic!("unexpected moderation kind {other:?}"),
1038 }
1039 }
1040
1041 #[test]
1042 fn metadata_round_trip() {
1043 let metadata = GroupMetadata {
1044 identifier: "pizzalovers".into(),
1045 name: Some("Pizza Lovers".into()),
1046 picture: Some(Url::parse("https://pizza.example/icon.png").unwrap()),
1047 about: Some("a group for pizza fans".into()),
1048 flags: GroupFlags::PRIVATE | GroupFlags::CLOSED,
1049 extra_tags: Vec::new(),
1050 };
1051 let event = EventBuilder::group_metadata(&metadata)
1052 .sign_with_keys(&keys())
1053 .unwrap();
1054 let parsed = GroupMetadata::from_event(&event).unwrap();
1055 assert_eq!(parsed, metadata);
1056 }
1057
1058 #[test]
1059 fn members_round_trip() {
1060 let members = GroupMembers {
1061 identifier: "pizzalovers".into(),
1062 content: "members".into(),
1063 members: vec![*keys().public_key()],
1064 extra_tags: Vec::new(),
1065 };
1066 let event = EventBuilder::group_members(&members)
1067 .sign_with_keys(&keys())
1068 .unwrap();
1069 let parsed = GroupMembers::from_event(&event).unwrap();
1070 assert_eq!(parsed, members);
1071 }
1072
1073 #[test]
1074 fn roles_round_trip() {
1075 let roles = GroupRoles {
1076 identifier: "pizzalovers".into(),
1077 content: "roles".into(),
1078 roles: vec![
1079 GroupRole {
1080 name: "ceo".into(),
1081 description: Some("the leader".into()),
1082 },
1083 GroupRole {
1084 name: "chef".into(),
1085 description: None,
1086 },
1087 ],
1088 extra_tags: Vec::new(),
1089 };
1090 let event = EventBuilder::group_roles(&roles)
1091 .sign_with_keys(&keys())
1092 .unwrap();
1093 let parsed = GroupRoles::from_event(&event).unwrap();
1094 assert_eq!(parsed, roles);
1095 }
1096
1097 #[test]
1098 fn delete_event_moderation_round_trip() {
1099 let event_id = EventId::from_byte_array([0x44; 32]);
1100 let action = ModerationAction {
1101 reason: "spam".into(),
1102 group: GroupRef::new("pizzalovers"),
1103 action: ModerationKind::DeleteEvent(event_id),
1104 extra_tags: Vec::new(),
1105 };
1106 let event = EventBuilder::group_moderation(&action)
1107 .sign_with_keys(&keys())
1108 .unwrap();
1109 let parsed = ModerationAction::from_event(&event).unwrap();
1110 assert_eq!(parsed.action, ModerationKind::DeleteEvent(event_id));
1111 }
1112}