From e98276e79cea0c3e474ca0251c276c474c35ed70 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Sun, 21 May 2023 11:19:28 +0000 Subject: groups --- src/groups/group.rs | 357 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/groups/groups.rs | 47 +++++++ src/groups/init.rs | 244 +++++++++++++++++++++++++++++++++++ src/groups/mod.rs | 3 + 4 files changed, 651 insertions(+) create mode 100644 src/groups/group.rs create mode 100644 src/groups/groups.rs create mode 100644 src/groups/init.rs create mode 100644 src/groups/mod.rs (limited to 'src/groups') diff --git a/src/groups/group.rs b/src/groups/group.rs new file mode 100644 index 0000000..d7f2fbf --- /dev/null +++ b/src/groups/group.rs @@ -0,0 +1,357 @@ +use std::{str::FromStr, path::PathBuf}; + +use nostr::{EventId, secp256k1::XOnlyPublicKey, prelude::UncheckedUrl, Tag, Event}; +use nostr_sdk::{Timestamp, Keys}; + +use crate::{utils::load_event, ngit_tag::{tag_extract_value_as_event_id, tag_extract_relays, tag_is_group, tag_group_with_relays}}; + +use super::{init::{InitializeGroup, self}}; + +/// [`Group`] error +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error processing initialisation group content json - incorrect format? + #[error("group cannot be initialised from content: {0}")] + InitializeJson(#[from] init::Error), + /// Error group is not avialable locally in .ngit + #[error("group is not available in .ngit/groups/{0}.json")] + GroupJsonNotAvailable(String), +} + +#[derive(Eq, PartialEq, Clone)] +pub struct StartFinish { + pub start:Timestamp, + pub finish: Option, +} + +struct MembershipDetails { + pub id:EventId, + pub dates: Vec, + pub relays: Vec, +} +pub struct MembershipCollection { + collection:Vec, +} +impl MembershipCollection { + pub fn new() -> Self { + Self { + collection: vec![], + } + } + pub fn add_group_dates( + &mut self, + group_tag:Tag, + start_finish:StartFinish, + ) { + + if !tag_is_group(&group_tag) { + panic!("tag supplied to add_group_dates isn't a group tag"); + } + + match self.collection.iter_mut().find( + |g| tag_extract_value_as_event_id(&group_tag).eq(&g.id) + ) { + None => { + self.collection.push( + MembershipDetails { + id: tag_extract_value_as_event_id(&group_tag), + dates: vec![ + start_finish + ], + relays: tag_extract_relays(&group_tag), + } + ); + }, + Some(group_dates_relays) => { + match group_dates_relays.dates.iter().find( + |d| start_finish.eq(&d) + ) { + None => group_dates_relays.dates.push(start_finish), + Some(_) => (), + } + } + } + } + + pub fn get_first_active_group(&self) -> Option<&EventId> { + let a = self.get_active_groups(); + if a.is_empty() { None } + else { Some(a[0]) } + } + + pub fn get_active_groups(&self) -> Vec<&EventId> { + let mut active = vec![]; + for m in &self.collection { + if m.dates.iter().any(|sf| sf.finish.is_none()) { + active.push(&m.id); + } + } + active + } +} + +pub struct PubKeyDates { + pubkey:XOnlyPublicKey, + dates: Vec, +} + +pub struct Group { + pub id: EventId, + name:Option, + about:Option, + picture:Option, + pub relays:Vec, + direct_members: Vec, + member_groups:MembershipCollection, + indirect_member_groups:MembershipCollection, + admin_group:MembershipCollection, + indirect_admin_groups:MembershipCollection, + pub events:Vec, + hash: String, // hash of event IDs that make up this state +} + +impl Group { + + pub fn new(init:&InitializeGroup, keys:&Keys) -> Result { + let event = init.initialize(&keys); + Group::new_from_event(event) + } + + pub fn new_from_json_event(json_string:String) -> Result { + let event = Event::from_json(json_string) + .expect("json_string to be formated as event"); + Group::new_from_event(event) + } + + pub fn open (group_id:String, repo_dir_path:&PathBuf) -> Result { + let path = repo_dir_path.join( + format!(".ngit/groups/{}.json",group_id) + ); + if path.exists() { + Ok( + Group::new_from_event( + load_event(path) + .expect("group event in json to be well formatted as a group event"), + ) + .expect("file content at path to be a well formated group event") + ) + } + else { + Err(Error::GroupJsonNotAvailable(group_id)) + } + } + + pub fn new_from_event(event:Event) -> Result { + match InitializeGroup::from_json(&event.content) { + Err(e) => return Err(Error::InitializeJson(e)), + Ok(g) => { + let start_finish = StartFinish { start: event.created_at, finish: None }; + let mut direct_members: Vec = vec![]; + // add direct_members + for m in g.direct_members { + let key = XOnlyPublicKey::from_str(m.as_str()); + match key { + Ok(k) => direct_members.push( + PubKeyDates { + pubkey: k, + dates: vec![ + start_finish.clone(), + ] + }, + ), + // could add pubkey to an invalid vector and report on it? + Err(_) => (), + } + } + // add member groups + // let mut member_groups: Vec = vec![]; + let mut member_groups = MembershipCollection::new(); + for m in g.member_groups { + // let event_id_relay = EventIdRelays::from_tag(m); + // member_groups.push(GroupDatesRelays { + // id: event_id_relay.id, + // dates: vec![ + // StartFinish { start: event.created_at, finish: None } + // ], + // relays: match event_id_relay.relay { + // None => vec![], + // Some(r) => vec![r], + // } + // }); + // add_group_dates_to_vector( + // &mut member_groups, + // m, + // StartFinish { start: event.created_at, finish: None }, + // ) + member_groups.add_group_dates( + m, + start_finish.clone(), + ) + } + // add admin group + // let admin_group = match g.admin { + // None => vec![], + // Some(a) => match EventId::from_str(a.as_str()) { + // Ok(id) => vec![ + // GroupDatesRelays { + // id, + // dates: vec![StartFinish { + // start: event.created_at.clone(), + // finish: None, + // }] + // } + // ], + // // could report on it? + // Err(_) => vec![], + // } + // }; + let mut admin_group = MembershipCollection::new(); + // let admin_group = vec![]; + match g.admin { + None => (), + Some(t) => { + admin_group.add_group_dates( + t, + start_finish.clone(), + ) + } + } + Ok(Self { + id: event.id, + name: g.name, + about: g.about, + picture: g.picture, + relays: g.relays, + direct_members, + admin_group, + member_groups, + indirect_member_groups: MembershipCollection::new(), + indirect_admin_groups: MembershipCollection::new(), + events:vec![event], + hash: "hash".to_string(), // hash of event IDs that make up this state + }) + } + } + } + + fn load_recurring_sub_groups() { + + } + + pub fn get_ref(&self) -> Tag { + tag_group_with_relays( + &self.id.to_string(), + &self.relays, + ) + } + + // fn add_member(&self,) -> Self; + // fn init(&self,keys:Keys) -> Event; + // fn remove_member() -> Self; + // fn set_admin(&self) -> Self; + // fn set_name(&self) -> Self; + // fn set_about(&self) -> Self; + // fn set_picture(&self) -> Self; + + // use enums instead of having so many functions? then use a vector to store all the changes so they can be made in one event? + + // fn new( + // &self, + // direct_members:Vec, + // sub_groups:String, + // relays:Vec, + // name:String, + // ) -> Self { + // // create initialation event + // // EventBuilder::new( + // // 100, + + // // ) + // self + // } + + pub fn members(&self) -> Vec<&XOnlyPublicKey> { + let mut pubkeys = vec![]; + for m in &self.direct_members { + pubkeys.push(&m.pubkey); + } + pubkeys + } + + pub fn is_member(&self, pubkey: &XOnlyPublicKey) -> bool { + self.members().iter().any(|m| *pubkey == **m) + } + // pub fn admins(&self) -> Vec<&String> { get_el(&self.admins) } + // pub fn voters(&self) -> Vec<&String> { get_el(&self.voters) } + // pub fn members(&self) -> Vec<&String> { get_el(&self.members) } + // pub fn is_admin(&self, pubkey:&String) -> bool { is_el(&self.admins, &pubkey) } + // pub fn is_voter(&self, pubkey:&String) -> bool { is_el(&self.voters, &pubkey) } + // pub fn is_member(&self, pubkey:&String) -> bool { is_el(&self.members, &pubkey) } + // pub fn get_admins_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.admins,×tamp) } + // pub fn get_voters_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.voters,×tamp) } + // pub fn get_members_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.members,×tamp) } + // pub fn was_admin_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.admins, pubkey, ×tamp) } + // pub fn was_voter_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.voters, pubkey, ×tamp) } + // pub fn was_members_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.members, pubkey, ×tamp) } +} + +// fn get_el(el:&Vec) -> Vec<&String> { +// let mut current: Vec<&String> = vec![]; +// for m in el { +// if m.dates.last().unwrap().finish.is_none() { +// current.push(&m.pubkey) +// } +// } +// current +// } +// fn is_el(el:&Vec, pubkey:&String) -> bool { +// el +// .iter() +// .any( +// |m| +// &m.pubkey == pubkey +// && m.dates.last().unwrap().finish.is_none() +// ) +// } +// fn get_el_at<'a>(el:&'a Vec, timestamp: &'a Timestamp) -> Vec<&'a String> { +// let mut el_at_timestamp: Vec<&String> = vec![]; +// for m in el { +// if m.dates +// .iter() +// .any( +// |d| +// &d.start < ×tamp +// // && match &d.finish { +// // None => true, +// // _ => &d.finish.unwrap() > ×tamp +// // } +// && ( +// d.finish.is_none() +// || &d.finish.unwrap() > ×tamp +// ) +// ) { +// el_at_timestamp.push(&m.pubkey) +// } +// } +// el_at_timestamp +// } +// fn was_el_at(el:&Vec, pubkey:&String,timestamp:&Timestamp) -> bool { +// // PublicKey::try_from_hex_string(pubkey); +// el +// .iter() +// .any( +// |m| +// &m.pubkey == pubkey +// && m.dates +// .iter() +// .any( +// |d| +// &d.start < ×tamp +// && ( +// d.finish.is_none() +// || &d.finish.unwrap() > ×tamp +// ) +// ) +// ) +// } + diff --git a/src/groups/groups.rs b/src/groups/groups.rs new file mode 100644 index 0000000..3b837a7 --- /dev/null +++ b/src/groups/groups.rs @@ -0,0 +1,47 @@ +use std::fs; + +use nostr::EventId; + +use crate::{utils::load_file}; + +use super::group::Group; + +pub struct Groups { + groups:Vec +} +impl Groups { + pub fn new() -> Self { + + let cur_dir = std::env::current_dir().unwrap(); + + // check for potential problems + let ngit_path = cur_dir.clone().join(".ngit"); + if !ngit_path.is_dir() { + panic!("ngit not initialised. Run 'ngit init' first..."); + } + + let mut groups = vec![]; + + for dir_entry in fs::read_dir(ngit_path.join("groups")) + .expect("groups folder to exist and read_dir to read it") + { + groups.push( + Group::new_from_json_event( + load_file( + dir_entry + .expect("DirEntry in read_dir should exist").path() + ) + .expect("group json to load from file") + ).expect("group json to produce Group") + ); + } + + Self { + groups, + } + } + + pub fn by_event_id(&self, id:&EventId) -> Option<&Group> { + self.groups.iter().find(|g| g.id == *id) + } +} \ No newline at end of file diff --git a/src/groups/init.rs b/src/groups/init.rs new file mode 100644 index 0000000..797d190 --- /dev/null +++ b/src/groups/init.rs @@ -0,0 +1,244 @@ + +use std::{str::FromStr, fmt::Debug}; +use nostr_sdk::{EventBuilder, Tag, secp256k1::XOnlyPublicKey, Keys, Event}; +use serde::{Deserialize, Serialize}; + +use crate::{kind::Kind, ngit_tag::{tag_extract_relays, tag_admin_group_with_relays, tag_extract_value, tag_hashtag, tag_into_event}}; + +/// [`InitializeGroup`] error +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error serializing or deserializing JSON data + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + // /// Error adding wrong tag kind to member_groups + // #[error("expecting event tag for member_groups but got: {0}")] + // WongMemberGroupsTagKind(String), + // /// Error InvalidGroupIdInTag + // #[error("invalid group id in member_groups tag: {0}")] + // InvalidGroupIdInTag(String), +} + +impl InitializeGroup { + + pub fn initialize(&self,keys:&Keys) -> Event { + // let keys = Keys::generate(); + EventBuilder::new( + Kind::InitializeGroup.into_sdk_custom_kind(), + self.as_json(), + &self.generate_tags(), + ) + .to_unsigned_event(keys.public_key()) + .sign(&keys) + .unwrap() + } + + fn generate_tags(&self) -> Vec { + let mut tags:Vec = vec![ + tag_hashtag("ngit-event"), + tag_hashtag("ngit-format-0.0.1"), + ]; + for m in &self.direct_members { + let key = XOnlyPublicKey::from_str(m); + match key { + Ok(k) => tags.push(Tag::PubKey(k, None)), + Err(error) => print!("could not add this pubkey to tag: {m} error: {error}"), + } + } + for m in &self.member_groups { + tags.push(m.clone()); + tags.push(tag_into_event(m.clone())); + + } + match &self.admin { + None => (), + Some(admin) => { + tags.push(tag_admin_group_with_relays( + &tag_extract_value(admin), + &tag_extract_relays(admin), + )); + tags.push(tag_into_event(admin.clone())); + }, + }; + tags + } +} + +/// InitializeGroup +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub struct InitializeGroup { + /// Name + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Description + #[serde(skip_serializing_if = "Option::is_none")] + pub about: Option, + /// Picture + #[serde(skip_serializing_if = "Option::is_none")] + pub picture: Option, + /// relays + pub relays: Vec, + /// Direct Members + pub direct_members: Vec, + /// Member Groups as group tag vector + pub member_groups: Vec, + /// Admin + #[serde(skip_serializing_if = "Option::is_none")] + pub admin: Option, + +} + +impl Default for InitializeGroup { + fn default() -> Self { + Self::new() + } +} + +impl InitializeGroup { + /// New empty [`InitializeGroup`] + pub fn new() -> Self { + Self { + name: None, + about: None, + picture: None, + relays: vec![], + direct_members: vec![], + member_groups: vec![], + admin: None, + } + } + + /// Deserialize [`InitializeGroup`] from `JSON` string + pub fn from_json(json: S) -> Result + where + S: Into, + { + Ok(serde_json::from_str(&json.into())?) + } + + /// Serialize [`InitializeGroup`] to `JSON` string + pub fn as_json(&self) -> String { + serde_json::json!(self).to_string() + } + + /// Set name + pub fn name(self, name: S) -> Self + where + S: Into, + { + Self { + name: Some(name.into()), + ..self + } + } + + /// Set about + pub fn about(self, about: S) -> Self + where + S: Into, + { + Self { + about: Some(about.into()), + ..self + } + } + + /// Set picture + pub fn picture(self, picture: S) -> Self + where + S: Into, + { + Self { + picture: Some(picture.into()), + ..self + } + } + + /// Set relays + pub fn relays(mut self, relays: &Vec) -> Self { + for m in relays { + self.relays.push(m.clone()); + } + self + } + + /// Set members + pub fn members(mut self, pubkeys: Vec, group_refs:Vec) -> Self /* Result*/ { + for m in pubkeys { + let key = XOnlyPublicKey::from_str(m.as_str()); + match key { + Ok(_k) => self.direct_members.push(m), + Err(error) => print!("could not add this pubkey to members: {m} error: {error}"), + } + } + for group_ref in group_refs { + self.member_groups.push(group_ref); + } + self + } + + /// Set admin + pub fn admin(self, group_ref: Tag) -> Self { + Self { + admin: Some(group_ref), + ..self + } + } +} + +#[cfg(test)] +mod tests { + + use nostr::prelude::UncheckedUrl; + + use crate::ngit_tag::{tag_group_with_relays, tag_group}; + + use super::*; + + #[test] + fn test_deserialize_content() { + let content = r#"{ + "name":"myname", + "picture":"https://www.example.com/profile.jpg", + "direct_members":[ + "88a14a0df1aa0223e9f3a44cd4964fb82a19590440bb8cf1610d8c7367798314", + "14c27d59268ae2554d03b89c5c01dac17a604b17ac258ad345bd0648d3f5c011" + ], + "member_groups":[ + ["group","109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897","ws://localhost"], + ["group","06bd7667a7c115fd8faf7f300302f39c019e16e6461845930686b84fbeae8c87"] + ], + "relays":["wss://relay.damus.io","ws://localhost"], + "admin":["admin-group","109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897","ws://localhost"] + }"#; + assert_eq!( + InitializeGroup::from_json(content).unwrap(), + InitializeGroup::new() + .name("myname") + // 'about' intentionally ommitted + .picture("https://www.example.com/profile.jpg") + .members( + vec![ + "88a14a0df1aa0223e9f3a44cd4964fb82a19590440bb8cf1610d8c7367798314".to_string(), + "14c27d59268ae2554d03b89c5c01dac17a604b17ac258ad345bd0648d3f5c011".to_string(), + ], + vec![ + tag_group_with_relays( + &"109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897".to_string(), + &vec!["ws://localhost".to_string()], + ), + tag_group( + &"06bd7667a7c115fd8faf7f300302f39c019e16e6461845930686b84fbeae8c87".to_string(), + ), + ], + ) + .relays(&vec!["wss://relay.damus.io".to_string(),"ws://localhost".to_string()]) + .admin( + tag_admin_group_with_relays( + &"109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897".to_string(), + &vec![UncheckedUrl::from_str("ws://localhost").unwrap()], + ), + ) + ); + } +} diff --git a/src/groups/mod.rs b/src/groups/mod.rs new file mode 100644 index 0000000..ff81bbc --- /dev/null +++ b/src/groups/mod.rs @@ -0,0 +1,3 @@ +pub mod groups; +pub mod group; +pub mod init; \ No newline at end of file -- cgit v1.2.3