2024-08-01 14:07:40 +03:00
|
|
|
use std::cell::RefCell;
|
2024-08-11 12:05:29 +03:00
|
|
|
use std::collections::{HashMap, VecDeque};
|
2024-07-27 22:40:52 +03:00
|
|
|
use std::env::{args, var};
|
2024-07-24 16:03:34 +03:00
|
|
|
use std::fs;
|
2024-07-26 12:58:09 +03:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::{BufRead, BufReader, stdin, stdout, Write};
|
2024-08-12 23:02:50 +03:00
|
|
|
use std::iter::once;
|
2024-08-01 20:00:45 +03:00
|
|
|
use std::ops::Sub;
|
2024-07-27 16:00:43 +03:00
|
|
|
use std::path::PathBuf;
|
2024-07-24 16:03:34 +03:00
|
|
|
use std::str::FromStr;
|
|
|
|
use std::sync::mpsc;
|
2024-08-13 11:54:14 +03:00
|
|
|
use std::sync::mpsc::RecvTimeoutError;
|
2024-08-08 13:52:02 +03:00
|
|
|
use std::sync::mpsc::Sender;
|
2024-08-13 11:54:14 +03:00
|
|
|
use std::time::Duration;
|
2024-08-08 13:52:02 +03:00
|
|
|
|
2024-08-14 15:32:42 +03:00
|
|
|
use colored::{ColoredString, Colorize};
|
2024-08-13 21:24:25 +03:00
|
|
|
use env_logger::Builder;
|
2024-08-08 00:18:34 +03:00
|
|
|
use itertools::Itertools;
|
2024-08-13 21:24:25 +03:00
|
|
|
use log::{debug, error, info, LevelFilter, trace, warn};
|
2024-07-19 01:23:25 +03:00
|
|
|
use nostr_sdk::prelude::*;
|
2024-08-14 19:42:58 +03:00
|
|
|
use nostr_sdk::TagStandard::Hashtag;
|
2024-08-07 15:03:29 +03:00
|
|
|
use regex::Regex;
|
2024-07-27 16:00:43 +03:00
|
|
|
use xdg::BaseDirectories;
|
2024-07-19 01:23:25 +03:00
|
|
|
|
2024-08-06 23:01:59 +03:00
|
|
|
use crate::helpers::*;
|
2024-08-08 00:18:34 +03:00
|
|
|
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
2024-08-12 23:02:50 +03:00
|
|
|
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State};
|
2024-08-11 10:58:34 +03:00
|
|
|
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
2024-07-19 01:23:25 +03:00
|
|
|
|
2024-08-06 23:01:59 +03:00
|
|
|
mod helpers;
|
2024-07-19 01:23:25 +03:00
|
|
|
mod task;
|
|
|
|
mod tasks;
|
2024-08-02 14:43:39 +03:00
|
|
|
mod kinds;
|
2024-07-13 16:00:42 +03:00
|
|
|
|
2024-08-13 21:29:55 +03:00
|
|
|
const UNDO_DELAY: u64 = 60;
|
|
|
|
const INACTVITY_DELAY: u64 = 200;
|
|
|
|
|
2024-08-01 14:07:40 +03:00
|
|
|
type Events = Vec<Event>;
|
|
|
|
|
2024-07-26 21:45:29 +03:00
|
|
|
#[derive(Debug, Clone)]
|
2024-07-24 15:47:24 +03:00
|
|
|
struct EventSender {
|
2024-08-07 15:03:29 +03:00
|
|
|
url: Option<Url>,
|
2024-08-13 21:35:35 +03:00
|
|
|
tx: Sender<MostrMessage>,
|
2024-07-24 15:47:24 +03:00
|
|
|
keys: Keys,
|
2024-08-01 14:07:40 +03:00
|
|
|
queue: RefCell<Events>,
|
2024-07-24 15:47:24 +03:00
|
|
|
}
|
|
|
|
impl EventSender {
|
2024-08-13 21:35:35 +03:00
|
|
|
fn from(url: Option<Url>, tx: &Sender<MostrMessage>, keys: &Keys) -> Self {
|
2024-08-07 15:03:29 +03:00
|
|
|
EventSender {
|
|
|
|
url,
|
|
|
|
tx: tx.clone(),
|
|
|
|
keys: keys.clone(),
|
|
|
|
queue: Default::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-01 20:00:45 +03:00
|
|
|
fn submit(&self, event_builder: EventBuilder) -> Result<Event> {
|
2024-08-02 14:29:52 +03:00
|
|
|
{
|
2024-08-06 17:57:01 +03:00
|
|
|
// Always flush if oldest event older than a minute or newer than now
|
2024-08-02 14:29:52 +03:00
|
|
|
let borrow = self.queue.borrow();
|
2024-08-13 21:29:55 +03:00
|
|
|
let min = Timestamp::now().sub(UNDO_DELAY);
|
2024-08-06 17:57:01 +03:00
|
|
|
if borrow.iter().any(|e| e.created_at < min || e.created_at > Timestamp::now()) {
|
2024-08-02 14:29:52 +03:00
|
|
|
drop(borrow);
|
2024-08-06 17:57:01 +03:00
|
|
|
debug!("Flushing event queue because it is older than a minute");
|
|
|
|
self.force_flush();
|
2024-08-01 20:00:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
let mut queue = self.queue.borrow_mut();
|
|
|
|
Ok(event_builder.to_event(&self.keys).inspect(|event| {
|
2024-08-06 11:34:18 +03:00
|
|
|
if event.kind.as_u16() == TRACKING_KIND {
|
2024-08-01 20:00:45 +03:00
|
|
|
queue.retain(|e| {
|
2024-08-06 11:34:18 +03:00
|
|
|
e.kind.as_u16() != TRACKING_KIND
|
2024-08-01 20:00:45 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
queue.push(event.clone());
|
|
|
|
})?)
|
2024-08-01 14:07:40 +03:00
|
|
|
}
|
2024-08-02 14:29:52 +03:00
|
|
|
/// Sends all pending events
|
|
|
|
fn force_flush(&self) {
|
2024-08-01 20:00:45 +03:00
|
|
|
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
2024-08-07 15:03:29 +03:00
|
|
|
let values = self.clear();
|
|
|
|
self.url.as_ref().map(|url| {
|
2024-08-14 15:32:42 +03:00
|
|
|
self.tx.send(MostrMessage::AddTasks(url.clone(), values)).inspect_err(|e| {
|
|
|
|
error!("Nostr communication thread failure, changes will not be persisted: {}", e)
|
|
|
|
})
|
2024-08-07 15:03:29 +03:00
|
|
|
});
|
2024-08-02 14:29:52 +03:00
|
|
|
}
|
|
|
|
/// Sends all pending events if there is a non-tracking event
|
|
|
|
fn flush(&self) {
|
2024-08-06 11:34:18 +03:00
|
|
|
if self.queue.borrow().iter().any(|event| event.kind.as_u16() != TRACKING_KIND) {
|
2024-08-02 14:29:52 +03:00
|
|
|
self.force_flush()
|
2024-08-01 20:00:45 +03:00
|
|
|
}
|
2024-08-01 14:07:40 +03:00
|
|
|
}
|
|
|
|
fn clear(&self) -> Events {
|
2024-08-01 20:00:45 +03:00
|
|
|
trace!("Cleared queue: {:?}", self.queue.borrow());
|
2024-08-01 14:07:40 +03:00
|
|
|
self.queue.replace(Vec::with_capacity(3))
|
2024-07-24 15:47:24 +03:00
|
|
|
}
|
2024-07-31 20:05:52 +03:00
|
|
|
pub(crate) fn pubkey(&self) -> PublicKey {
|
|
|
|
self.keys.public_key()
|
|
|
|
}
|
2024-07-24 15:47:24 +03:00
|
|
|
}
|
2024-08-01 14:07:40 +03:00
|
|
|
impl Drop for EventSender {
|
|
|
|
fn drop(&mut self) {
|
2024-08-07 15:03:29 +03:00
|
|
|
self.force_flush();
|
|
|
|
debug!("Dropped {:?}", self);
|
2024-08-01 14:07:40 +03:00
|
|
|
}
|
|
|
|
}
|
2024-07-24 15:47:24 +03:00
|
|
|
|
2024-08-13 21:35:35 +03:00
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
|
|
enum MostrMessage {
|
|
|
|
Flush,
|
|
|
|
NewRelay(Url),
|
|
|
|
AddTasks(Url, Vec<Event>),
|
|
|
|
}
|
|
|
|
|
2024-07-09 17:56:08 +03:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() {
|
2024-08-13 21:24:25 +03:00
|
|
|
let mut args = args().skip(1).peekable();
|
|
|
|
if args.peek().is_some_and(|arg| arg == "--debug") {
|
|
|
|
args.next();
|
|
|
|
Builder::new()
|
|
|
|
.filter(None, LevelFilter::Debug)
|
|
|
|
.filter(Some("mostr"), LevelFilter::Trace)
|
|
|
|
.parse_default_env()
|
|
|
|
.init();
|
|
|
|
} else {
|
|
|
|
colog::default_builder()
|
|
|
|
.filter(Some("nostr-relay-pool"), LevelFilter::Error)
|
|
|
|
//.filter(Some("nostr-relay-pool::relay::internal"), LevelFilter::Off)
|
|
|
|
.init();
|
|
|
|
}
|
2024-07-29 21:06:23 +03:00
|
|
|
|
2024-07-27 16:00:43 +03:00
|
|
|
let config_dir = or_print(BaseDirectories::new())
|
|
|
|
.and_then(|d| or_print(d.create_config_directory("mostr")))
|
|
|
|
.unwrap_or(PathBuf::new());
|
|
|
|
let keysfile = config_dir.join("key");
|
|
|
|
let relayfile = config_dir.join("relays");
|
|
|
|
|
|
|
|
let keys = match fs::read_to_string(&keysfile).map(|s| Keys::from_str(&s)) {
|
|
|
|
Ok(Ok(key)) => key,
|
|
|
|
_ => {
|
2024-07-29 21:06:23 +03:00
|
|
|
warn!("Could not read keys from {}", keysfile.to_string_lossy());
|
2024-07-27 16:00:43 +03:00
|
|
|
let keys = prompt("Secret Key?")
|
|
|
|
.and_then(|s| or_print(Keys::from_str(&s)))
|
|
|
|
.unwrap_or_else(|| Keys::generate());
|
|
|
|
or_print(fs::write(&keysfile, keys.secret_key().unwrap().to_string()));
|
|
|
|
keys
|
|
|
|
}
|
|
|
|
};
|
2024-07-09 17:56:08 +03:00
|
|
|
|
2024-07-27 16:00:43 +03:00
|
|
|
let client = Client::new(&keys);
|
2024-07-29 21:06:23 +03:00
|
|
|
info!("My public key: {}", keys.public_key());
|
2024-08-07 15:03:29 +03:00
|
|
|
|
2024-08-14 15:32:42 +03:00
|
|
|
// TODO use NewRelay message for all relays
|
2024-07-27 22:40:52 +03:00
|
|
|
match var("MOSTR_RELAY") {
|
|
|
|
Ok(relay) => {
|
|
|
|
or_print(client.add_relay(relay).await);
|
2024-07-26 12:58:09 +03:00
|
|
|
}
|
2024-07-29 09:59:17 +03:00
|
|
|
_ => match File::open(&relayfile).map(|f| BufReader::new(f).lines().flatten()) {
|
|
|
|
Ok(lines) => {
|
|
|
|
for line in lines {
|
|
|
|
or_print(client.add_relay(line).await);
|
2024-07-27 22:40:52 +03:00
|
|
|
}
|
2024-07-29 09:59:17 +03:00
|
|
|
}
|
|
|
|
Err(e) => {
|
2024-07-29 21:06:23 +03:00
|
|
|
warn!("Could not read relays file: {}", e);
|
2024-07-29 09:59:17 +03:00
|
|
|
if let Some(line) = prompt("Relay?") {
|
|
|
|
let url = if line.contains("://") {
|
|
|
|
line
|
|
|
|
} else {
|
|
|
|
"wss://".to_string() + &line
|
2024-07-27 22:40:52 +03:00
|
|
|
};
|
2024-07-29 09:59:17 +03:00
|
|
|
or_print(client.add_relay(url.clone()).await).map(|bool| {
|
|
|
|
if bool {
|
|
|
|
or_print(fs::write(&relayfile, url));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2024-07-27 22:40:52 +03:00
|
|
|
}
|
2024-07-29 09:59:17 +03:00
|
|
|
},
|
2024-07-26 12:58:09 +03:00
|
|
|
}
|
2024-07-27 16:00:43 +03:00
|
|
|
|
2024-08-07 15:03:29 +03:00
|
|
|
let sub_id = client.subscribe(vec![Filter::new().kinds(KINDS.into_iter().map(|k| Kind::from(k)))], None).await;
|
|
|
|
info!("Subscribed with {:?}", sub_id);
|
|
|
|
|
2024-07-27 16:00:43 +03:00
|
|
|
//let proxy = Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9050)));
|
2024-07-23 13:27:36 +03:00
|
|
|
//client
|
2024-07-09 17:56:08 +03:00
|
|
|
// .add_relay_with_opts(
|
|
|
|
// "wss://relay.nostr.info",
|
|
|
|
// RelayOptions::new().proxy(proxy).flags(RelayServiceFlags::default().remove(RelayServiceFlags::WRITE)),
|
|
|
|
// )
|
|
|
|
// .await?;
|
2024-07-23 13:27:36 +03:00
|
|
|
//client
|
2024-07-09 17:56:08 +03:00
|
|
|
// .add_relay_with_opts(
|
|
|
|
// "ws://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion",
|
|
|
|
// RelayOptions::new().proxy(proxy),
|
|
|
|
// )
|
|
|
|
// .await?;
|
|
|
|
|
|
|
|
//let metadata = Metadata::new()
|
|
|
|
// .name("username")
|
|
|
|
// .display_name("My Username")
|
|
|
|
// .about("Description")
|
|
|
|
// .picture(Url::parse("https://example.com/avatar.png")?)
|
|
|
|
// .banner(Url::parse("https://example.com/banner.png")?)
|
|
|
|
// .nip05("username@example.com")
|
|
|
|
// .lud16("yuki@getalby.com")
|
|
|
|
// .custom_field("custom_field", "my value");
|
|
|
|
|
2024-07-23 13:27:36 +03:00
|
|
|
//client.set_metadata(&metadata).await?;
|
2024-07-09 17:56:08 +03:00
|
|
|
|
2024-07-23 13:27:36 +03:00
|
|
|
client.connect().await;
|
2024-08-07 15:03:29 +03:00
|
|
|
let mut notifications = client.notifications();
|
2024-07-09 17:56:08 +03:00
|
|
|
|
2024-08-13 21:35:35 +03:00
|
|
|
let (tx, rx) = mpsc::channel::<MostrMessage>();
|
2024-08-07 15:03:29 +03:00
|
|
|
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys);
|
|
|
|
let mut relays: HashMap<Url, Tasks> =
|
|
|
|
client.relays().await.into_keys().map(|url| (url.clone(), tasks_for_url(Some(url)))).collect();
|
2024-07-19 21:04:21 +03:00
|
|
|
|
2024-07-24 16:03:34 +03:00
|
|
|
/*println!("Finding existing events");
|
|
|
|
let _ = client
|
|
|
|
.get_events_of(vec![Filter::new()], Some(Duration::from_secs(5)))
|
2024-07-19 16:49:23 +03:00
|
|
|
.map_ok(|res| {
|
|
|
|
println!("Found {} events", res.len());
|
2024-07-24 16:03:34 +03:00
|
|
|
let (mut task_events, props): (Vec<Event>, Vec<Event>) =
|
|
|
|
res.into_iter().partition(|e| e.kind.as_u32() == 1621);
|
2024-07-19 16:49:23 +03:00
|
|
|
task_events.sort_unstable();
|
|
|
|
for event in task_events {
|
2024-07-19 21:04:21 +03:00
|
|
|
print_event(&event);
|
2024-07-19 16:49:23 +03:00
|
|
|
tasks.add_task(event);
|
|
|
|
}
|
|
|
|
for event in props {
|
2024-07-19 21:04:21 +03:00
|
|
|
print_event(&event);
|
|
|
|
tasks.add_prop(&event);
|
2024-07-19 16:49:23 +03:00
|
|
|
}
|
|
|
|
})
|
2024-07-24 16:03:34 +03:00
|
|
|
.await;*/
|
2024-07-19 16:49:23 +03:00
|
|
|
|
2024-07-24 16:03:34 +03:00
|
|
|
let sender = tokio::spawn(async move {
|
2024-08-13 11:54:14 +03:00
|
|
|
let mut queue: Option<(Url, Vec<Event>)> = None;
|
|
|
|
|
|
|
|
loop {
|
2024-08-14 15:32:42 +03:00
|
|
|
let result = rx.recv_timeout(Duration::from_secs(INACTVITY_DELAY));
|
|
|
|
match result {
|
|
|
|
Ok(MostrMessage::NewRelay(url)) => {
|
|
|
|
if client.add_relay(&url).await.unwrap() {
|
|
|
|
match client.connect_relay(&url).await {
|
|
|
|
Ok(()) => info!("Connected to {url}"),
|
|
|
|
Err(e) => warn!("Unable to connect to relay {url}: {e}")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
warn!("Relay {url} already added");
|
2024-08-13 11:54:14 +03:00
|
|
|
}
|
2024-08-14 15:32:42 +03:00
|
|
|
}
|
|
|
|
Ok(MostrMessage::AddTasks(url, mut events)) => {
|
|
|
|
trace!("Queueing {:?}", &events);
|
2024-08-13 11:54:14 +03:00
|
|
|
if let Some((queue_url, mut queue_events)) = queue {
|
|
|
|
if queue_url == url {
|
|
|
|
queue_events.append(&mut events);
|
|
|
|
queue = Some((queue_url, queue_events));
|
|
|
|
} else {
|
2024-08-14 15:32:42 +03:00
|
|
|
info!("Sending {} events to {url} due to relay change", queue_events.len());
|
2024-08-13 11:54:14 +03:00
|
|
|
client.batch_event_to(vec![queue_url], queue_events, RelaySendOptions::new()).await;
|
|
|
|
queue = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if queue.is_none() {
|
|
|
|
events.reserve(events.len() + 10);
|
|
|
|
queue = Some((url, events))
|
|
|
|
}
|
|
|
|
}
|
2024-08-14 15:32:42 +03:00
|
|
|
Ok(MostrMessage::Flush) | Err(RecvTimeoutError::Timeout) => if let Some((url, events)) = queue {
|
|
|
|
info!("Sending {} events to {url} due to {:?}", events.len(), result);
|
2024-08-13 11:54:14 +03:00
|
|
|
client.batch_event_to(vec![url], events, RelaySendOptions::new()).await;
|
|
|
|
queue = None;
|
|
|
|
}
|
2024-08-14 15:32:42 +03:00
|
|
|
Err(err) => {
|
|
|
|
debug!("Finalizing nostr communication thread because of {:?}", err);
|
|
|
|
break;
|
2024-08-13 21:35:35 +03:00
|
|
|
}
|
2024-08-13 11:54:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some((url, events)) = queue {
|
2024-08-14 15:32:42 +03:00
|
|
|
info!("Sending {} events to {url} before exiting", events.len());
|
2024-08-13 11:54:14 +03:00
|
|
|
client.batch_event_to(vec![url], events, RelaySendOptions::new()).await;
|
2024-07-24 16:03:34 +03:00
|
|
|
}
|
2024-08-13 11:54:14 +03:00
|
|
|
info!("Shutting down nostr communication thread");
|
2024-07-24 16:03:34 +03:00
|
|
|
});
|
2024-08-07 15:03:29 +03:00
|
|
|
|
|
|
|
let mut local_tasks = Tasks::from(None, &tx, &keys);
|
|
|
|
let mut selected_relay: Option<Url> = relays.keys().nth(0).cloned();
|
|
|
|
|
|
|
|
{
|
|
|
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
2024-08-13 21:24:25 +03:00
|
|
|
for argument in args {
|
2024-08-07 15:03:29 +03:00
|
|
|
tasks.make_task(&argument);
|
|
|
|
}
|
2024-07-24 16:03:34 +03:00
|
|
|
}
|
2024-07-19 16:49:23 +03:00
|
|
|
|
2024-07-26 21:45:29 +03:00
|
|
|
let mut lines = stdin().lines();
|
2024-07-13 16:00:42 +03:00
|
|
|
loop {
|
2024-08-14 15:56:40 +03:00
|
|
|
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
|
|
|
|
format!("{}: [{}]", url, tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
|
2024-08-08 00:18:34 +03:00
|
|
|
println!();
|
2024-08-14 15:33:50 +03:00
|
|
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get(url)).unwrap_or(&local_tasks);
|
|
|
|
print!(
|
2024-08-15 09:31:49 +03:00
|
|
|
"{} {}{}) ",
|
|
|
|
selected_relay.as_ref().map_or("TEMP".to_string(), |url| url.to_string()).bright_black(),
|
|
|
|
tasks.get_task_path(tasks.get_position()).bold(),
|
|
|
|
tasks.get_prompt_suffix().italic(),
|
2024-08-14 15:33:50 +03:00
|
|
|
);
|
2024-07-13 16:00:42 +03:00
|
|
|
stdout().flush().unwrap();
|
2024-07-26 21:45:29 +03:00
|
|
|
match lines.next() {
|
2024-07-13 16:00:42 +03:00
|
|
|
Some(Ok(input)) => {
|
2024-07-29 21:06:23 +03:00
|
|
|
let mut count = 0;
|
2024-07-26 11:07:47 +03:00
|
|
|
while let Ok(notification) = notifications.try_recv() {
|
|
|
|
if let RelayPoolNotification::Event {
|
2024-08-07 15:03:29 +03:00
|
|
|
relay_url,
|
2024-07-26 11:07:47 +03:00
|
|
|
event,
|
|
|
|
..
|
|
|
|
} = notification
|
|
|
|
{
|
2024-08-14 15:56:40 +03:00
|
|
|
debug!(
|
|
|
|
"At {} found {} kind {} content \"{}\" tags {:?}",
|
|
|
|
event.created_at, event.id, event.kind, event.content, event.tags.iter().map(|tag| tag.as_vec()).collect_vec()
|
|
|
|
);
|
2024-08-07 15:03:29 +03:00
|
|
|
match relays.get_mut(&relay_url) {
|
|
|
|
Some(tasks) => tasks.add(*event),
|
2024-08-14 15:56:40 +03:00
|
|
|
None => warn!("Event received from unknown relay {relay_url}: {:?}", *event)
|
2024-08-07 15:03:29 +03:00
|
|
|
}
|
2024-07-29 21:06:23 +03:00
|
|
|
count += 1;
|
2024-07-26 11:07:47 +03:00
|
|
|
}
|
|
|
|
}
|
2024-07-30 09:02:56 +03:00
|
|
|
if count > 0 {
|
2024-08-07 15:03:29 +03:00
|
|
|
info!("Received {count} Updates");
|
2024-07-29 21:06:23 +03:00
|
|
|
}
|
2024-07-26 11:07:47 +03:00
|
|
|
|
2024-07-18 19:10:52 +03:00
|
|
|
let mut iter = input.chars();
|
2024-07-18 20:48:51 +03:00
|
|
|
let op = iter.next();
|
2024-07-29 09:59:17 +03:00
|
|
|
let arg = if input.len() > 1 {
|
2024-08-08 00:18:34 +03:00
|
|
|
Some(input[1..].trim())
|
2024-07-29 09:59:17 +03:00
|
|
|
} else {
|
2024-08-08 00:18:34 +03:00
|
|
|
None
|
2024-07-29 09:59:17 +03:00
|
|
|
};
|
2024-08-08 00:18:34 +03:00
|
|
|
let arg_default = arg.unwrap_or("");
|
2024-08-07 15:03:29 +03:00
|
|
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
2024-07-18 20:48:51 +03:00
|
|
|
match op {
|
2024-08-01 14:07:40 +03:00
|
|
|
None => {
|
2024-08-02 14:29:52 +03:00
|
|
|
debug!("Flushing Tasks because of empty command");
|
2024-08-01 14:07:40 +03:00
|
|
|
tasks.flush()
|
|
|
|
}
|
2024-07-18 20:48:51 +03:00
|
|
|
|
2024-08-11 10:58:34 +03:00
|
|
|
Some(':') => {
|
|
|
|
let next = iter.next();
|
|
|
|
if let Some(':') = next {
|
2024-08-11 12:05:29 +03:00
|
|
|
let str: String = iter.collect();
|
|
|
|
let result = str.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
|
|
|
|
if result.len() == 1 {
|
|
|
|
tasks.add_sorting_property(str.trim().to_string())
|
|
|
|
} else {
|
|
|
|
tasks.set_sorting(result)
|
|
|
|
}
|
2024-08-11 10:58:34 +03:00
|
|
|
} else if let Some(digit) = next.and_then(|s| s.to_digit(10)) {
|
2024-08-07 00:06:09 +03:00
|
|
|
let index = (digit as usize).saturating_sub(1);
|
2024-07-29 09:59:17 +03:00
|
|
|
let remaining = iter.collect::<String>().trim().to_string();
|
|
|
|
if remaining.is_empty() {
|
2024-08-11 10:58:34 +03:00
|
|
|
tasks.get_columns().remove_at(index);
|
2024-08-08 00:18:34 +03:00
|
|
|
} else {
|
2024-08-11 10:58:34 +03:00
|
|
|
tasks.get_columns().add_or_remove_at(remaining, index);
|
2024-07-30 21:19:41 +03:00
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
} else if let Some(arg) = arg {
|
2024-08-11 10:58:34 +03:00
|
|
|
tasks.get_columns().add_or_remove(arg.to_string());
|
2024-08-08 00:18:34 +03:00
|
|
|
} else {
|
|
|
|
println!("{}", PROPERTY_COLUMNS);
|
2024-08-08 13:52:02 +03:00
|
|
|
continue;
|
2024-08-11 10:58:34 +03:00
|
|
|
}
|
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
Some(',') =>
|
2024-08-08 00:18:34 +03:00
|
|
|
match arg {
|
|
|
|
None => {
|
|
|
|
tasks.get_current_task().map_or_else(
|
|
|
|
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|
2024-08-14 21:49:36 +03:00
|
|
|
|task| println!("{}", task.description_events().map(|e| format!("{} {}", local_datetimestamp(&e.created_at), e.content)).join("\n")),
|
2024-08-08 00:18:34 +03:00
|
|
|
);
|
2024-08-08 13:52:02 +03:00
|
|
|
continue;
|
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
Some(arg) => tasks.make_note(arg),
|
2024-07-18 19:10:52 +03:00
|
|
|
}
|
2024-07-18 20:48:51 +03:00
|
|
|
|
2024-07-29 11:15:13 +03:00
|
|
|
Some('>') => {
|
2024-08-08 00:18:34 +03:00
|
|
|
tasks.update_state(&arg_default, State::Done);
|
2024-07-29 11:15:13 +03:00
|
|
|
tasks.move_up();
|
|
|
|
}
|
|
|
|
|
|
|
|
Some('<') => {
|
2024-08-08 00:18:34 +03:00
|
|
|
tasks.update_state(&arg_default, State::Closed);
|
2024-07-29 11:15:13 +03:00
|
|
|
tasks.move_up();
|
2024-07-18 20:48:51 +03:00
|
|
|
}
|
2024-07-26 11:07:47 +03:00
|
|
|
|
2024-08-14 15:59:43 +03:00
|
|
|
Some('@') | Some('&') => {
|
2024-08-01 14:07:40 +03:00
|
|
|
tasks.undo();
|
|
|
|
}
|
|
|
|
|
2024-08-08 21:10:17 +03:00
|
|
|
Some('|') =>
|
|
|
|
match arg {
|
|
|
|
None => match tasks.get_position() {
|
|
|
|
None => {
|
2024-08-15 10:33:52 +03:00
|
|
|
info!("Filtering for Procedures");
|
2024-08-08 21:10:17 +03:00
|
|
|
tasks.set_filter(
|
2024-08-15 10:16:40 +03:00
|
|
|
tasks.filtered_tasks(None)
|
2024-08-08 21:10:17 +03:00
|
|
|
.filter(|t| t.pure_state() == State::Procedure)
|
|
|
|
.map(|t| t.event.id)
|
|
|
|
.collect()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Some(id) => {
|
|
|
|
tasks.set_state_for(id, "", State::Procedure);
|
|
|
|
}
|
|
|
|
},
|
2024-08-12 23:02:50 +03:00
|
|
|
Some(arg) => 'arm: {
|
|
|
|
if arg.chars().next() != Some('|') {
|
|
|
|
if let Some(pos) = tasks.get_position() {
|
|
|
|
tasks.move_up();
|
|
|
|
tasks.make_task_with(
|
|
|
|
arg,
|
|
|
|
once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS))
|
|
|
|
.chain(tasks.parent_tag()),
|
|
|
|
true);
|
|
|
|
break 'arm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let arg: String = arg.chars().skip_while(|c| c == &'|').collect();
|
|
|
|
tasks.make_task_and_enter(&arg, State::Procedure);
|
2024-08-08 21:10:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-01 14:07:40 +03:00
|
|
|
Some('?') => {
|
2024-08-10 15:44:52 +03:00
|
|
|
match arg {
|
|
|
|
None => tasks.set_state_filter(StateFilter::Default),
|
|
|
|
Some("?") => tasks.set_state_filter(StateFilter::All),
|
|
|
|
Some(arg) => tasks.set_state_filter(StateFilter::State(arg.to_string())),
|
|
|
|
}
|
2024-08-01 14:07:40 +03:00
|
|
|
}
|
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
Some('!') =>
|
|
|
|
match tasks.get_position() {
|
|
|
|
None => warn!("First select a task to set its state!"),
|
|
|
|
Some(id) => {
|
|
|
|
tasks.set_state_for_with(id, arg_default);
|
2024-08-12 11:57:21 +03:00
|
|
|
tasks.move_up();
|
2024-08-08 18:16:25 +03:00
|
|
|
}
|
2024-07-29 11:15:13 +03:00
|
|
|
}
|
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
Some('#') =>
|
2024-08-08 13:04:22 +03:00
|
|
|
match arg {
|
2024-08-14 19:42:58 +03:00
|
|
|
Some(arg) => tasks.set_tags(arg.split_whitespace().map(|s| Hashtag(s.to_string()).into())),
|
2024-08-08 13:04:22 +03:00
|
|
|
None => {
|
|
|
|
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" "));
|
2024-08-08 13:52:02 +03:00
|
|
|
continue;
|
2024-08-08 13:04:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
Some('+') =>
|
2024-08-08 00:18:34 +03:00
|
|
|
match arg {
|
|
|
|
Some(arg) => tasks.add_tag(arg.to_string()),
|
|
|
|
None => tasks.clear_filter()
|
|
|
|
}
|
2024-08-01 21:11:33 +03:00
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
Some('-') =>
|
2024-08-08 00:18:34 +03:00
|
|
|
match arg {
|
|
|
|
Some(arg) => tasks.remove_tag(arg),
|
|
|
|
None => tasks.clear_filter()
|
|
|
|
}
|
2024-08-02 20:40:42 +03:00
|
|
|
|
2024-08-14 15:36:54 +03:00
|
|
|
Some('(') => {
|
|
|
|
if let Some(arg) = arg {
|
|
|
|
if !tasks.track_from(arg) {
|
2024-08-08 18:16:25 +03:00
|
|
|
continue;
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
2024-08-10 20:48:57 +03:00
|
|
|
}
|
2024-08-14 15:36:54 +03:00
|
|
|
println!("{}", tasks.times_tracked());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-08-10 20:48:57 +03:00
|
|
|
Some(')') => {
|
|
|
|
tasks.move_to(None);
|
|
|
|
if let Some(arg) = arg {
|
|
|
|
if !tasks.track_from(arg) {
|
|
|
|
continue;
|
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
2024-08-10 20:48:57 +03:00
|
|
|
}
|
2024-07-18 20:48:51 +03:00
|
|
|
|
2024-07-18 19:10:52 +03:00
|
|
|
Some('.') => {
|
|
|
|
let mut dots = 1;
|
2024-07-19 01:15:11 +03:00
|
|
|
let mut pos = tasks.get_position();
|
2024-07-18 19:10:52 +03:00
|
|
|
for _ in iter.take_while(|c| c == &'.') {
|
|
|
|
dots += 1;
|
2024-08-01 14:07:40 +03:00
|
|
|
pos = tasks.get_parent(pos).cloned();
|
2024-07-18 19:10:52 +03:00
|
|
|
}
|
2024-08-10 18:10:40 +03:00
|
|
|
|
2024-08-15 10:16:40 +03:00
|
|
|
let slice = input[dots..].trim();
|
2024-08-13 21:31:06 +03:00
|
|
|
if pos != tasks.get_position() || slice.is_empty() {
|
|
|
|
tasks.move_to(pos);
|
|
|
|
}
|
2024-07-25 00:26:29 +03:00
|
|
|
if slice.is_empty() {
|
2024-08-08 13:52:02 +03:00
|
|
|
if dots > 1 {
|
2024-08-08 00:18:34 +03:00
|
|
|
info!("Moving up {} tasks", dots - 1)
|
|
|
|
}
|
|
|
|
} else if let Ok(depth) = slice.parse::<i8>() {
|
2024-08-07 00:06:09 +03:00
|
|
|
tasks.set_depth(depth);
|
2024-08-01 21:11:33 +03:00
|
|
|
} else {
|
|
|
|
tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
|
2024-07-18 21:43:35 +03:00
|
|
|
}
|
2024-07-18 19:10:52 +03:00
|
|
|
}
|
2024-07-18 20:48:51 +03:00
|
|
|
|
2024-08-01 21:40:15 +03:00
|
|
|
Some('/') => {
|
|
|
|
let mut dots = 1;
|
|
|
|
let mut pos = tasks.get_position();
|
|
|
|
for _ in iter.take_while(|c| c == &'/') {
|
|
|
|
dots += 1;
|
|
|
|
pos = tasks.get_parent(pos).cloned();
|
|
|
|
}
|
2024-08-10 18:10:40 +03:00
|
|
|
|
2024-08-15 10:16:40 +03:00
|
|
|
let slice = input[dots..].trim();
|
2024-08-01 21:40:15 +03:00
|
|
|
if slice.is_empty() {
|
|
|
|
tasks.move_to(pos);
|
2024-08-15 10:16:40 +03:00
|
|
|
if dots > 1 {
|
|
|
|
info!("Moving up {} tasks", dots - 1)
|
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
} else if let Ok(depth) = slice.parse::<i8>() {
|
2024-08-07 00:06:09 +03:00
|
|
|
tasks.set_depth(depth);
|
2024-08-01 21:40:15 +03:00
|
|
|
} else {
|
2024-08-15 10:16:40 +03:00
|
|
|
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
|
|
|
|
if slice.chars().find(|c| c.is_ascii_uppercase()).is_none() {
|
|
|
|
// Smart-case - case-sensitive if any uppercase char is entered
|
|
|
|
transform = Box::new(|s| s.to_ascii_lowercase());
|
|
|
|
}
|
|
|
|
|
|
|
|
let filtered = tasks.filtered_tasks(pos)
|
|
|
|
.filter(|t| {
|
|
|
|
transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any(|tag|
|
|
|
|
tag.content().is_some_and(|s| transform(s).contains(slice))
|
|
|
|
)
|
|
|
|
})
|
2024-08-01 21:40:15 +03:00
|
|
|
.map(|t| t.event.id)
|
2024-08-15 10:16:40 +03:00
|
|
|
.collect_vec();
|
2024-08-01 21:40:15 +03:00
|
|
|
if filtered.len() == 1 {
|
|
|
|
tasks.move_to(filtered.into_iter().nth(0));
|
|
|
|
} else {
|
|
|
|
tasks.move_to(pos);
|
|
|
|
tasks.set_filter(filtered);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-08 18:16:25 +03:00
|
|
|
_ =>
|
2024-08-10 18:10:40 +03:00
|
|
|
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
|
2024-08-07 15:03:29 +03:00
|
|
|
tasks.move_to(None);
|
2024-08-14 15:32:42 +03:00
|
|
|
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_str().starts_with(&input)) {
|
|
|
|
selected_relay = Some(url.clone());
|
|
|
|
or_print(tasks.print_tasks());
|
2024-08-14 15:40:15 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
match Url::parse(&input) {
|
|
|
|
Err(e) => warn!("Failed to parse url \"{input}\": {}", e),
|
|
|
|
Ok(url) => match tx.send(MostrMessage::NewRelay(url.clone())) {
|
2024-08-14 15:32:42 +03:00
|
|
|
Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"),
|
|
|
|
Ok(_) => {
|
|
|
|
info!("Connecting to {url}");
|
|
|
|
selected_relay = Some(url.clone());
|
|
|
|
relays.insert(url.clone(), tasks_for_url(Some(url)));
|
|
|
|
}
|
2024-08-07 15:03:29 +03:00
|
|
|
}
|
|
|
|
}
|
2024-08-08 13:52:02 +03:00
|
|
|
continue;
|
2024-08-07 15:03:29 +03:00
|
|
|
} else {
|
|
|
|
tasks.filter_or_create(&input);
|
|
|
|
}
|
2024-07-17 22:55:25 +03:00
|
|
|
}
|
2024-08-08 00:18:34 +03:00
|
|
|
or_print(tasks.print_tasks());
|
2024-07-13 16:00:42 +03:00
|
|
|
}
|
2024-07-29 21:06:23 +03:00
|
|
|
Some(Err(e)) => warn!("{}", e),
|
2024-07-18 18:25:09 +03:00
|
|
|
None => break,
|
2024-07-13 16:00:42 +03:00
|
|
|
}
|
|
|
|
}
|
2024-07-24 19:11:08 +03:00
|
|
|
println!();
|
2024-07-18 20:48:51 +03:00
|
|
|
|
2024-08-07 15:03:29 +03:00
|
|
|
drop(tx);
|
|
|
|
drop(local_tasks);
|
|
|
|
drop(relays);
|
2024-07-23 13:27:36 +03:00
|
|
|
|
2024-08-07 15:03:29 +03:00
|
|
|
info!("Submitting pending updates...");
|
2024-07-24 16:03:34 +03:00
|
|
|
or_print(sender.await);
|
2024-07-23 13:27:36 +03:00
|
|
|
}
|