upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/groups
diff options
context:
space:
mode:
Diffstat (limited to 'src/groups')
-rw-r--r--src/groups/group.rs357
-rw-r--r--src/groups/groups.rs47
-rw-r--r--src/groups/init.rs244
-rw-r--r--src/groups/mod.rs3
4 files changed, 651 insertions, 0 deletions
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 @@
1use std::{str::FromStr, path::PathBuf};
2
3use nostr::{EventId, secp256k1::XOnlyPublicKey, prelude::UncheckedUrl, Tag, Event};
4use nostr_sdk::{Timestamp, Keys};
5
6use crate::{utils::load_event, ngit_tag::{tag_extract_value_as_event_id, tag_extract_relays, tag_is_group, tag_group_with_relays}};
7
8use super::{init::{InitializeGroup, self}};
9
10/// [`Group`] error
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13 /// Error processing initialisation group content json - incorrect format?
14 #[error("group cannot be initialised from content: {0}")]
15 InitializeJson(#[from] init::Error),
16 /// Error group is not avialable locally in .ngit
17 #[error("group is not available in .ngit/groups/{0}.json")]
18 GroupJsonNotAvailable(String),
19}
20
21#[derive(Eq, PartialEq, Clone)]
22pub struct StartFinish {
23 pub start:Timestamp,
24 pub finish: Option<Timestamp>,
25}
26
27struct MembershipDetails {
28 pub id:EventId,
29 pub dates: Vec<StartFinish>,
30 pub relays: Vec<UncheckedUrl>,
31}
32pub struct MembershipCollection {
33 collection:Vec<MembershipDetails>,
34}
35impl MembershipCollection {
36 pub fn new() -> Self {
37 Self {
38 collection: vec![],
39 }
40 }
41 pub fn add_group_dates(
42 &mut self,
43 group_tag:Tag,
44 start_finish:StartFinish,
45 ) {
46
47 if !tag_is_group(&group_tag) {
48 panic!("tag supplied to add_group_dates isn't a group tag");
49 }
50
51 match self.collection.iter_mut().find(
52 |g| tag_extract_value_as_event_id(&group_tag).eq(&g.id)
53 ) {
54 None => {
55 self.collection.push(
56 MembershipDetails {
57 id: tag_extract_value_as_event_id(&group_tag),
58 dates: vec![
59 start_finish
60 ],
61 relays: tag_extract_relays(&group_tag),
62 }
63 );
64 },
65 Some(group_dates_relays) => {
66 match group_dates_relays.dates.iter().find(
67 |d| start_finish.eq(&d)
68 ) {
69 None => group_dates_relays.dates.push(start_finish),
70 Some(_) => (),
71 }
72 }
73 }
74 }
75
76 pub fn get_first_active_group(&self) -> Option<&EventId> {
77 let a = self.get_active_groups();
78 if a.is_empty() { None }
79 else { Some(a[0]) }
80 }
81
82 pub fn get_active_groups(&self) -> Vec<&EventId> {
83 let mut active = vec![];
84 for m in &self.collection {
85 if m.dates.iter().any(|sf| sf.finish.is_none()) {
86 active.push(&m.id);
87 }
88 }
89 active
90 }
91}
92
93pub struct PubKeyDates {
94 pubkey:XOnlyPublicKey,
95 dates: Vec<StartFinish>,
96}
97
98pub struct Group {
99 pub id: EventId,
100 name:Option<String>,
101 about:Option<String>,
102 picture:Option<String>,
103 pub relays:Vec<String>,
104 direct_members: Vec<PubKeyDates>,
105 member_groups:MembershipCollection,
106 indirect_member_groups:MembershipCollection,
107 admin_group:MembershipCollection,
108 indirect_admin_groups:MembershipCollection,
109 pub events:Vec<Event>,
110 hash: String, // hash of event IDs that make up this state
111}
112
113impl Group {
114
115 pub fn new(init:&InitializeGroup, keys:&Keys) -> Result<Self,Error> {
116 let event = init.initialize(&keys);
117 Group::new_from_event(event)
118 }
119
120 pub fn new_from_json_event(json_string:String) -> Result<Self,Error> {
121 let event = Event::from_json(json_string)
122 .expect("json_string to be formated as event");
123 Group::new_from_event(event)
124 }
125
126 pub fn open (group_id:String, repo_dir_path:&PathBuf) -> Result<Self,Error> {
127 let path = repo_dir_path.join(
128 format!(".ngit/groups/{}.json",group_id)
129 );
130 if path.exists() {
131 Ok(
132 Group::new_from_event(
133 load_event(path)
134 .expect("group event in json to be well formatted as a group event"),
135 )
136 .expect("file content at path to be a well formated group event")
137 )
138 }
139 else {
140 Err(Error::GroupJsonNotAvailable(group_id))
141 }
142 }
143
144 pub fn new_from_event(event:Event) -> Result<Self,Error> {
145 match InitializeGroup::from_json(&event.content) {
146 Err(e) => return Err(Error::InitializeJson(e)),
147 Ok(g) => {
148 let start_finish = StartFinish { start: event.created_at, finish: None };
149 let mut direct_members: Vec<PubKeyDates> = vec![];
150 // add direct_members
151 for m in g.direct_members {
152 let key = XOnlyPublicKey::from_str(m.as_str());
153 match key {
154 Ok(k) => direct_members.push(
155 PubKeyDates {
156 pubkey: k,
157 dates: vec![
158 start_finish.clone(),
159 ]
160 },
161 ),
162 // could add pubkey to an invalid vector and report on it?
163 Err(_) => (),
164 }
165 }
166 // add member groups
167 // let mut member_groups: Vec<GroupDatesRelays> = vec![];
168 let mut member_groups = MembershipCollection::new();
169 for m in g.member_groups {
170 // let event_id_relay = EventIdRelays::from_tag(m);
171 // member_groups.push(GroupDatesRelays {
172 // id: event_id_relay.id,
173 // dates: vec![
174 // StartFinish { start: event.created_at, finish: None }
175 // ],
176 // relays: match event_id_relay.relay {
177 // None => vec![],
178 // Some(r) => vec![r],
179 // }
180 // });
181 // add_group_dates_to_vector(
182 // &mut member_groups,
183 // m,
184 // StartFinish { start: event.created_at, finish: None },
185 // )
186 member_groups.add_group_dates(
187 m,
188 start_finish.clone(),
189 )
190 }
191 // add admin group
192 // let admin_group = match g.admin {
193 // None => vec![],
194 // Some(a) => match EventId::from_str(a.as_str()) {
195 // Ok(id) => vec![
196 // GroupDatesRelays {
197 // id,
198 // dates: vec![StartFinish {
199 // start: event.created_at.clone(),
200 // finish: None,
201 // }]
202 // }
203 // ],
204 // // could report on it?
205 // Err(_) => vec![],
206 // }
207 // };
208 let mut admin_group = MembershipCollection::new();
209 // let admin_group = vec![];
210 match g.admin {
211 None => (),
212 Some(t) => {
213 admin_group.add_group_dates(
214 t,
215 start_finish.clone(),
216 )
217 }
218 }
219 Ok(Self {
220 id: event.id,
221 name: g.name,
222 about: g.about,
223 picture: g.picture,
224 relays: g.relays,
225 direct_members,
226 admin_group,
227 member_groups,
228 indirect_member_groups: MembershipCollection::new(),
229 indirect_admin_groups: MembershipCollection::new(),
230 events:vec![event],
231 hash: "hash".to_string(), // hash of event IDs that make up this state
232 })
233 }
234 }
235 }
236
237 fn load_recurring_sub_groups() {
238
239 }
240
241 pub fn get_ref(&self) -> Tag {
242 tag_group_with_relays(
243 &self.id.to_string(),
244 &self.relays,
245 )
246 }
247
248 // fn add_member(&self,) -> Self;
249 // fn init(&self,keys:Keys) -> Event;
250 // fn remove_member() -> Self;
251 // fn set_admin(&self) -> Self;
252 // fn set_name(&self) -> Self;
253 // fn set_about(&self) -> Self;
254 // fn set_picture(&self) -> Self;
255
256 // 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?
257
258 // fn new(
259 // &self,
260 // direct_members:Vec<String>,
261 // sub_groups:String,
262 // relays:Vec<String>,
263 // name:String,
264 // ) -> Self {
265 // // create initialation event
266 // // EventBuilder::new(
267 // // 100,
268
269 // // )
270 // self
271 // }
272
273 pub fn members(&self) -> Vec<&XOnlyPublicKey> {
274 let mut pubkeys = vec![];
275 for m in &self.direct_members {
276 pubkeys.push(&m.pubkey);
277 }
278 pubkeys
279 }
280
281 pub fn is_member(&self, pubkey: &XOnlyPublicKey) -> bool {
282 self.members().iter().any(|m| *pubkey == **m)
283 }
284 // pub fn admins(&self) -> Vec<&String> { get_el(&self.admins) }
285 // pub fn voters(&self) -> Vec<&String> { get_el(&self.voters) }
286 // pub fn members(&self) -> Vec<&String> { get_el(&self.members) }
287 // pub fn is_admin(&self, pubkey:&String) -> bool { is_el(&self.admins, &pubkey) }
288 // pub fn is_voter(&self, pubkey:&String) -> bool { is_el(&self.voters, &pubkey) }
289 // pub fn is_member(&self, pubkey:&String) -> bool { is_el(&self.members, &pubkey) }
290 // pub fn get_admins_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.admins,&timestamp) }
291 // pub fn get_voters_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.voters,&timestamp) }
292 // pub fn get_members_at<'a>(&'a self, timestamp: &'a Timestamp) -> Vec<&String> { get_el_at(&self.members,&timestamp) }
293 // pub fn was_admin_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.admins, pubkey, &timestamp) }
294 // pub fn was_voter_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.voters, pubkey, &timestamp) }
295 // pub fn was_members_at(&self, pubkey:&String, timestamp: &Timestamp) -> bool { was_el_at(&self.members, pubkey, &timestamp) }
296}
297
298// fn get_el(el:&Vec<PubKeyDates>) -> Vec<&String> {
299// let mut current: Vec<&String> = vec![];
300// for m in el {
301// if m.dates.last().unwrap().finish.is_none() {
302// current.push(&m.pubkey)
303// }
304// }
305// current
306// }
307// fn is_el(el:&Vec<PubKeyDates>, pubkey:&String) -> bool {
308// el
309// .iter()
310// .any(
311// |m|
312// &m.pubkey == pubkey
313// && m.dates.last().unwrap().finish.is_none()
314// )
315// }
316// fn get_el_at<'a>(el:&'a Vec<PubKeyDates>, timestamp: &'a Timestamp) -> Vec<&'a String> {
317// let mut el_at_timestamp: Vec<&String> = vec![];
318// for m in el {
319// if m.dates
320// .iter()
321// .any(
322// |d|
323// &d.start < &timestamp
324// // && match &d.finish {
325// // None => true,
326// // _ => &d.finish.unwrap() > &timestamp
327// // }
328// && (
329// d.finish.is_none()
330// || &d.finish.unwrap() > &timestamp
331// )
332// ) {
333// el_at_timestamp.push(&m.pubkey)
334// }
335// }
336// el_at_timestamp
337// }
338// fn was_el_at(el:&Vec<PubKeyDates>, pubkey:&String,timestamp:&Timestamp) -> bool {
339// // PublicKey::try_from_hex_string(pubkey);
340// el
341// .iter()
342// .any(
343// |m|
344// &m.pubkey == pubkey
345// && m.dates
346// .iter()
347// .any(
348// |d|
349// &d.start < &timestamp
350// && (
351// d.finish.is_none()
352// || &d.finish.unwrap() > &timestamp
353// )
354// )
355// )
356// }
357
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 @@
1use std::fs;
2
3use nostr::EventId;
4
5use crate::{utils::load_file};
6
7use super::group::Group;
8
9pub struct Groups {
10 groups:Vec<Group>
11}
12impl Groups {
13 pub fn new() -> Self {
14
15 let cur_dir = std::env::current_dir().unwrap();
16
17 // check for potential problems
18 let ngit_path = cur_dir.clone().join(".ngit");
19 if !ngit_path.is_dir() {
20 panic!("ngit not initialised. Run 'ngit init' first...");
21 }
22
23 let mut groups = vec![];
24
25 for dir_entry in fs::read_dir(ngit_path.join("groups"))
26 .expect("groups folder to exist and read_dir to read it")
27 {
28 groups.push(
29 Group::new_from_json_event(
30 load_file(
31 dir_entry
32 .expect("DirEntry in read_dir should exist").path()
33 )
34 .expect("group json to load from file")
35 ).expect("group json to produce Group")
36 );
37 }
38
39 Self {
40 groups,
41 }
42 }
43
44 pub fn by_event_id(&self, id:&EventId) -> Option<&Group> {
45 self.groups.iter().find(|g| g.id == *id)
46 }
47} \ 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 @@
1
2use std::{str::FromStr, fmt::Debug};
3use nostr_sdk::{EventBuilder, Tag, secp256k1::XOnlyPublicKey, Keys, Event};
4use serde::{Deserialize, Serialize};
5
6use crate::{kind::Kind, ngit_tag::{tag_extract_relays, tag_admin_group_with_relays, tag_extract_value, tag_hashtag, tag_into_event}};
7
8/// [`InitializeGroup`] error
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11 /// Error serializing or deserializing JSON data
12 #[error("json error: {0}")]
13 Json(#[from] serde_json::Error),
14 // /// Error adding wrong tag kind to member_groups
15 // #[error("expecting event tag for member_groups but got: {0}")]
16 // WongMemberGroupsTagKind(String),
17 // /// Error InvalidGroupIdInTag
18 // #[error("invalid group id in member_groups tag: {0}")]
19 // InvalidGroupIdInTag(String),
20}
21
22impl InitializeGroup {
23
24 pub fn initialize(&self,keys:&Keys) -> Event {
25 // let keys = Keys::generate();
26 EventBuilder::new(
27 Kind::InitializeGroup.into_sdk_custom_kind(),
28 self.as_json(),
29 &self.generate_tags(),
30 )
31 .to_unsigned_event(keys.public_key())
32 .sign(&keys)
33 .unwrap()
34 }
35
36 fn generate_tags(&self) -> Vec<Tag> {
37 let mut tags:Vec<Tag> = vec![
38 tag_hashtag("ngit-event"),
39 tag_hashtag("ngit-format-0.0.1"),
40 ];
41 for m in &self.direct_members {
42 let key = XOnlyPublicKey::from_str(m);
43 match key {
44 Ok(k) => tags.push(Tag::PubKey(k, None)),
45 Err(error) => print!("could not add this pubkey to tag: {m} error: {error}"),
46 }
47 }
48 for m in &self.member_groups {
49 tags.push(m.clone());
50 tags.push(tag_into_event(m.clone()));
51
52 }
53 match &self.admin {
54 None => (),
55 Some(admin) => {
56 tags.push(tag_admin_group_with_relays(
57 &tag_extract_value(admin),
58 &tag_extract_relays(admin),
59 ));
60 tags.push(tag_into_event(admin.clone()));
61 },
62 };
63 tags
64 }
65}
66
67/// InitializeGroup
68#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
69pub struct InitializeGroup {
70 /// Name
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub name: Option<String>,
73 /// Description
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub about: Option<String>,
76 /// Picture
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub picture: Option<String>,
79 /// relays
80 pub relays: Vec<String>,
81 /// Direct Members
82 pub direct_members: Vec<String>,
83 /// Member Groups as group tag vector
84 pub member_groups: Vec<Tag>,
85 /// Admin
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub admin: Option<Tag>,
88
89}
90
91impl Default for InitializeGroup {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl InitializeGroup {
98 /// New empty [`InitializeGroup`]
99 pub fn new() -> Self {
100 Self {
101 name: None,
102 about: None,
103 picture: None,
104 relays: vec![],
105 direct_members: vec![],
106 member_groups: vec![],
107 admin: None,
108 }
109 }
110
111 /// Deserialize [`InitializeGroup`] from `JSON` string
112 pub fn from_json<S>(json: S) -> Result<Self, Error>
113 where
114 S: Into<String>,
115 {
116 Ok(serde_json::from_str(&json.into())?)
117 }
118
119 /// Serialize [`InitializeGroup`] to `JSON` string
120 pub fn as_json(&self) -> String {
121 serde_json::json!(self).to_string()
122 }
123
124 /// Set name
125 pub fn name<S>(self, name: S) -> Self
126 where
127 S: Into<String>,
128 {
129 Self {
130 name: Some(name.into()),
131 ..self
132 }
133 }
134
135 /// Set about
136 pub fn about<S>(self, about: S) -> Self
137 where
138 S: Into<String>,
139 {
140 Self {
141 about: Some(about.into()),
142 ..self
143 }
144 }
145
146 /// Set picture
147 pub fn picture<S>(self, picture: S) -> Self
148 where
149 S: Into<String>,
150 {
151 Self {
152 picture: Some(picture.into()),
153 ..self
154 }
155 }
156
157 /// Set relays
158 pub fn relays(mut self, relays: &Vec<String>) -> Self {
159 for m in relays {
160 self.relays.push(m.clone());
161 }
162 self
163 }
164
165 /// Set members
166 pub fn members(mut self, pubkeys: Vec<String>, group_refs:Vec<Tag>) -> Self /* Result<Self,Error>*/ {
167 for m in pubkeys {
168 let key = XOnlyPublicKey::from_str(m.as_str());
169 match key {
170 Ok(_k) => self.direct_members.push(m),
171 Err(error) => print!("could not add this pubkey to members: {m} error: {error}"),
172 }
173 }
174 for group_ref in group_refs {
175 self.member_groups.push(group_ref);
176 }
177 self
178 }
179
180 /// Set admin
181 pub fn admin(self, group_ref: Tag) -> Self {
182 Self {
183 admin: Some(group_ref),
184 ..self
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191
192 use nostr::prelude::UncheckedUrl;
193
194 use crate::ngit_tag::{tag_group_with_relays, tag_group};
195
196 use super::*;
197
198 #[test]
199 fn test_deserialize_content() {
200 let content = r#"{
201 "name":"myname",
202 "picture":"https://www.example.com/profile.jpg",
203 "direct_members":[
204 "88a14a0df1aa0223e9f3a44cd4964fb82a19590440bb8cf1610d8c7367798314",
205 "14c27d59268ae2554d03b89c5c01dac17a604b17ac258ad345bd0648d3f5c011"
206 ],
207 "member_groups":[
208 ["group","109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897","ws://localhost"],
209 ["group","06bd7667a7c115fd8faf7f300302f39c019e16e6461845930686b84fbeae8c87"]
210 ],
211 "relays":["wss://relay.damus.io","ws://localhost"],
212 "admin":["admin-group","109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897","ws://localhost"]
213 }"#;
214 assert_eq!(
215 InitializeGroup::from_json(content).unwrap(),
216 InitializeGroup::new()
217 .name("myname")
218 // 'about' intentionally ommitted
219 .picture("https://www.example.com/profile.jpg")
220 .members(
221 vec![
222 "88a14a0df1aa0223e9f3a44cd4964fb82a19590440bb8cf1610d8c7367798314".to_string(),
223 "14c27d59268ae2554d03b89c5c01dac17a604b17ac258ad345bd0648d3f5c011".to_string(),
224 ],
225 vec![
226 tag_group_with_relays(
227 &"109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897".to_string(),
228 &vec!["ws://localhost".to_string()],
229 ),
230 tag_group(
231 &"06bd7667a7c115fd8faf7f300302f39c019e16e6461845930686b84fbeae8c87".to_string(),
232 ),
233 ],
234 )
235 .relays(&vec!["wss://relay.damus.io".to_string(),"ws://localhost".to_string()])
236 .admin(
237 tag_admin_group_with_relays(
238 &"109ca9850488d301147ac92c6ea3e1d3dd3ebe3a59dcd1151e99c7e16ef48897".to_string(),
239 &vec![UncheckedUrl::from_str("ws://localhost").unwrap()],
240 ),
241 )
242 );
243 }
244}
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 @@
1pub mod groups;
2pub mod group;
3pub mod init; \ No newline at end of file