234 lines
7.5 KiB
Rust
234 lines
7.5 KiB
Rust
use gpui::SharedString;
|
|
use nostr::prelude::*;
|
|
|
|
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Announcement {
|
|
/// Repository ID (`d` tag).
|
|
pub id: String,
|
|
/// Author of the announcement event.
|
|
pub owner: PublicKey,
|
|
/// When the announcement was published (for latest-wins resolution).
|
|
pub created_at: Timestamp,
|
|
pub name: Option<SharedString>,
|
|
pub description: Option<SharedString>,
|
|
/// Webpage URLs for browsing.
|
|
pub web: Vec<Url>,
|
|
/// URLs for `git clone`.
|
|
pub clone: Vec<Url>,
|
|
/// Relays the repository monitors for patches and issues.
|
|
pub relays: Vec<RelayUrl>,
|
|
/// Earliest unique commit ID (`r` tag with `euc` marker).
|
|
pub euc: Option<String>,
|
|
/// Other recognized maintainers.
|
|
pub maintainers: Vec<PublicKey>,
|
|
/// Hashtags labelling the repository (`t` tags).
|
|
pub hashtags: Vec<String>,
|
|
}
|
|
|
|
/// Subject of a NIP-34 issue or pull request event: the `subject` tag,
|
|
/// falling back to the first non-empty line of the content.
|
|
pub fn activity_subject(event: &Event) -> SharedString {
|
|
let subject = event
|
|
.tags
|
|
.iter()
|
|
.find_map(|tag| match Nip34Tag::parse(tag.as_slice()) {
|
|
Ok(Nip34Tag::Subject(subject)) => Some(subject),
|
|
_ => None,
|
|
});
|
|
|
|
subject
|
|
.map(SharedString::from)
|
|
.or_else(|| {
|
|
event
|
|
.content
|
|
.lines()
|
|
.map(str::trim)
|
|
.find(|line| !line.is_empty())
|
|
.map(SharedString::from)
|
|
})
|
|
.unwrap_or(SharedString::from("Untitled"))
|
|
}
|
|
|
|
impl Announcement {
|
|
/// Parse a kind `30617` event. Returns `None` if the kind is wrong or the `d` tag is missing.
|
|
pub fn from_event(event: &Event) -> Option<Self> {
|
|
if event.kind != Kind::GitRepoAnnouncement {
|
|
return None;
|
|
}
|
|
|
|
let id = event.tags.identifier()?;
|
|
|
|
let mut hashtags: Vec<String> = Vec::new();
|
|
hashtags.extend(event.tags.hashtags().map(|t| t.to_string()));
|
|
|
|
let mut name: Option<SharedString> = None;
|
|
let mut description: Option<SharedString> = None;
|
|
let mut web: Vec<Url> = Vec::new();
|
|
let mut clone: Vec<Url> = Vec::new();
|
|
let mut relays: Vec<RelayUrl> = Vec::new();
|
|
let mut euc: Option<String> = None;
|
|
let mut maintainers: Vec<PublicKey> = Vec::new();
|
|
|
|
for tag in event.tags.iter() {
|
|
match Nip34Tag::parse(tag.as_slice()) {
|
|
Ok(Nip34Tag::Name(value)) => name = Some(value.into()),
|
|
Ok(Nip34Tag::Description(value)) => description = Some(value.into()),
|
|
Ok(Nip34Tag::Web(urls)) => web.extend(urls),
|
|
Ok(Nip34Tag::Clone(urls)) => clone.extend(urls),
|
|
Ok(Nip34Tag::Relays(urls)) => relays.extend(urls),
|
|
Ok(Nip34Tag::EarliestUniqueCommitId(commit)) => euc = Some(commit.to_string()),
|
|
Ok(Nip34Tag::Maintainers(keys)) => maintainers.extend(keys),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Some(Self {
|
|
owner: event.pubkey,
|
|
created_at: event.created_at,
|
|
id,
|
|
name,
|
|
description,
|
|
web,
|
|
clone,
|
|
relays,
|
|
euc,
|
|
maintainers,
|
|
hashtags,
|
|
})
|
|
}
|
|
|
|
/// The repository address of this announcement.
|
|
pub fn addr(&self) -> crate::RepoAddr {
|
|
crate::repo_addr(self.owner, self.id.clone())
|
|
}
|
|
|
|
/// The description of the repository, or a default if none is provided.
|
|
pub fn description(&self) -> SharedString {
|
|
self.description
|
|
.clone()
|
|
.unwrap_or(SharedString::from("No description"))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const MAINTAINER_HEX: &str = "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272";
|
|
|
|
fn keys() -> Keys {
|
|
Keys::new(
|
|
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
|
|
.expect("valid secret key"),
|
|
)
|
|
}
|
|
|
|
/// Build a signed kind `30617` event from raw tag values.
|
|
fn announcement_event(tags: &[&[&str]]) -> Event {
|
|
let tags: Vec<Tag> = tags
|
|
.iter()
|
|
.map(|t| Tag::parse(t.to_vec()).expect("valid tag"))
|
|
.collect();
|
|
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
|
.tags(tags)
|
|
.finalize(&keys())
|
|
.expect("signed event")
|
|
}
|
|
|
|
#[test]
|
|
fn parses_full_announcement() {
|
|
let event = announcement_event(&[
|
|
&["d", "my-repo"],
|
|
&["name", "My Repo"],
|
|
&["description", "A test repository"],
|
|
&["web", "https://example.com/repo"],
|
|
&["clone", "https://example.com/repo.git"],
|
|
&["relays", "wss://relay.example.com"],
|
|
&["r", "aa231c4c6a5777dc89b42207b499891a344add5c", "euc"],
|
|
&["maintainers", MAINTAINER_HEX],
|
|
&["t", "rust"],
|
|
&["t", "nostr"],
|
|
]);
|
|
|
|
let announcement = Announcement::from_event(&event).expect("parses");
|
|
|
|
assert_eq!(announcement.owner, keys().public_key());
|
|
assert_eq!(announcement.id, "my-repo");
|
|
assert_eq!(announcement.name.as_deref(), Some("My Repo"));
|
|
assert_eq!(
|
|
announcement.description.as_deref(),
|
|
Some("A test repository")
|
|
);
|
|
assert_eq!(
|
|
announcement.web,
|
|
vec![Url::parse("https://example.com/repo").unwrap()]
|
|
);
|
|
assert_eq!(
|
|
announcement.clone,
|
|
vec![Url::parse("https://example.com/repo.git").unwrap()]
|
|
);
|
|
assert_eq!(
|
|
announcement.relays,
|
|
vec![RelayUrl::parse("wss://relay.example.com").unwrap()]
|
|
);
|
|
assert_eq!(
|
|
announcement.euc.as_deref(),
|
|
Some("aa231c4c6a5777dc89b42207b499891a344add5c")
|
|
);
|
|
assert_eq!(
|
|
announcement.maintainers,
|
|
vec![PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey")]
|
|
);
|
|
assert_eq!(announcement.hashtags, vec!["rust", "nostr"]);
|
|
}
|
|
|
|
#[test]
|
|
fn requires_d_tag() {
|
|
let event = announcement_event(&[&["name", "No id"]]);
|
|
|
|
assert!(Announcement::from_event(&event).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_other_kinds() {
|
|
let event = EventBuilder::new(Kind::GitIssue, "")
|
|
.finalize(&keys())
|
|
.expect("signed event");
|
|
|
|
assert!(Announcement::from_event(&event).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn drops_malformed_values() {
|
|
let event = announcement_event(&[
|
|
&["d", "my-repo"],
|
|
&["clone", "not a url"],
|
|
&["relays", "wss://good.example.com"],
|
|
&["maintainers", "not-a-pubkey"],
|
|
]);
|
|
|
|
let announcement = Announcement::from_event(&event).expect("parses");
|
|
|
|
// An invalid URL keeps the whole clone tag from being parsed.
|
|
assert!(announcement.clone.is_empty());
|
|
assert_eq!(
|
|
announcement.relays,
|
|
vec![RelayUrl::parse("wss://good.example.com").unwrap()]
|
|
);
|
|
assert!(announcement.maintainers.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_unknown_tags() {
|
|
let event = announcement_event(&[&["d", "my-repo"], &["t", "label"], &["subject", "n/a"]]);
|
|
|
|
let announcement = Announcement::from_event(&event).expect("parses");
|
|
|
|
assert_eq!(announcement.id, "my-repo");
|
|
assert!(announcement.name.is_none());
|
|
assert!(announcement.web.is_empty());
|
|
}
|
|
}
|