use thiserror::Error;
use crate::event::{
Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
SingleLetterTag, Tag, TagKind, Tags,
};
use crate::key::{PublicKey, PublicKeyError};
use crate::nips::nip22::{Comment, CommentScope};
use crate::types::{ImageDimensions, ImageError, RelayUrl, RelayUrlError};
pub const KIND_COMMUNITY_DEFINITION: Kind = Kind::new(34_550);
pub const KIND_POST_APPROVAL: Kind = Kind::new(4_550);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CommunityRelayMarker {
Author,
Requests,
Approvals,
Default,
Other(String),
}
impl CommunityRelayMarker {
#[must_use]
pub const fn as_str(&self) -> Option<&str> {
match self {
Self::Author => Some("author"),
Self::Requests => Some("requests"),
Self::Approvals => Some("approvals"),
Self::Default => None,
Self::Other(s) => Some(s.as_str()),
}
}
#[must_use]
pub fn parse(marker: Option<&str>) -> Self {
match marker {
None => Self::Default,
Some("author") => Self::Author,
Some("requests") => Self::Requests,
Some("approvals") => Self::Approvals,
Some(other) => Self::Other(other.to_owned()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityRelay {
pub url: RelayUrl,
pub marker: CommunityRelayMarker,
}
impl CommunityRelay {
#[must_use]
pub const fn new(url: RelayUrl, marker: CommunityRelayMarker) -> Self {
Self { url, marker }
}
#[must_use]
pub const fn unmarked(url: RelayUrl) -> Self {
Self {
url,
marker: CommunityRelayMarker::Default,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityImage {
pub url: String,
pub dim: Option<ImageDimensions>,
}
impl CommunityImage {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
dim: None,
}
}
#[must_use]
pub const fn dim(mut self, dim: ImageDimensions) -> Self {
self.dim = Some(dim);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityModerator {
pub pubkey: PublicKey,
pub relay_hint: Option<RelayUrl>,
}
impl CommunityModerator {
#[must_use]
pub const fn new(pubkey: PublicKey) -> Self {
Self {
pubkey,
relay_hint: None,
}
}
#[must_use]
pub fn relay_hint(mut self, hint: RelayUrl) -> Self {
self.relay_hint = Some(hint);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityDefinition {
pub identifier: String,
pub name: Option<String>,
pub description: Option<String>,
pub image: Option<CommunityImage>,
pub moderators: Vec<CommunityModerator>,
pub relays: Vec<CommunityRelay>,
pub extra_tags: Vec<Tag>,
}
impl CommunityDefinition {
#[must_use]
pub fn new(identifier: impl Into<String>) -> Self {
Self {
identifier: identifier.into(),
name: None,
description: None,
image: None,
moderators: Vec::new(),
relays: Vec::new(),
extra_tags: Vec::new(),
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn image(mut self, image: CommunityImage) -> Self {
self.image = Some(image);
self
}
#[must_use]
pub fn moderator(mut self, m: CommunityModerator) -> Self {
self.moderators.push(m);
self
}
#[must_use]
pub fn relay(mut self, r: CommunityRelay) -> Self {
self.relays.push(r);
self
}
#[must_use]
pub fn extra_tag(mut self, tag: Tag) -> Self {
self.extra_tags.push(tag);
self
}
#[must_use]
pub fn to_tags(&self) -> Vec<Tag> {
let mut tags: Vec<Tag> = Vec::with_capacity(
4 + self.moderators.len() + self.relays.len() + self.extra_tags.len(),
);
tags.push(Tag::d(&self.identifier));
if let Some(name) = &self.name {
tags.push(custom("name", [name.clone()]));
}
if let Some(desc) = &self.description {
tags.push(custom("description", [desc.clone()]));
}
if let Some(image) = &self.image {
let mut values: Vec<String> = Vec::with_capacity(2);
values.push(image.url.clone());
if let Some(dim) = image.dim {
values.push(dim.to_string());
}
tags.push(custom("image", values));
}
for m in &self.moderators {
let mut values: Vec<String> = Vec::with_capacity(4);
values.push(m.pubkey.to_hex());
values.push(
m.relay_hint
.as_ref()
.map(|r| r.as_str().to_owned())
.unwrap_or_default(),
);
values.push("moderator".to_owned());
tags.push(letter(Alphabet::P, values));
}
for r in &self.relays {
let mut values: Vec<String> = Vec::with_capacity(2);
values.push(r.url.as_str().to_owned());
if let Some(marker) = r.marker.as_str() {
values.push(marker.to_owned());
}
tags.push(custom("relay", values));
}
for tag in &self.extra_tags {
tags.push(tag.clone());
}
tags
}
pub fn from_event(event: &Event) -> Result<Self, CommunityError> {
if event.kind != KIND_COMMUNITY_DEFINITION {
return Err(CommunityError::WrongKind(event.kind));
}
let identifier = identifier_value(&event.tags)
.ok_or(CommunityError::MissingIdentifier)?
.to_owned();
let mut def = Self::new(identifier);
for tag in &event.tags {
match tag.kind() {
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {
}
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
handle_p_tag(tag, &mut def)?;
}
TagKind::Custom(name) if name == "name" => {
def.name = tag.get(1).map(str::to_owned);
}
TagKind::Custom(name) if name == "description" => {
def.description = tag.get(1).map(str::to_owned);
}
TagKind::Custom(name) if name == "image" => {
def.image = parse_image(tag)?;
}
TagKind::Custom(name) if name == "relay" => {
def.relays.push(parse_relay(tag)?);
}
_ => def.extra_tags.push(tag.clone()),
}
}
Ok(def)
}
#[must_use]
pub fn coordinate(&self, author: PublicKey) -> Coordinate {
Coordinate::new(KIND_COMMUNITY_DEFINITION, author, self.identifier.clone())
}
}
fn handle_p_tag(tag: &Tag, def: &mut CommunityDefinition) -> Result<(), CommunityError> {
if tag.get(3) == Some("moderator") {
def.moderators.push(parse_moderator(tag)?);
} else {
def.extra_tags.push(tag.clone());
}
Ok(())
}
fn parse_a_tag(tag: &Tag) -> Result<(Coordinate, Option<RelayUrl>), ApprovalError> {
let coord_str = tag.get(1).ok_or(ApprovalError::MalformedAddressTag)?;
let coord = Coordinate::parse(coord_str).map_err(ApprovalError::InvalidCoordinate)?;
let relay = tag_optional_relay(tag, 2)?;
Ok((coord, relay))
}
fn dispatch_a_tag(
parsed: (Coordinate, Option<RelayUrl>),
community: &mut Option<(Coordinate, Option<RelayUrl>)>,
address_target: &mut Option<(Coordinate, Option<RelayUrl>)>,
) {
if parsed.0.kind == KIND_COMMUNITY_DEFINITION && community.is_none() {
*community = Some(parsed);
} else {
*address_target = Some(parsed);
}
}
fn parse_moderator(tag: &Tag) -> Result<CommunityModerator, CommunityError> {
let pk_hex = tag.get(1).ok_or(CommunityError::MalformedModerator)?;
let pubkey = PublicKey::parse(pk_hex).map_err(CommunityError::InvalidPublicKey)?;
let relay_hint = match tag.get(2) {
Some(s) if !s.is_empty() => {
Some(RelayUrl::parse(s).map_err(CommunityError::InvalidRelayUrl)?)
}
_ => None,
};
Ok(CommunityModerator { pubkey, relay_hint })
}
fn parse_image(tag: &Tag) -> Result<Option<CommunityImage>, CommunityError> {
let Some(url) = tag.get(1) else {
return Ok(None);
};
let mut image = CommunityImage::new(url.to_owned());
if let Some(dim_str) = tag.get(2)
&& !dim_str.is_empty()
{
image.dim = Some(
dim_str
.parse::<ImageDimensions>()
.map_err(CommunityError::InvalidImageDim)?,
);
}
Ok(Some(image))
}
fn parse_relay(tag: &Tag) -> Result<CommunityRelay, CommunityError> {
let url_str = tag.get(1).ok_or(CommunityError::MalformedRelay)?;
let url = RelayUrl::parse(url_str).map_err(CommunityError::InvalidRelayUrl)?;
let marker = CommunityRelayMarker::parse(tag.get(2));
Ok(CommunityRelay { url, marker })
}
fn identifier_value(tags: &Tags) -> Option<&str> {
let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::D));
tags.find_first(&head).and_then(|tag| tag.get(1))
}
fn custom<I, S>(name: &str, args: I) -> Tag
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Tag::with(&TagKind::Custom(name.to_owned()), args)
}
fn letter<I, S>(alphabet: Alphabet, args: I) -> Tag
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let head = TagKind::single_letter(SingleLetterTag::lowercase(alphabet));
Tag::with(&head, args)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApprovalTarget {
Event {
id: EventId,
relay_hint: Option<RelayUrl>,
},
Address {
coordinate: Coordinate,
relay_hint: Option<RelayUrl>,
},
Both {
id: EventId,
coordinate: Coordinate,
relay_hint: Option<RelayUrl>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostApproval {
pub community: Coordinate,
pub community_relay: Option<RelayUrl>,
pub target: ApprovalTarget,
pub post_kind: Kind,
pub post_author: PublicKey,
pub post_author_relay: Option<RelayUrl>,
pub original_event_json: String,
}
impl PostApproval {
#[must_use]
pub fn new(
community: Coordinate,
target: ApprovalTarget,
post_kind: Kind,
post_author: PublicKey,
original_event_json: impl Into<String>,
) -> Self {
Self {
community,
community_relay: None,
target,
post_kind,
post_author,
post_author_relay: None,
original_event_json: original_event_json.into(),
}
}
#[must_use]
pub fn community_relay(mut self, relay: RelayUrl) -> Self {
self.community_relay = Some(relay);
self
}
#[must_use]
pub fn post_author_relay(mut self, relay: RelayUrl) -> Self {
self.post_author_relay = Some(relay);
self
}
#[must_use]
pub fn to_tags(&self) -> Vec<Tag> {
let mut tags: Vec<Tag> = Vec::with_capacity(5);
let mut a_values: Vec<String> = Vec::with_capacity(2);
a_values.push(self.community.to_wire());
if let Some(relay) = &self.community_relay {
a_values.push(relay.as_str().to_owned());
}
tags.push(letter(Alphabet::A, a_values));
match &self.target {
ApprovalTarget::Event { id, relay_hint } => {
tags.push(event_tag(*id, relay_hint.as_ref()));
}
ApprovalTarget::Address {
coordinate,
relay_hint,
} => {
tags.push(address_tag(coordinate, relay_hint.as_ref()));
}
ApprovalTarget::Both {
id,
coordinate,
relay_hint,
} => {
tags.push(event_tag(*id, relay_hint.as_ref()));
tags.push(address_tag(coordinate, relay_hint.as_ref()));
}
}
let mut p_values: Vec<String> = Vec::with_capacity(2);
p_values.push(self.post_author.to_hex());
if let Some(relay) = &self.post_author_relay {
p_values.push(relay.as_str().to_owned());
}
tags.push(letter(Alphabet::P, p_values));
tags.push(letter(Alphabet::K, [self.post_kind.as_u16().to_string()]));
tags
}
pub fn from_event(event: &Event) -> Result<Self, ApprovalError> {
if event.kind != KIND_POST_APPROVAL {
return Err(ApprovalError::WrongKind(event.kind));
}
let mut community: Option<(Coordinate, Option<RelayUrl>)> = None;
let mut event_target: Option<(EventId, Option<RelayUrl>)> = None;
let mut address_target: Option<(Coordinate, Option<RelayUrl>)> = None;
let mut post_author: Option<(PublicKey, Option<RelayUrl>)> = None;
let mut post_kind: Option<Kind> = None;
for tag in &event.tags {
match tag.kind() {
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
dispatch_a_tag(parse_a_tag(tag)?, &mut community, &mut address_target);
}
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
let id_hex = tag.get(1).ok_or(ApprovalError::MalformedEventTag)?;
let id = EventId::parse(id_hex).map_err(ApprovalError::InvalidEventId)?;
let relay = tag_optional_relay(tag, 2)?;
event_target = Some((id, relay));
}
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
let pk_hex = tag.get(1).ok_or(ApprovalError::MalformedAuthorTag)?;
let pk = PublicKey::parse(pk_hex).map_err(ApprovalError::InvalidPublicKey)?;
let relay = tag_optional_relay(tag, 2)?;
post_author = Some((pk, relay));
}
TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::K => {
let k_str = tag.get(1).ok_or(ApprovalError::MalformedKindTag)?;
let raw: u16 = k_str.parse().map_err(|_| ApprovalError::MalformedKindTag)?;
post_kind = Some(Kind::new(raw));
}
_ => {}
}
}
let (community, community_relay) = community.ok_or(ApprovalError::MissingCommunity)?;
let target = match (event_target, address_target) {
(Some((id, r1)), Some((coordinate, r2))) => ApprovalTarget::Both {
id,
coordinate,
relay_hint: r1.or(r2),
},
(Some((id, relay_hint)), None) => ApprovalTarget::Event { id, relay_hint },
(None, Some((coordinate, relay_hint))) => ApprovalTarget::Address {
coordinate,
relay_hint,
},
(None, None) => return Err(ApprovalError::MissingTarget),
};
let (post_author, post_author_relay) =
post_author.ok_or(ApprovalError::MissingPostAuthor)?;
let post_kind = post_kind.ok_or(ApprovalError::MissingPostKind)?;
Ok(Self {
community,
community_relay,
target,
post_kind,
post_author,
post_author_relay,
original_event_json: event.content.clone(),
})
}
}
fn event_tag(id: EventId, relay: Option<&RelayUrl>) -> Tag {
let mut values: Vec<String> = Vec::with_capacity(2);
values.push(id.to_hex());
if let Some(r) = relay {
values.push(r.as_str().to_owned());
}
letter(Alphabet::E, values)
}
fn address_tag(coordinate: &Coordinate, relay: Option<&RelayUrl>) -> Tag {
let mut values: Vec<String> = Vec::with_capacity(2);
values.push(coordinate.to_wire());
if let Some(r) = relay {
values.push(r.as_str().to_owned());
}
letter(Alphabet::A, values)
}
fn tag_optional_relay(tag: &Tag, idx: usize) -> Result<Option<RelayUrl>, ApprovalError> {
match tag.get(idx) {
Some(s) if !s.is_empty() => Ok(Some(
RelayUrl::parse(s).map_err(ApprovalError::InvalidRelayUrl)?,
)),
_ => Ok(None),
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CommunityError {
#[error("expected kind 34550 (community definition), got {}", .0.as_u16())]
WrongKind(Kind),
#[error("NIP-72 community must carry a `d` tag")]
MissingIdentifier,
#[error("malformed `p` moderator tag (missing pubkey)")]
MalformedModerator,
#[error("malformed `relay` tag (missing URL)")]
MalformedRelay,
#[error("invalid public key: {0}")]
InvalidPublicKey(#[source] PublicKeyError),
#[error("invalid relay URL: {0}")]
InvalidRelayUrl(#[source] RelayUrlError),
#[error("invalid image dimensions: {0}")]
InvalidImageDim(#[source] ImageError),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApprovalError {
#[error("expected kind 4550 (post approval), got {}", .0.as_u16())]
WrongKind(Kind),
#[error("NIP-72 approval must carry a community `a` tag")]
MissingCommunity,
#[error("NIP-72 approval must carry an `e` and/or `a` post tag")]
MissingTarget,
#[error("NIP-72 approval must carry a `p` author tag")]
MissingPostAuthor,
#[error("NIP-72 approval must carry a `k` kind tag")]
MissingPostKind,
#[error("malformed `e` post tag (missing event id)")]
MalformedEventTag,
#[error("malformed `a` tag (missing coordinate)")]
MalformedAddressTag,
#[error("malformed `p` tag (missing author)")]
MalformedAuthorTag,
#[error("malformed `k` tag (must be unsigned 16-bit kind number)")]
MalformedKindTag,
#[error("invalid coordinate: {0}")]
InvalidCoordinate(#[source] CoordinateError),
#[error("invalid event id: {0}")]
InvalidEventId(#[source] EventIdError),
#[error("invalid public key: {0}")]
InvalidPublicKey(#[source] PublicKeyError),
#[error("invalid relay URL: {0}")]
InvalidRelayUrl(#[source] RelayUrlError),
}
impl EventBuilder {
#[must_use]
pub fn community_definition(definition: &CommunityDefinition) -> Self {
let mut builder = Self::new(KIND_COMMUNITY_DEFINITION, "");
for tag in definition.to_tags() {
builder = builder.tag(tag);
}
builder
}
#[must_use]
pub fn community_post_approval(approval: &PostApproval) -> Self {
let mut builder = Self::new(KIND_POST_APPROVAL, approval.original_event_json.clone());
for tag in approval.to_tags() {
builder = builder.tag(tag);
}
builder
}
#[must_use]
pub fn community_top_level_post(
community: Coordinate,
community_relay: Option<RelayUrl>,
community_author: PublicKey,
content: impl Into<String>,
) -> Self {
let scope = CommentScope::Address {
coordinate: community,
relay_hint: community_relay,
};
let comment = Comment::top_level(scope, content)
.with_root_kind(KIND_COMMUNITY_DEFINITION)
.with_root_author(community_author);
Self::comment(&comment)
}
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "every argument maps directly to a NIP-72 §\"Nested replies\" tag column"
)]
pub fn community_nested_reply(
community: Coordinate,
community_relay: Option<RelayUrl>,
community_author: PublicKey,
parent_post: EventId,
parent_relay: Option<RelayUrl>,
parent_kind: Kind,
parent_author: PublicKey,
content: impl Into<String>,
) -> Self {
let root = CommentScope::Address {
coordinate: community,
relay_hint: community_relay,
};
let parent = CommentScope::Event {
id: parent_post,
relay_hint: parent_relay,
};
let comment = Comment::top_level(root, content)
.with_root_kind(KIND_COMMUNITY_DEFINITION)
.with_root_author(community_author)
.with_parent(parent)
.with_parent_kind(parent_kind)
.with_parent_author(parent_author);
Self::comment(&comment)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
fn other_keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
}
fn fixture_definition() -> CommunityDefinition {
let owner = *keys().public_key();
let _ = owner;
let mod1 = CommunityModerator::new(*keys().public_key())
.relay_hint(RelayUrl::parse("wss://moderator-relay.example/").unwrap());
let mod2 = CommunityModerator::new(*other_keys().public_key());
let auth_relay = CommunityRelay::new(
RelayUrl::parse("wss://author.example/").unwrap(),
CommunityRelayMarker::Author,
);
let req_relay = CommunityRelay::new(
RelayUrl::parse("wss://requests.example/").unwrap(),
CommunityRelayMarker::Requests,
);
let plain_relay =
CommunityRelay::unmarked(RelayUrl::parse("wss://plain.example/").unwrap());
CommunityDefinition::new("rust-nostr")
.name("Rust Nostr")
.description("Implementations and discussion")
.image(
CommunityImage::new("https://example.com/logo.png")
.dim(ImageDimensions::new(800, 600).unwrap()),
)
.moderator(mod1)
.moderator(mod2)
.relay(auth_relay)
.relay(req_relay)
.relay(plain_relay)
}
#[test]
fn community_definition_round_trips_through_event() {
let def = fixture_definition();
let event = EventBuilder::community_definition(&def)
.sign_with_keys(&keys())
.unwrap();
assert_eq!(event.kind, KIND_COMMUNITY_DEFINITION);
let parsed = CommunityDefinition::from_event(&event).unwrap();
assert_eq!(parsed, def);
}
#[test]
fn community_definition_rejects_wrong_kind() {
let event = EventBuilder::text_note("nope")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
CommunityDefinition::from_event(&event),
Err(CommunityError::WrongKind(_))
));
}
#[test]
fn community_definition_requires_d_tag() {
let event = EventBuilder::new(KIND_COMMUNITY_DEFINITION, "")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
CommunityDefinition::from_event(&event),
Err(CommunityError::MissingIdentifier)
));
}
#[test]
fn community_relay_marker_round_trips_unknown() {
let m = CommunityRelayMarker::parse(Some("custom-marker"));
assert_eq!(m, CommunityRelayMarker::Other("custom-marker".to_owned()));
assert_eq!(m.as_str(), Some("custom-marker"));
}
#[test]
fn coordinate_helper_uses_owner_pubkey() {
let def = CommunityDefinition::new("slug");
let coord = def.coordinate(*keys().public_key());
assert_eq!(coord.kind, KIND_COMMUNITY_DEFINITION);
assert_eq!(coord.identifier, "slug");
assert_eq!(coord.author, *keys().public_key());
}
#[test]
fn approval_with_event_target_round_trips() {
let community = Coordinate::new(KIND_COMMUNITY_DEFINITION, *keys().public_key(), "slug");
let id = EventId::from_byte_array([0x77; 32]);
let approval = PostApproval::new(
community,
ApprovalTarget::Event {
id,
relay_hint: None,
},
Kind::TEXT_NOTE,
*other_keys().public_key(),
r#"{"id":"77...","kind":1}"#,
);
let event = EventBuilder::community_post_approval(&approval)
.sign_with_keys(&keys())
.unwrap();
assert_eq!(event.kind, KIND_POST_APPROVAL);
let parsed = PostApproval::from_event(&event).unwrap();
assert_eq!(parsed, approval);
}
#[test]
fn approval_with_address_target_round_trips() {
let community = Coordinate::new(KIND_COMMUNITY_DEFINITION, *keys().public_key(), "slug");
let target_coord = Coordinate::new(
Kind::LONG_FORM_TEXT_NOTE,
*other_keys().public_key(),
"post-1",
);
let approval = PostApproval::new(
community,
ApprovalTarget::Address {
coordinate: target_coord,
relay_hint: None,
},
Kind::LONG_FORM_TEXT_NOTE,
*other_keys().public_key(),
"{}",
);
let event = EventBuilder::community_post_approval(&approval)
.sign_with_keys(&keys())
.unwrap();
let parsed = PostApproval::from_event(&event).unwrap();
assert_eq!(parsed, approval);
}
#[test]
fn approval_with_both_targets_round_trips() {
let community = Coordinate::new(KIND_COMMUNITY_DEFINITION, *keys().public_key(), "slug");
let target_coord = Coordinate::new(
Kind::LONG_FORM_TEXT_NOTE,
*other_keys().public_key(),
"post-1",
);
let target_id = EventId::from_byte_array([0xab; 32]);
let approval = PostApproval::new(
community,
ApprovalTarget::Both {
id: target_id,
coordinate: target_coord,
relay_hint: None,
},
Kind::LONG_FORM_TEXT_NOTE,
*other_keys().public_key(),
"{}",
);
let event = EventBuilder::community_post_approval(&approval)
.sign_with_keys(&keys())
.unwrap();
let parsed = PostApproval::from_event(&event).unwrap();
assert_eq!(parsed, approval);
}
#[test]
fn approval_rejects_wrong_kind() {
let event = EventBuilder::text_note("nope")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
PostApproval::from_event(&event),
Err(ApprovalError::WrongKind(_))
));
}
#[test]
fn community_top_level_post_uses_kind_1111() {
let community = Coordinate::new(KIND_COMMUNITY_DEFINITION, *keys().public_key(), "slug");
let event =
EventBuilder::community_top_level_post(community, None, *keys().public_key(), "hi")
.sign_with_keys(&other_keys())
.unwrap();
assert_eq!(event.kind, Kind::new(1111));
}
#[test]
fn community_nested_reply_uses_kind_1111() {
let community = Coordinate::new(KIND_COMMUNITY_DEFINITION, *keys().public_key(), "slug");
let parent = EventId::from_byte_array([0xab; 32]);
let event = EventBuilder::community_nested_reply(
community,
None,
*keys().public_key(),
parent,
None,
Kind::new(1111),
*other_keys().public_key(),
"agreed",
)
.sign_with_keys(&other_keys())
.unwrap();
assert_eq!(event.kind, Kind::new(1111));
}
}