Compare commits
14 commits
ae4a678d77
...
d6d9a876a3
Author | SHA1 | Date | |
---|---|---|---|
|
d6d9a876a3 | ||
|
eb1bafad2d | ||
|
828114f5de | ||
|
2e76250edc | ||
|
86010962a2 | ||
|
fb3a479147 | ||
|
dcc7778815 | ||
|
9ea491a301 | ||
|
50503f7f66 | ||
|
984e4f129d | ||
|
ee33086824 | ||
|
a1347def62 | ||
|
4769c12336 | ||
|
1b065c434f |
7 changed files with 934 additions and 474 deletions
1021
Cargo.lock
generated
1021
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
|
|||
readme = "README.md"
|
||||
license = "GPL 3.0"
|
||||
authors = ["melonion"]
|
||||
version = "0.7.1"
|
||||
version = "0.9.0"
|
||||
rust-version = "1.82"
|
||||
edition = "2021"
|
||||
default-run = "mostr"
|
||||
|
@ -13,26 +13,28 @@ default-run = "mostr"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nostr-sdk = "0.38"
|
||||
# Basics
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
regex = "1.10.6"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
regex = "1.11"
|
||||
# System
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
colog = "1.3"
|
||||
colored = "2.1"
|
||||
rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" }
|
||||
colored = "2.2"
|
||||
rustyline = { git = "https://github.com/xeruf/rustyline", rev = "5364854" }
|
||||
# OS-Specific Abstractions
|
||||
keyring = "3"
|
||||
directories = "5.0"
|
||||
whoami = "1.5"
|
||||
# slint = "1.8"
|
||||
# Application Utils
|
||||
base64 = "0.22"
|
||||
simple_crypt = "0.2"
|
||||
itertools = "0.12"
|
||||
chrono = "0.4"
|
||||
parse_datetime = "0.5.0"
|
||||
parse_datetime = "0.5"
|
||||
interim = { version = "0.1", features = ["chrono"] }
|
||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "e82bc787bdd8490ceadb034fe4483e4df1e91b2a" }
|
||||
|
||||
[dev-dependencies]
|
||||
mostr = { path = ".", default-features = false }
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "1.82.0"
|
||||
channel = "1.84.0"
|
||||
|
|
|
@ -13,22 +13,21 @@ const UNDO_DELAY: u64 = 60;
|
|||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum MostrMessage {
|
||||
Flush,
|
||||
NewRelay(Url),
|
||||
AddTasks(Url, Vec<Event>),
|
||||
NewRelay(RelayUrl),
|
||||
SendTask(RelayUrl, Event),
|
||||
}
|
||||
|
||||
type Events = Vec<Event>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct EventSender {
|
||||
pub(crate) url: Option<Url>,
|
||||
pub(crate) url: Option<RelayUrl>,
|
||||
pub(crate) tx: Sender<MostrMessage>,
|
||||
pub(crate) keys: Keys,
|
||||
pub(crate) queue: RefCell<Events>,
|
||||
}
|
||||
impl EventSender {
|
||||
pub(crate) fn from(url: Option<Url>, tx: &Sender<MostrMessage>, keys: &Keys) -> Self {
|
||||
pub(crate) fn from(url: Option<RelayUrl>, tx: &Sender<MostrMessage>, keys: &Keys) -> Self {
|
||||
EventSender {
|
||||
url,
|
||||
tx: tx.clone(),
|
||||
|
@ -39,45 +38,38 @@ impl EventSender {
|
|||
|
||||
// TODO this direly needs testing
|
||||
pub(crate) fn submit(&self, event_builder: EventBuilder) -> Result<Event> {
|
||||
let min = Timestamp::now().sub(UNDO_DELAY);
|
||||
let event = event_builder.sign_with_keys(&self.keys)?;
|
||||
|
||||
let time = event.created_at;
|
||||
{
|
||||
// Always flush if oldest event older than a minute or newer than now
|
||||
// Always flush if any event is newer or more than a minute older than the current event
|
||||
let borrow = self.queue.borrow();
|
||||
if borrow
|
||||
.iter()
|
||||
.any(|e| e.created_at < min || e.created_at > Timestamp::now())
|
||||
.any(|e| e.created_at < time.sub(UNDO_DELAY) || e.created_at > time)
|
||||
{
|
||||
drop(borrow);
|
||||
debug!("Flushing event queue because it is older than a minute");
|
||||
debug!("Flushing event queue because it is offset from the current event");
|
||||
self.force_flush();
|
||||
}
|
||||
}
|
||||
|
||||
let mut queue = self.queue.borrow_mut();
|
||||
Ok(event_builder.sign_with_keys(&self.keys).inspect(|event| {
|
||||
if event.kind == TRACKING_KIND
|
||||
&& event.created_at > min
|
||||
&& event.created_at < tasks::now()
|
||||
{
|
||||
// Do not send redundant movements
|
||||
queue.retain(|e| e.kind != TRACKING_KIND);
|
||||
}
|
||||
queue.push(event.clone());
|
||||
})?)
|
||||
if event.kind == TRACKING_KIND {
|
||||
// Remove extraneous movements if tracking event is not at a custom time
|
||||
queue.retain(|e| e.kind != TRACKING_KIND);
|
||||
}
|
||||
queue.push(event.clone());
|
||||
Ok(event)
|
||||
}
|
||||
/// Sends all pending events
|
||||
pub(crate) fn force_flush(&self) {
|
||||
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
||||
let values = self.clear();
|
||||
self.url.as_ref().map(|url| {
|
||||
self.tx
|
||||
.try_send(MostrMessage::AddTasks(url.clone(), values))
|
||||
.err()
|
||||
.map(|e| {
|
||||
error!(
|
||||
"Nostr communication thread failure, changes will not be persisted: {}",
|
||||
e
|
||||
)
|
||||
})
|
||||
values.into_iter()
|
||||
.find_map(|event| self.tx.try_send(MostrMessage::SendTask(url.clone(), event)).err())
|
||||
.map(|e| error!("Nostr communication thread failure, changes will not be persisted: {}", e))
|
||||
});
|
||||
}
|
||||
/// Sends all pending events if there is a non-tracking event
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::ops::Sub;
|
||||
|
||||
use chrono::LocalResult::Single;
|
||||
use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
@ -34,16 +32,24 @@ impl<T: TimeZone> ToTimestamp for DateTime<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses the hour from a plain number in the String,
|
||||
|
||||
/// Parses the hour optionally with minute from a plain number in a String,
|
||||
/// with max of max_future hours into the future.
|
||||
// TODO parse HHMM as well
|
||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
||||
str.parse::<u32>().ok().and_then(|hour| {
|
||||
let now = Local::now();
|
||||
parse_hour_after(str, Local::now() - TimeDelta::hours(24 - max_future))
|
||||
}
|
||||
|
||||
/// Parses the hour optionally with minute from a plain number in a String.
|
||||
pub fn parse_hour_after<T: TimeZone>(str: &str, after: DateTime<T>) -> Option<DateTime<T>> {
|
||||
str.parse::<u32>().ok().and_then(|number| {
|
||||
#[allow(deprecated)]
|
||||
now.date().and_hms_opt(hour, 0, 0).map(|time| {
|
||||
if time - now > TimeDelta::hours(max_future) {
|
||||
time.sub(TimeDelta::days(1))
|
||||
after.date().and_hms_opt(
|
||||
if str.len() > 2 { number / 100 } else { number },
|
||||
if str.len() > 2 { number % 100 } else { 0 },
|
||||
0,
|
||||
).map(|time| {
|
||||
if time < after {
|
||||
time + TimeDelta::days(1)
|
||||
} else {
|
||||
time
|
||||
}
|
||||
|
@ -52,11 +58,15 @@ pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
|||
}
|
||||
|
||||
pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
|
||||
parse_date_with_ref(str, Local::now())
|
||||
}
|
||||
|
||||
pub fn parse_date_with_ref(str: &str, reference: DateTime<Local>) -> Option<DateTime<Utc>> {
|
||||
// Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84
|
||||
match interim::parse_date_string(str, Local::now(), interim::Dialect::Us) {
|
||||
match interim::parse_date_string(str, reference, interim::Dialect::Us) {
|
||||
Ok(date) => Some(date.to_utc()),
|
||||
Err(e) => {
|
||||
match parse_datetime::parse_datetime_at_date(Local::now(), str) {
|
||||
match parse_datetime::parse_datetime_at_date(reference, str) {
|
||||
Ok(date) => Some(date.to_utc()),
|
||||
Err(_) => {
|
||||
warn!("Could not parse date from \"{str}\": {e}");
|
||||
|
@ -76,15 +86,16 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
|
|||
}
|
||||
|
||||
/// Turn a human-readable relative timestamp into a nostr Timestamp.
|
||||
/// - Plain number as hour, 18 hours back or 6 hours forward
|
||||
/// - Plain number as hour after given date, if none 18 hours back or 6 hours forward
|
||||
/// - Number with prefix as minute offset
|
||||
/// - Otherwise try to parse a relative date
|
||||
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
|
||||
if let Some(num) = parse_hour(str, 6) {
|
||||
pub fn parse_tracking_stamp(str: &str, after: Option<DateTime<Local>>) -> Option<Timestamp> {
|
||||
if let Some(num) = parse_hour_after(str, after.unwrap_or(Local::now() - TimeDelta::hours(18))) {
|
||||
return Some(num.to_timestamp());
|
||||
}
|
||||
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
|
||||
if let Ok(num) = stripped.parse::<i64>() {
|
||||
// Complication needed because timestamp can only add u64, but we also want reverse
|
||||
return Some(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
|
||||
}
|
||||
parse_date(str).and_then(|time| {
|
||||
|
@ -123,10 +134,8 @@ pub fn format_as_datetime<F>(stamp: &Timestamp, formatter: F) -> String
|
|||
where
|
||||
F: Fn(DateTime<Local>) -> String,
|
||||
{
|
||||
match Local.timestamp_opt(stamp.as_u64() as i64 + 1, 0) {
|
||||
Single(time) => formatter(time),
|
||||
_ => stamp.to_human_datetime().to_string(),
|
||||
}
|
||||
Local.timestamp_opt(stamp.as_u64() as i64 + 1, 0).earliest()
|
||||
.map_or_else(|| stamp.to_human_datetime().to_string(), formatter)
|
||||
}
|
||||
|
||||
/// Format nostr Timestamp relative to local time
|
||||
|
@ -160,3 +169,49 @@ pub fn format_timestamp_relative_to(stamp: &Timestamp, reference: &Timestamp) ->
|
|||
_ => format_timestamp_local(stamp),
|
||||
}
|
||||
}
|
||||
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::{FixedOffset, NaiveDate, Timelike};
|
||||
use interim::datetime::DateTime;
|
||||
|
||||
#[test]
|
||||
fn parse_hours() {
|
||||
let now = Local::now();
|
||||
#[allow(deprecated)]
|
||||
let date = now.date();
|
||||
if now.hour() > 2 {
|
||||
assert_eq!(
|
||||
parse_hour("23", 22).unwrap(),
|
||||
date.and_hms_opt(23, 0, 0).unwrap()
|
||||
);
|
||||
}
|
||||
if now.hour() < 22 {
|
||||
assert_eq!(
|
||||
parse_hour("02", 2).unwrap(),
|
||||
date.and_hms_opt(2, 0, 0).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hour("2301", 1).unwrap(),
|
||||
(date - TimeDelta::days(1)).and_hms_opt(23, 01, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2020, 10, 10).unwrap();
|
||||
let time = Utc.from_utc_datetime(
|
||||
&date.and_hms_opt(10, 1,0).unwrap()
|
||||
);
|
||||
assert_eq!(parse_hour_after("2201", time).unwrap(), Utc.from_utc_datetime(&date.and_hms_opt(22, 1, 0).unwrap()));
|
||||
assert_eq!(parse_hour_after("10", time).unwrap(), Utc.from_utc_datetime(&(date + TimeDelta::days(1)).and_hms_opt(10, 0, 0).unwrap()));
|
||||
|
||||
// TODO test timezone offset issues
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone() {
|
||||
assert_eq!(
|
||||
FixedOffset::east_opt(7200).unwrap().timestamp_millis_opt(1000).unwrap().time(),
|
||||
NaiveTime::from_hms_opt(2, 0, 1).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
188
src/main.rs
188
src/main.rs
|
@ -1,3 +1,27 @@
|
|||
use crate::event_sender::MostrMessage;
|
||||
use crate::hashtag::Hashtag;
|
||||
use crate::helpers::*;
|
||||
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
||||
use crate::task::{State, StateChange, Task, MARKER_PROPERTY};
|
||||
use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
|
||||
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use colored::Colorize;
|
||||
use directories::ProjectDirs;
|
||||
use env_logger::{Builder, Target, WriteStyle};
|
||||
use itertools::Itertools;
|
||||
use keyring::Entry;
|
||||
use keyring::Error::NoEntry;
|
||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||
use nostr_sdk::bitcoin::hex::DisplayHex;
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostr_sdk::serde_json::Serializer;
|
||||
use regex::Regex;
|
||||
use rustyline::config::Configurer;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::DefaultEditor;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::env::{args, var};
|
||||
use std::fs;
|
||||
|
@ -7,25 +31,6 @@ use std::iter::once;
|
|||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::event_sender::MostrMessage;
|
||||
use crate::hashtag::Hashtag;
|
||||
use crate::helpers::*;
|
||||
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
||||
use crate::task::{State, StateChange, Task, MARKER_PROPERTY};
|
||||
use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
|
||||
use chrono::Local;
|
||||
use colored::Colorize;
|
||||
use directories::ProjectDirs;
|
||||
use env_logger::{Builder, Target, WriteStyle};
|
||||
use itertools::Itertools;
|
||||
use keyring::Entry;
|
||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||
use nostr_sdk::prelude::*;
|
||||
use regex::Regex;
|
||||
use rustyline::config::Configurer;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::DefaultEditor;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::error::Elapsed;
|
||||
use tokio::time::timeout;
|
||||
|
@ -37,7 +42,6 @@ mod kinds;
|
|||
mod event_sender;
|
||||
mod hashtag;
|
||||
|
||||
const INACTVITY_DELAY: u64 = 200;
|
||||
const LOCAL_RELAY_NAME: &str = "TEMP";
|
||||
|
||||
/// Turn a Result into an Option, showing a warning on error with optional prefix
|
||||
|
@ -62,13 +66,13 @@ macro_rules! or_warn {
|
|||
}
|
||||
}
|
||||
|
||||
fn read_keys(readline: &mut DefaultEditor) -> Result<Keys> {
|
||||
let keys_entry = Entry::new("mostr", "keys")?;
|
||||
fn read_keys(keys_entry: Entry, readline: &mut DefaultEditor) -> Result<Keys> {
|
||||
if let Ok(pass) = keys_entry.get_secret() {
|
||||
return Ok(SecretKey::from_slice(&pass).map(|s| Keys::new(s))
|
||||
.inspect_err(|e| eprintln!("Invalid key in keychain: {e}"))?);
|
||||
}
|
||||
let line = readline.readline("Secret key? (leave blank to generate and save a new keypair) ")?;
|
||||
let line = read_password(readline, "Secret key? (leave blank to generate and save a new keypair) ")?;
|
||||
|
||||
let keys = if line.is_empty() {
|
||||
info!("Generating and persisting new key");
|
||||
Keys::generate()
|
||||
|
@ -76,18 +80,30 @@ fn read_keys(readline: &mut DefaultEditor) -> Result<Keys> {
|
|||
Keys::from_str(&line)
|
||||
.inspect_err(|e| eprintln!("Invalid key provided: {e}"))?
|
||||
};
|
||||
or_warn!(keys_entry.set_secret(keys.secret_key().as_secret_bytes()),
|
||||
"Could not persist keys");
|
||||
if let Err(e) = keys_entry.set_secret(keys.secret_key().as_secret_bytes()) {
|
||||
if line.is_empty() {
|
||||
return Err(e.into());
|
||||
} else {
|
||||
warn!("Could not persist keys: {}", e)
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
fn read_password(readline: &mut DefaultEditor, prompt: &str) -> Result<String> {
|
||||
let line = readline.readline(prompt)?;
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("Running Mostr Version {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let mut debug = false;
|
||||
let mut args = args().skip(1).peekable();
|
||||
let mut builder = if args.peek().is_some_and(|arg| arg == "--debug") {
|
||||
debug = true;
|
||||
args.next();
|
||||
let mut builder = Builder::new();
|
||||
builder.filter(None, LevelFilter::Debug)
|
||||
|
@ -102,7 +118,6 @@ async fn main() -> Result<()> {
|
|||
};
|
||||
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
rl.set_auto_add_history(true);
|
||||
or_warn!(
|
||||
rl.create_external_writer().map(
|
||||
|wr| builder
|
||||
|
@ -134,18 +149,44 @@ async fn main() -> Result<()> {
|
|||
.inspect(|_| { or_warn!(fs::remove_file(key_file)); }));
|
||||
}
|
||||
|
||||
let keys = read_keys(&mut rl)?;
|
||||
let relays_file = config_dir.join("relays");
|
||||
let keys_entry = Entry::new("mostr", "keys")?;
|
||||
let keys =
|
||||
if args.peek().is_some_and(|arg| arg.trim_start_matches('-') == "import") {
|
||||
let key = rl.readline("Enter your encrypted or plaintext secret key: ")?;
|
||||
|
||||
let mut guard = rl.set_cursor_visibility(false)?;
|
||||
let enc_pwd = read_password(&mut rl, "Please enter the encryption password you used: ")?;
|
||||
guard.take();
|
||||
|
||||
let data = simple_crypt::decrypt(&(BASE64_STANDARD.decode(key)?), enc_pwd.as_bytes())?;
|
||||
let keys = Keys::new(SecretKey::from_slice(&data)?);
|
||||
if keys_entry.get_secret().is_err_and(|e| matches!(e, NoEntry)) ||
|
||||
rl.readline(&format!("Override stored key with given keypair, public key: {} (y/n)? ", keys.public_key()))? == "y" {
|
||||
keys_entry.set_secret(keys.secret_key().as_secret_bytes())?;
|
||||
}
|
||||
keys
|
||||
} else {
|
||||
read_keys(keys_entry, &mut rl)?
|
||||
};
|
||||
|
||||
info!("My active public key: {}", keys.public_key());
|
||||
if args.peek().is_some_and(|arg| arg.trim_start_matches('-') == "export") {
|
||||
let enc_pwd = read_password(&mut rl, "Please enter an encryption password for your secret key: ")?;
|
||||
let data = simple_crypt::encrypt(keys.secret_key().as_secret_bytes(), enc_pwd.as_bytes())?;
|
||||
println!("Your encrypted key: {}", BASE64_STANDARD.encode(&data));
|
||||
// TODO optionally delete
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client = ClientBuilder::new()
|
||||
.opts(Options::new()
|
||||
.automatic_authentication(true)
|
||||
.notification_channel_size(8192)
|
||||
.notification_channel_size(16384)
|
||||
)
|
||||
.signer(keys.clone())
|
||||
.build();
|
||||
info!("My public key: {}", keys.public_key());
|
||||
|
||||
let relays_file = config_dir.join("relays");
|
||||
// TODO use NewRelay message for all relays
|
||||
match var("MOSTR_RELAY") {
|
||||
Ok(relay) => {
|
||||
|
@ -206,19 +247,16 @@ async fn main() -> Result<()> {
|
|||
let metadata_clone = metadata.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
|
||||
let tasks_for_url = |url: Option<Url>| TasksRelay::from(url, &tx, &keys, Some(metadata.clone()));
|
||||
let mut relays: HashMap<Option<Url>, TasksRelay> =
|
||||
let tasks_for_url = |url: Option<RelayUrl>| TasksRelay::from(url, &tx, &keys, Some(metadata.clone()));
|
||||
let mut relays: HashMap<Option<RelayUrl>, TasksRelay> =
|
||||
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
|
||||
|
||||
let sender = tokio::spawn(async move {
|
||||
let mut queue: Option<(Url, Vec<Event>)> = None;
|
||||
|
||||
or_warn!(client.set_metadata(&metadata_clone).await, "Unable to set metadata");
|
||||
|
||||
'repl: loop {
|
||||
let result_received = timeout(Duration::from_secs(INACTVITY_DELAY), rx.recv()).await;
|
||||
match result_received {
|
||||
Ok(Some(MostrMessage::NewRelay(url))) => {
|
||||
'receiver: loop {
|
||||
match rx.recv().await {
|
||||
Some(MostrMessage::NewRelay(url)) => {
|
||||
if client.add_relay(&url).await.unwrap() {
|
||||
match client.connect_relay(&url).await {
|
||||
Ok(()) => info!("Connected to {url}"),
|
||||
|
@ -228,48 +266,39 @@ async fn main() -> Result<()> {
|
|||
warn!("Relay {url} already added");
|
||||
}
|
||||
}
|
||||
Ok(Some(MostrMessage::AddTasks(url, mut events))) => {
|
||||
trace!("Queueing {:?}", &events);
|
||||
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 {
|
||||
info!("Sending {} events to {queue_url} due to relay change", queue_events.len());
|
||||
client.batch_event_to(vec![queue_url], queue_events).await;
|
||||
queue = None;
|
||||
Some(MostrMessage::SendTask(url, event)) => {
|
||||
trace!("Sending {:?}", &event);
|
||||
let id = event.id;
|
||||
let url_str = url.as_str_without_trailing_slash().to_string();
|
||||
if let Err(e) = client.send_event_to(vec![url], event.clone()).await {
|
||||
let url_s = url_str.split("//").last().map(ToString::to_string).unwrap_or(url_str);
|
||||
if debug {
|
||||
debug!("Error sending event: {:?}", e);
|
||||
continue 'receiver;
|
||||
}
|
||||
let path = format!("failed-events-{}/", url_s);
|
||||
let dir = fs::create_dir_all(&path).map(|_| path).unwrap_or("".to_string());
|
||||
let filename = dir.to_string() + &id.to_string();
|
||||
match File::create(&filename).and_then(|mut f|
|
||||
f.write_all(or_warn!(serde_json::to_string_pretty(&event), "Failed serializing event for file writing").unwrap_or(String::new()).as_bytes())) {
|
||||
Ok(_) => error!("Failed sending update, saved a copy at {filename}: {:?}", e),
|
||||
Err(fe) => error!("Failed sending update {:?} and saving copy of event {:?}", e, fe),
|
||||
}
|
||||
}
|
||||
if queue.is_none() {
|
||||
events.reserve(events.len() + 10);
|
||||
queue = Some((url, events))
|
||||
}
|
||||
}
|
||||
Ok(Some(MostrMessage::Flush)) | Err(Elapsed { .. }) => if let Some((url, events)) = queue {
|
||||
info!("Sending {} events to {url} due to {}", events.len(),
|
||||
result_received.map_or("inactivity", |_| "flush message"));
|
||||
client.batch_event_to(vec![url], events).await;
|
||||
queue = None;
|
||||
}
|
||||
Ok(None) => {
|
||||
None => {
|
||||
debug!("Finalizing nostr communication thread because communication channel was closed");
|
||||
break 'repl;
|
||||
break 'receiver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((url, events)) = queue {
|
||||
info!("Sending {} events to {url} before exiting", events.len());
|
||||
client.batch_event_to(vec![url], events).await;
|
||||
}
|
||||
info!("Shutting down nostr communication thread");
|
||||
});
|
||||
|
||||
if relays.is_empty() {
|
||||
relays.insert(None, tasks_for_url(None));
|
||||
}
|
||||
let mut selected_relay: Option<Url> = relays.keys()
|
||||
.find_or_first(|url| url.as_ref().is_some_and(|u| u.scheme() == "wss"))
|
||||
.unwrap().clone();
|
||||
let mut selected_relay: Option<RelayUrl> = relays.keys().next().unwrap().clone();
|
||||
|
||||
{
|
||||
let tasks = relays.get_mut(&selected_relay).unwrap();
|
||||
|
@ -278,6 +307,7 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
rl.set_auto_add_history(true);
|
||||
'repl: loop {
|
||||
println!();
|
||||
let tasks = relays.get(&selected_relay).unwrap();
|
||||
|
@ -328,15 +358,17 @@ async fn main() -> Result<()> {
|
|||
continue 'repl;
|
||||
}
|
||||
Some('@') => {}
|
||||
Some(_) => {
|
||||
Some(_) =>
|
||||
if let Some((left, arg)) = command.split_once("@") {
|
||||
if let Some(time) = parse_hour(arg, 20)
|
||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) {
|
||||
command = left.to_string();
|
||||
tasks.custom_time = Some(time.to_timestamp());
|
||||
if !arg.contains(|s: char| s.is_alphabetic()) {
|
||||
let pos = tasks.get_position_timestamped();
|
||||
let time = pos.1.and_then(|_| Local.timestamp_opt(pos.0.as_u64() as i64, 0).earliest());
|
||||
if let Some(time) = parse_tracking_stamp(arg, time) {
|
||||
command = left.to_string();
|
||||
tasks.custom_time = Some(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arg = if command.len() > 1 {
|
||||
|
@ -650,7 +682,10 @@ async fn main() -> Result<()> {
|
|||
match arg {
|
||||
None => tasks.move_to(None),
|
||||
Some(arg) => {
|
||||
if parse_tracking_stamp(arg).and_then(|stamp| tasks.track_at(stamp, None)).is_some() {
|
||||
let pos = tasks.get_position_timestamped();
|
||||
let time = pos.1.and_then(|_| Local.timestamp_opt(pos.0.as_u64() as i64, 0).earliest());
|
||||
if parse_tracking_stamp(arg, time)
|
||||
.and_then(|stamp| tasks.track_at(stamp, None)).is_some() {
|
||||
println!("{}", tasks.times_tracked(15));
|
||||
}
|
||||
// So the error message is not covered up
|
||||
|
@ -679,7 +714,8 @@ async fn main() -> Result<()> {
|
|||
tasks.set_view_depth(depth);
|
||||
}
|
||||
_ => {
|
||||
tasks.filter_or_create(pos, &remaining).map(|id| tasks.move_to(Some(id)));
|
||||
tasks.filter_or_create(pos, &remaining)
|
||||
.map(|id| tasks.move_to(Some(id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -733,7 +769,7 @@ async fn main() -> Result<()> {
|
|||
println!("{}", tasks);
|
||||
continue 'repl;
|
||||
}
|
||||
or_warn!(Url::parse(&command), "Failed to parse url {}", command).map(|url| {
|
||||
or_warn!(RelayUrl::parse(&command), "Failed to parse url {}", command).map(|url| {
|
||||
match tx.try_send(MostrMessage::NewRelay(url.clone())) {
|
||||
Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"),
|
||||
Ok(_) => {
|
||||
|
|
42
src/tasks.rs
42
src/tasks.rs
|
@ -4,13 +4,6 @@ mod tests;
|
|||
mod children_traversal;
|
||||
mod durations;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::iter::{empty, once, FusedIterator};
|
||||
use std::ops::{Deref, Div, Rem};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::event_sender::{EventSender, MostrMessage};
|
||||
use crate::hashtag::Hashtag;
|
||||
use crate::helpers::{
|
||||
|
@ -22,14 +15,19 @@ use crate::task::{State, StateChange, Task, MARKER_DEPENDS, MARKER_PARENT, MARKE
|
|||
use crate::tasks::children_traversal::ChildrenTraversal;
|
||||
use crate::tasks::durations::{referenced_events, timestamps, Durations};
|
||||
pub use crate::tasks::nostr_users::NostrUsers;
|
||||
|
||||
use chrono::{Local, TimeDelta};
|
||||
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 nostr_sdk::{Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, RelayUrl, SingleLetterTag, Tag, TagKind, Timestamp, Url};
|
||||
use regex::bytes::Regex;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::iter::{empty, once, FusedIterator};
|
||||
use std::ops::{Deref, Div, Rem};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
const DEFAULT_PRIO: Prio = 25;
|
||||
|
@ -154,7 +152,7 @@ impl Display for StateFilter {
|
|||
|
||||
impl TasksRelay {
|
||||
pub(crate) fn from(
|
||||
url: Option<Url>,
|
||||
url: Option<RelayUrl>,
|
||||
tx: &Sender<MostrMessage>,
|
||||
keys: &Keys,
|
||||
metadata: Option<Metadata>,
|
||||
|
@ -240,6 +238,10 @@ impl TasksRelay {
|
|||
self.get_position_at(now()).1
|
||||
}
|
||||
|
||||
pub(crate) fn get_position_timestamped(&self) -> (Timestamp, Option<EventId>) {
|
||||
self.get_position_at(now())
|
||||
}
|
||||
|
||||
fn sorting_key(&self, task: &Task) -> impl Ord {
|
||||
self.sorting
|
||||
.iter()
|
||||
|
@ -277,9 +279,7 @@ impl TasksRelay {
|
|||
label
|
||||
} else {
|
||||
format!("{}{}",
|
||||
if limit > times.len() || limit == usize::MAX { "All ".to_string() }
|
||||
else if limit < 20 { "Recent ".to_string() }
|
||||
else { format!("Latest {limit} Entries of ") },
|
||||
if limit > times.len() || limit == usize::MAX { "All ".to_string() } else if limit < 20 { "Recent ".to_string() } else { format!("Latest {limit} Entries of ") },
|
||||
label)
|
||||
}.italic(),
|
||||
×[times.len().saturating_sub(limit)..].join("\n"))
|
||||
|
@ -1160,7 +1160,7 @@ impl TasksRelay {
|
|||
///
|
||||
/// Returns false and prints a message if parsing failed
|
||||
pub(crate) fn track_from(&mut self, str: &str) -> bool {
|
||||
parse_tracking_stamp(str)
|
||||
parse_tracking_stamp(str, None)
|
||||
.and_then(|stamp| self.track_at(stamp, self.get_position()))
|
||||
.is_some()
|
||||
}
|
||||
|
@ -1178,29 +1178,29 @@ impl TasksRelay {
|
|||
time = time + 1;
|
||||
}
|
||||
}
|
||||
let current_pos = self.get_position_at(time);
|
||||
if (time < Timestamp::now() || target.is_none()) && current_pos.1 == target {
|
||||
let pos_at = self.get_position_at(time);
|
||||
if (time < Timestamp::now() || target.is_none()) && pos_at.1 == target {
|
||||
warn!(
|
||||
"Already {} from {}",
|
||||
target.map_or("stopped time-tracking".to_string(), |id| format!(
|
||||
"tracking \"{}\"",
|
||||
self.get_task_title(&id)
|
||||
)),
|
||||
format_timestamp_relative(¤t_pos.0),
|
||||
format_timestamp_relative(&pos_at.0),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
info!("{}", match target {
|
||||
None => format!(
|
||||
"Stopping time-tracking of \"{}\" at {}",
|
||||
current_pos.1.map_or("???".to_string(), |id| self.get_task_title(&id)),
|
||||
pos_at.1.map_or("???".to_string(), |id| self.get_task_title(&id)),
|
||||
format_timestamp_relative(&time)
|
||||
),
|
||||
Some(new_id) => format!(
|
||||
"Tracking \"{}\" from {}{}",
|
||||
self.get_task_title(&new_id),
|
||||
format_timestamp_relative(&time),
|
||||
current_pos.1.filter(|id| id != &new_id).map(|id|
|
||||
pos_at.1.filter(|id| id != &new_id).map(|id|
|
||||
format!(" replacing \"{}\"", self.get_task_title(&id)))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue