Compare commits
35 Commits
letterbox-
...
letterbox-
| Author | SHA1 | Date | |
|---|---|---|---|
| 75ad27ec2f | |||
| f904fa0001 | |||
| b94596bf65 | |||
| aa24599921 | |||
| c81a8c1cd3 | |||
| 7c3cfec3d1 | |||
| a2920fde3b | |||
| 8bc449ae6e | |||
| 0febd0535a | |||
| a9e00a54e4 | |||
| 6811c689ff | |||
| 8ba6b3d0b0 | |||
| a7c5585e80 | |||
| 4ef4d49113 | |||
| f8af303110 | |||
| fa5aac34ba | |||
| b58556254e | |||
| e365ced7dd | |||
| 93d569fb14 | |||
| f86a5f464d | |||
| 956c20b156 | |||
| 1eb498712b | |||
| f12979c0be | |||
| 4665f34e54 | |||
| bbdc35061c | |||
| f11f0b4d23 | |||
| c7c47e4a73 | |||
| c3835522b2 | |||
| dfa80f9046 | |||
| b8dfdabf8d | |||
| bbcf52b006 | |||
| f92c05cd28 | |||
| 885bbe0a8c | |||
| 8b1d111837 | |||
| 08abf31fa9 |
855
Cargo.lock
generated
855
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
license = "UNLICENSED"
|
||||
publish = ["xinu"]
|
||||
version = "0.17.13"
|
||||
version = "0.17.25"
|
||||
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
||||
|
||||
[profile.dev]
|
||||
|
||||
@@ -214,9 +214,8 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use tracing::{error, info, instrument, warn};
|
||||
|
||||
/// # Number of seconds since the Epoch
|
||||
pub type UnixTime = isize;
|
||||
@@ -718,6 +717,13 @@ impl Notmuch {
|
||||
cmd.args(args);
|
||||
info!("{:?}", &cmd);
|
||||
let out = cmd.output()?;
|
||||
if !out.stderr.is_empty() {
|
||||
warn!(
|
||||
"{:?}: STDERR:\n{}",
|
||||
&cmd,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
Ok(out.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,9 @@ css-inline = "0.14.4"
|
||||
futures = "0.3.31"
|
||||
headers = "0.4.0"
|
||||
html-escape = "0.2.13"
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.13", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared", version = "0.17.13", registry = "xinu" }
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.25", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared", version = "0.17.25", registry = "xinu" }
|
||||
linkify = "0.10.0"
|
||||
log = "0.4.27"
|
||||
lol_html = "2.3.0"
|
||||
mailparse = "0.16.1"
|
||||
maplit = "1.0.2"
|
||||
|
||||
@@ -200,15 +200,21 @@ async fn send_refresh_websocket_handler(
|
||||
None => Some(10),
|
||||
};
|
||||
|
||||
if let Err(err) = label_unprocessed(&nm, &pool, false, limit, "tag:unprocessed").await {
|
||||
error!("Failed to label_unprocessed: {err:?}");
|
||||
let mut ids = None;
|
||||
match label_unprocessed(&nm, &pool, false, limit, "tag:unprocessed").await {
|
||||
Ok(i) => ids = Some(i),
|
||||
Err(err) => error!("Failed to label_unprocessed: {err:?}"),
|
||||
};
|
||||
connection_tracker
|
||||
.lock()
|
||||
.await
|
||||
.send_message_all(WebsocketMessage::RefreshMessages)
|
||||
.await;
|
||||
"refresh triggered"
|
||||
if let Some(ids) = ids {
|
||||
format!("{ids:?}")
|
||||
} else {
|
||||
"refresh triggered".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
async fn watch_new(
|
||||
|
||||
@@ -9,11 +9,10 @@ use async_graphql::{
|
||||
use cacher::FilesystemCacher;
|
||||
use futures::stream;
|
||||
use letterbox_notmuch::Notmuch;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use tokio::join;
|
||||
use tracing::instrument;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
use crate::tantivy::TantivyConnection;
|
||||
|
||||
@@ -21,7 +21,6 @@ use cacher::{Cacher, FilesystemCacher};
|
||||
use css_inline::{CSSInliner, InlineError, InlineOptions};
|
||||
pub use error::ServerError;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use log::{debug, error, info, warn};
|
||||
use lol_html::{
|
||||
element, errors::RewritingError, html_content::ContentType, rewrite_str, text,
|
||||
RewriteStrSettings,
|
||||
@@ -32,6 +31,7 @@ use reqwest::StatusCode;
|
||||
use scraper::{Html, Selector};
|
||||
use sqlx::types::time::PrimitiveDateTime;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -3,11 +3,10 @@ use std::collections::HashMap;
|
||||
use cacher::FilesystemCacher;
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use letterbox_shared::compute_color;
|
||||
use log::{error, info};
|
||||
use maplit::hashmap;
|
||||
use scraper::Selector;
|
||||
use sqlx::postgres::PgPool;
|
||||
use tracing::instrument;
|
||||
use tracing::{error, info, instrument};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
@@ -353,6 +352,9 @@ fn slurp_contents_selectors() -> HashMap<String, Vec<Selector>> {
|
||||
"natwelch.com".to_string() => vec![
|
||||
Selector::parse("article div.prose").unwrap(),
|
||||
],
|
||||
"seiya.me".to_string() => vec![
|
||||
Selector::parse("header + div").unwrap(),
|
||||
],
|
||||
"rustacean-station.org".to_string() => vec![
|
||||
Selector::parse("article").unwrap(),
|
||||
],
|
||||
|
||||
@@ -5,11 +5,10 @@ use std::{
|
||||
|
||||
use letterbox_notmuch::Notmuch;
|
||||
use letterbox_shared::{compute_color, Rule};
|
||||
use log::{error, info, warn};
|
||||
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||
use memmap::MmapOptions;
|
||||
use sqlx::{types::Json, PgPool};
|
||||
use tracing::instrument;
|
||||
use tracing::{error, info, info_span, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
compute_offset_limit,
|
||||
@@ -946,7 +945,7 @@ pub async fn label_unprocessed(
|
||||
dryrun: bool,
|
||||
limit: Option<usize>,
|
||||
query: &str,
|
||||
) -> Result<(), ServerError> {
|
||||
) -> Result<Box<[String]>, ServerError> {
|
||||
use futures::StreamExt;
|
||||
let ids = nm.message_ids(query)?;
|
||||
info!(
|
||||
@@ -977,22 +976,38 @@ pub async fn label_unprocessed(
|
||||
*/
|
||||
info!("Loaded {} rules", rules.len());
|
||||
|
||||
let ids = if let Some(limit) = limit {
|
||||
&ids[..limit]
|
||||
} else {
|
||||
&ids[..]
|
||||
};
|
||||
let limit = limit.unwrap_or(ids.len());
|
||||
let limit = limit.min(ids.len());
|
||||
let ids = &ids[..limit];
|
||||
|
||||
let mut add_mutations = HashMap::new();
|
||||
let mut rm_mutations = HashMap::new();
|
||||
for id in ids {
|
||||
let id = format!("id:{id}");
|
||||
let files = nm.files(&id)?;
|
||||
// Only process the first file path is multiple files have the same id
|
||||
let path = files.iter().next().unwrap();
|
||||
let Some(path) = files.iter().next() else {
|
||||
error!("No files for message-ID {id}");
|
||||
let t = "Letterbox/Bad";
|
||||
nm.tag_add(t, &id)?;
|
||||
let t = "unprocessed";
|
||||
nm.tag_remove(t, &id)?;
|
||||
continue;
|
||||
};
|
||||
let file = File::open(&path)?;
|
||||
info!("mmaping {path}");
|
||||
info!("parsing {path}");
|
||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||
let m = parse_mail(&mmap)?;
|
||||
let m = match info_span!("parse_mail", path = path).in_scope(|| parse_mail(&mmap)) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
error!("Failed to parse {path}: {err}");
|
||||
let t = "Letterbox/Bad";
|
||||
nm.tag_add(t, &id)?;
|
||||
let t = "unprocessed";
|
||||
nm.tag_remove(t, &id)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (matched_rule, add_tags) = find_tags(&rules, &m.headers);
|
||||
if matched_rule {
|
||||
if dryrun {
|
||||
@@ -1037,11 +1052,6 @@ pub async fn label_unprocessed(
|
||||
.or_insert_with(|| Vec::new())
|
||||
.push(id.clone());
|
||||
}
|
||||
let t = "unprocessed".to_string();
|
||||
rm_mutations
|
||||
.entry(t)
|
||||
.or_insert_with(|| Vec::new())
|
||||
.push(id.clone());
|
||||
//nm.tag_remove("inbox", &id)?;
|
||||
let t = "inbox".to_string();
|
||||
rm_mutations
|
||||
@@ -1049,13 +1059,19 @@ pub async fn label_unprocessed(
|
||||
.or_insert_with(|| Vec::new())
|
||||
.push(id.clone());
|
||||
}
|
||||
let t = "unprocessed".to_string();
|
||||
rm_mutations
|
||||
.entry(t)
|
||||
.or_insert_with(|| Vec::new())
|
||||
.push(id.clone());
|
||||
}
|
||||
info!("Adding {} distinct labels", add_mutations.len());
|
||||
for (tag, ids) in add_mutations.iter() {
|
||||
info!(" {tag}: {}", ids.len());
|
||||
if !dryrun {
|
||||
let ids: Vec<_> = ids.iter().map(|s| s.as_str()).collect();
|
||||
nm.tags_add(tag, &ids)?;
|
||||
info_span!("tags_add", tag = tag, count = ids.len())
|
||||
.in_scope(|| nm.tags_add(tag, &ids))?;
|
||||
}
|
||||
}
|
||||
info!("Removing {} distinct labels", rm_mutations.len());
|
||||
@@ -1063,11 +1079,12 @@ pub async fn label_unprocessed(
|
||||
info!(" {tag}: {}", ids.len());
|
||||
if !dryrun {
|
||||
let ids: Vec<_> = ids.iter().map(|s| s.as_str()).collect();
|
||||
nm.tags_remove(tag, &ids)?;
|
||||
info_span!("tags_remove", tag = tag, count = ids.len())
|
||||
.in_scope(|| nm.tags_remove(tag, &ids))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(ids.into())
|
||||
}
|
||||
fn find_tags<'a, 'b>(rules: &'a [Rule], headers: &'b [MailHeader]) -> (bool, HashSet<&'a str>) {
|
||||
let mut matched_rule = false;
|
||||
|
||||
@@ -12,7 +12,7 @@ version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
build-info = "0.0.40"
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.13", registry = "xinu" }
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.25", registry = "xinu" }
|
||||
regex = "1.11.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = "0.8.5"
|
||||
|
||||
@@ -72,10 +72,6 @@ fn on_url_changed(old: &Url, mut new: Url) -> Msg {
|
||||
if did_change {
|
||||
messages.push(Msg::ScrollToTop)
|
||||
}
|
||||
info!(
|
||||
"url changed\nold '{old}'\nnew '{new}', history {}",
|
||||
history().length().unwrap_or(0)
|
||||
);
|
||||
let hpp = new.remaining_hash_path_parts();
|
||||
let msg = match hpp.as_slice() {
|
||||
["t", tid] => Msg::ShowThreadRequest {
|
||||
@@ -553,7 +549,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
});
|
||||
}
|
||||
Msg::ScrollToTop => {
|
||||
info!("scrolling to the top");
|
||||
web_sys::window().unwrap().scroll_to_with_x_and_y(0., 0.);
|
||||
}
|
||||
Msg::WindowScrolled => {
|
||||
@@ -619,6 +614,36 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders.send_msg(Msg::CatchupRequest { query });
|
||||
}
|
||||
Msg::CatchupKeepUnread => {
|
||||
if let Some(thread_id) = current_thread_id(&model.context) {
|
||||
if let Context::ThreadResult {
|
||||
thread:
|
||||
ShowThreadQueryThread::EmailThread(ShowThreadQueryThreadOnEmailThread {
|
||||
messages,
|
||||
..
|
||||
}),
|
||||
..
|
||||
} = &model.context
|
||||
{
|
||||
//orders.send_msg(Msg::SetUnread(thread_id, false));
|
||||
let unread_messages: Vec<_> = messages
|
||||
.iter()
|
||||
.filter(|msg| msg.tags.iter().any(|t| t == "unread"))
|
||||
.map(|msg| &msg.id)
|
||||
.collect();
|
||||
if unread_messages.is_empty() {
|
||||
// All messages are read, so mark them all unread
|
||||
orders.send_msg(Msg::SetUnread(thread_id, true));
|
||||
} else {
|
||||
// Do nothing if there are some messages unread
|
||||
}
|
||||
} else {
|
||||
// News post, not email, just mark unread
|
||||
orders.send_msg(Msg::SetUnread(thread_id, true));
|
||||
};
|
||||
} else {
|
||||
// This shouldn't happen
|
||||
warn!("no current thread_id");
|
||||
}
|
||||
orders.send_msg(Msg::CatchupNext);
|
||||
}
|
||||
Msg::CatchupMarkAsRead => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::VecDeque, rc::Rc};
|
||||
|
||||
use letterbox_shared::WebsocketMessage;
|
||||
use log::{error, info};
|
||||
use log::{debug, error};
|
||||
use seed::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -122,13 +122,13 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::WebSocketOpened => {
|
||||
model.web_socket_reconnector = None;
|
||||
info!("WebSocket connection is open now");
|
||||
debug!("WebSocket connection is open now");
|
||||
}
|
||||
Msg::TextMessageReceived(msg) => {
|
||||
model.updates.push_back(msg);
|
||||
}
|
||||
Msg::WebSocketClosed(close_event) => {
|
||||
info!(
|
||||
debug!(
|
||||
r#"==================
|
||||
WebSocket connection was closed:
|
||||
Clean: {0}
|
||||
@@ -148,7 +148,7 @@ Reason: {2}
|
||||
}
|
||||
}
|
||||
Msg::WebSocketFailed => {
|
||||
info!("WebSocket failed");
|
||||
debug!("WebSocket failed");
|
||||
if model.web_socket_reconnector.is_none() {
|
||||
model.web_socket_reconnector = Some(
|
||||
orders.stream_with_handle(streams::backoff(None, Msg::ReconnectWebSocket)),
|
||||
@@ -156,7 +156,7 @@ Reason: {2}
|
||||
}
|
||||
}
|
||||
Msg::ReconnectWebSocket(retries) => {
|
||||
info!("Reconnect attempt: {}", retries);
|
||||
debug!("Reconnect attempt: {}", retries);
|
||||
model.web_socket = create_websocket(&model.ws_url, orders).unwrap();
|
||||
}
|
||||
Msg::SendMessage(msg) => {
|
||||
@@ -177,16 +177,16 @@ fn create_websocket(url: &str, orders: &impl Orders<Msg>) -> Result<EventClient,
|
||||
|
||||
let send = msg_sender.clone();
|
||||
client.set_on_connection(Some(Box::new(move |client: &EventClient| {
|
||||
info!("{:#?}", client.status);
|
||||
debug!("{:#?}", client.status);
|
||||
let msg = match *client.status.borrow() {
|
||||
ConnectionStatus::Connecting => {
|
||||
info!("Connecting...");
|
||||
debug!("Connecting...");
|
||||
None
|
||||
}
|
||||
ConnectionStatus::Connected => Some(Msg::WebSocketOpened),
|
||||
ConnectionStatus::Error => Some(Msg::WebSocketFailed),
|
||||
ConnectionStatus::Disconnected => {
|
||||
info!("Disconnected");
|
||||
debug!("Disconnected");
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -195,7 +195,7 @@ fn create_websocket(url: &str, orders: &impl Orders<Msg>) -> Result<EventClient,
|
||||
|
||||
let send = msg_sender.clone();
|
||||
client.set_on_close(Some(Box::new(move |ev| {
|
||||
info!("WS: Connection closed");
|
||||
debug!("WS: Connection closed");
|
||||
send(Some(Msg::WebSocketClosed(ev)));
|
||||
})));
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ html {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.news-post.site-seiya-me figure>pre,
|
||||
.news-post.site-seiya-me figure>pre>code {
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
.news-post.site-slashdot .story-byline {
|
||||
display: block !important;
|
||||
height: initial !important;
|
||||
|
||||
Reference in New Issue
Block a user