From 6ef5c47e98bd2574a2580aaf97359193d09af121 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 25 Nov 2024 02:15:18 +0100 Subject: [PATCH] feat: assign users to task --- src/kinds.rs | 43 +++++++++++++++++++++++----------------- src/tasks.rs | 12 +++++------ src/tasks/nostr_users.rs | 20 ++++++++++++++++--- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/kinds.rs b/src/kinds.rs index 736160d..fe1edd9 100644 --- a/src/kinds.rs +++ b/src/kinds.rs @@ -1,7 +1,8 @@ use crate::task::MARKER_PARENT; +use crate::tasks::nostr_users::NostrUsers; use crate::tasks::HIGH_PRIO; use itertools::Itertools; -use nostr_sdk::{EventBuilder, EventId, Kind, Tag, TagKind, TagStandard}; +use nostr_sdk::{EventBuilder, EventId, Kind, PublicKey, Tag, TagKind, TagStandard}; use std::borrow::Cow; pub const TASK_KIND: Kind = Kind::GitIssue; @@ -99,22 +100,27 @@ pub(crate) fn extract_hashtags(input: &str) -> impl Iterator + '_ { /// as well as various embedded tags. /// /// Expects sanitized input. -pub(crate) fn extract_tags(input: &str) -> (String, Vec) { +pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec) { let words = input.split_ascii_whitespace(); - let mut prio = None; + let mut tags = Vec::with_capacity(4); let result = words.filter(|s| { - if s.starts_with('*') { - if s.len() == 1 { - prio = Some(HIGH_PRIO); + if s.starts_with('@') { + if let Ok(key) = PublicKey::parse(&s[1..]) { + tags.push(Tag::public_key(key)); + return false; + } else if let Some((key, _)) = users.find_user(&s[1..]) { + tags.push(Tag::public_key(*key)); return false; } - return match s[1..].parse::() { - Ok(num) => { - prio = Some(num * (if s.len() > 2 { 1 } else { 10 })); - false - } - _ => true, - }; + } else if s.starts_with('*') { + if s.len() == 1 { + tags.push(to_prio_tag(HIGH_PRIO)); + return false; + } + if let Ok(num) = s[1..].parse::() { + tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 }))); + return false + } } true }).collect_vec(); @@ -122,7 +128,7 @@ pub(crate) fn extract_tags(input: &str) -> (String, Vec) { let main = split.next().unwrap().join(" "); let mut tags = extract_hashtags(&main) .chain(split.flatten().map(|s| to_hashtag_tag(&s))) - .chain(prio.map(|p| to_prio_tag(p))) + .chain(tags) .collect_vec(); tags.sort(); tags.dedup(); @@ -161,10 +167,11 @@ pub fn to_prio_tag(value: Prio) -> Tag { #[test] fn test_extract_tags() { - assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"), + assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it", &Default::default()), ("Hello from #mars with #greetings #yeah".to_string(), std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()])) - .chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag_tag)).collect())); - assert_eq!(extract_tags("So tagless #"), - ("So tagless".to_string(), vec![])); + .chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag_tag)) + .collect())); + assert_eq!(extract_tags("So tagless @hewo #", &Default::default()), + ("So tagless @hewo".to_string(), vec![])); } \ No newline at end of file diff --git a/src/tasks.rs b/src/tasks.rs index 21551bd..de51531 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,4 +1,4 @@ -mod nostr_users; +pub(crate) mod nostr_users; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::fmt::{Display, Formatter}; @@ -7,18 +7,18 @@ use std::ops::{Div, Rem}; use std::str::FromStr; use std::time::Duration; -use crate::hashtag::Hashtag; use crate::event_sender::{EventSender, MostrMessage}; +use crate::hashtag::Hashtag; use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD}; use crate::kinds::*; use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY}; +use crate::tasks::nostr_users::NostrUsers; use colored::Colorize; use itertools::Itertools; use log::{debug, error, info, trace, warn}; use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp, Url}; use regex::bytes::Regex; use tokio::sync::mpsc::Sender; -use crate::tasks::nostr_users::NostrUsers; const DEFAULT_PRIO: Prio = 25; const QUICK_PRIO: Prio = 35; @@ -1058,7 +1058,7 @@ impl TasksRelay { /// /// Sanitizes input pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator, set_state: bool) -> EventId { - let (input, input_tags) = extract_tags(input.trim()); + let (input, input_tags) = extract_tags(input.trim(), &self.users); let prio = if input_tags.iter().any(|t| t.kind().to_string() == PRIO) { None } else { self.priority.map(|p| to_prio_tag(p)) }; info!("Created task \"{input}\" with tags [{}]", join_tags(&input_tags)); @@ -1282,7 +1282,7 @@ impl TasksRelay { } else { vec![id] }; - let (desc, tags) = extract_tags(comment); + let (desc, tags) = extract_tags(comment, &self.users); let prop = EventBuilder::new(state.into(), desc) .tags(ids.into_iter() @@ -1310,7 +1310,7 @@ impl TasksRelay { /// Creates a note or activity, depending on whether the parent is a task. /// Sanitizes Input. pub(crate) fn make_note(&mut self, note: &str) -> EventId { - let (name, tags) = extract_tags(note.trim()); + let (name, tags) = extract_tags(note.trim(), &self.users); let format = format!("\"{name}\" with tags [{}]", join_tags(&tags)); let mut prop = EventBuilder::new(Kind::TextNote, name) diff --git a/src/tasks/nostr_users.rs b/src/tasks/nostr_users.rs index bbf2f23..90596a6 100644 --- a/src/tasks/nostr_users.rs +++ b/src/tasks/nostr_users.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::str::FromStr; -use nostr_sdk::{Metadata, PublicKey}; +use nostr_sdk::{Keys, Metadata, PublicKey, Tag}; #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct NostrUsers { @@ -15,13 +15,18 @@ impl NostrUsers { // Find username or key starting with the given term. pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> { + let lowered = term.trim().to_ascii_lowercase(); + let term = lowered.as_str(); + if term.is_empty() { + return None + } if let Ok(key) = PublicKey::from_str(term) { return self.users.get_key_value(&key); } self.users.iter().find(|(k, v)| // TODO regex word boundary - v.name.as_ref().is_some_and(|n| n.starts_with(term)) || - v.display_name.as_ref().is_some_and(|n| n.starts_with(term)) || + v.name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) || + v.display_name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) || (term.len() > 4 && k.to_string().starts_with(term))) } @@ -46,4 +51,13 @@ impl NostrUsers { self.users.insert(pubkey, Default::default()); } } +} + +#[test] +fn test_user_extract() { + let keys = Keys::generate(); + let mut users = NostrUsers::default(); + users.insert(keys.public_key, Metadata::new().display_name("Tester Jo")); + assert_eq!(crate::kinds::extract_tags("Hello @test", &users), + ("Hello".to_string(), vec![Tag::public_key(keys.public_key)])); } \ No newline at end of file