WIP reading news from app

This commit is contained in:
2024-07-21 07:53:02 -07:00
parent 5c0c45b99f
commit 0bf865fdef
11 changed files with 734 additions and 62 deletions

View File

@@ -28,4 +28,5 @@ css-inline = "0.13.0"
anyhow = "1.0.79"
maplit = "1.0.2"
linkify = "0.10.0"
sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio"] }

View File

@@ -7,3 +7,4 @@ address = "0.0.0.0"
port = 9345
# Uncomment to make it production like.
#log_level = "critical"
newsreader_database_url = "postgres://newsreader@nixos-07.h.xinu.tv/newsreader"

13
server/sql/tags.sql Normal file
View File

@@ -0,0 +1,13 @@
SELECT
site,
name,
count (
NOT is_read
OR NULL
) unread
FROM
post AS p
JOIN feed AS f ON p.site = f.slug
GROUP BY
1,
2;

View File

@@ -7,6 +7,7 @@ use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse};
use glog::Flags;
use notmuch::{Notmuch, NotmuchError, ThreadSet};
use rocket::{
fairing::AdHoc,
http::{ContentType, Header},
request::Request,
response::{content, Debug, Responder},
@@ -14,12 +15,19 @@ use rocket::{
Response, State,
};
use rocket_cors::{AllowedHeaders, AllowedOrigins};
use serde::Deserialize;
use server::{
error::ServerError,
graphql::{
attachment_bytes, cid_attachment_bytes, Attachment, GraphqlSchema, Mutation, QueryRoot,
},
};
use sqlx::postgres::PgPool;
#[derive(Deserialize)]
struct Config {
newsreader_database_url: String,
}
#[get("/refresh")]
async fn refresh(nm: &State<Notmuch>) -> Result<Json<String>, Debug<NotmuchError>> {
@@ -224,12 +232,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
.to_cors()?;
let schema = Schema::build(QueryRoot, Mutation, EmptySubscription)
.data(Notmuch::default())
.extension(async_graphql::extensions::Logger)
.finish();
let _ = rocket::build()
let rkt = rocket::build()
.mount(
shared::urls::MOUNT_POINT,
routes![
@@ -248,11 +251,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
],
)
.attach(cors)
.manage(schema)
.manage(Notmuch::default())
//.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config"))
.launch()
.await?;
.attach(AdHoc::config::<Config>());
let config: Config = rkt.figment().extract()?;
let pool = PgPool::connect(&config.newsreader_database_url).await?;
let schema = Schema::build(QueryRoot, Mutation, EmptySubscription)
.data(Notmuch::default())
.data(pool.clone())
.extension(async_graphql::extensions::Logger)
.finish();
let rkt = rkt.manage(schema).manage(pool).manage(Notmuch::default());
//.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config"))
rkt.launch().await?;
Ok(())
}

View File

@@ -13,4 +13,6 @@ pub enum ServerError {
IoError(#[from] std::io::Error),
#[error("attachement not found")]
PartNotFound,
#[error("sqlx error")]
SQLXError(#[from] sqlx::Error),
}

View File

@@ -1,10 +1,5 @@
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
use std::{
collections::HashMap,
fs::File,
hash::{DefaultHasher, Hash, Hasher},
time::Instant,
};
use std::fs::File;
use async_graphql::{
connection::{self, Connection, Edge},
@@ -14,8 +9,9 @@ use log::{error, info, warn};
use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail};
use memmap::MmapOptions;
use notmuch::Notmuch;
use sqlx::postgres::PgPool;
use crate::{error::ServerError, linkify_html, sanitize_html};
use crate::{error::ServerError, linkify_html, newsreader, nm, sanitize_html};
/// # Number of seconds since the Epoch
pub type UnixTime = isize;
@@ -181,11 +177,11 @@ pub struct Email {
}
#[derive(SimpleObject)]
struct Tag {
name: String,
fg_color: String,
bg_color: String,
unread: usize,
pub(crate) struct Tag {
pub name: String,
pub fg_color: String,
pub bg_color: String,
pub unread: usize,
}
pub struct QueryRoot;
@@ -261,44 +257,10 @@ impl QueryRoot {
async fn tags<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Tag>> {
let nm = ctx.data_unchecked::<Notmuch>();
let now = Instant::now();
let pool = ctx.data_unchecked::<PgPool>();
let needs_unread = ctx.look_ahead().field("unread").exists();
let unread_msg_cnt: HashMap<String, usize> = if needs_unread {
// 10000 is an arbitrary number, if there's more than 10k unread messages, we'll
// get an inaccurate count.
nm.search("is:unread", 0, 10000)?
.0
.iter()
.fold(HashMap::new(), |mut m, ts| {
ts.tags.iter().for_each(|t| {
m.entry(t.clone()).and_modify(|c| *c += 1).or_insert(1);
});
m
})
} else {
HashMap::new()
};
let tags = nm
.tags()?
.into_iter()
.map(|tag| {
let mut hasher = DefaultHasher::new();
tag.hash(&mut hasher);
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
let unread = if needs_unread {
*unread_msg_cnt.get(&tag).unwrap_or(&0)
} else {
0
};
Tag {
name: tag,
fg_color: "white".to_string(),
bg_color: hex,
unread,
}
})
.collect();
info!("Fetching tags took {} seconds", now.elapsed().as_secs_f32());
let mut tags = nm::tags(nm, needs_unread)?;
tags.append(&mut newsreader::tags(pool, needs_unread).await?);
Ok(tags)
}
async fn thread<'ctx>(&self, ctx: &Context<'ctx>, thread_id: String) -> Result<Thread, Error> {

View File

@@ -1,6 +1,7 @@
pub mod error;
pub mod graphql;
pub mod nm;
mod newsreader;
mod nm;
use css_inline::{CSSInliner, InlineError, InlineOptions};
use linkify::{LinkFinder, LinkKind};

30
server/src/newsreader.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use log::info;
use sqlx::postgres::PgPool;
const TAG_PREFIX: &'static str = "News";
use crate::{error, graphql::Tag};
pub async fn tags(pool: &PgPool, needs_unread: bool) -> Result<Vec<Tag>, error::ServerError> {
// TODO: write separate query for needs_unread.
let tags = sqlx::query_file!("sql/tags.sql").fetch_all(pool).await?;
info!("sqlx tags {tags:#?}");
let tags = tags
.into_iter()
.map(|tag| {
let mut hasher = DefaultHasher::new();
tag.site.hash(&mut hasher);
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
let unread = tag.unread.unwrap_or(0).try_into().unwrap_or(0);
let name = format!("{TAG_PREFIX}/{}", tag.site.expect("tag must have site"));
Tag {
name,
fg_color: "white".to_string(),
bg_color: hex,
unread,
}
})
.collect();
Ok(tags)
}

View File

@@ -1,6 +1,14 @@
use std::{
collections::HashMap,
hash::{DefaultHasher, Hash, Hasher},
time::Instant,
};
use log::info;
use notmuch::Notmuch;
use shared::Message;
use crate::error;
use crate::{error, graphql::Tag};
// TODO(wathiede): decide good error type
pub fn threadset_to_messages(
@@ -11,3 +19,44 @@ pub fn threadset_to_messages(
}
Ok(Vec::new())
}
pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, error::ServerError> {
let now = Instant::now();
let unread_msg_cnt: HashMap<String, usize> = if needs_unread {
// 10000 is an arbitrary number, if there's more than 10k unread messages, we'll
// get an inaccurate count.
nm.search("is:unread", 0, 10000)?
.0
.iter()
.fold(HashMap::new(), |mut m, ts| {
ts.tags.iter().for_each(|t| {
m.entry(t.clone()).and_modify(|c| *c += 1).or_insert(1);
});
m
})
} else {
HashMap::new()
};
let tags = nm
.tags()?
.into_iter()
.map(|tag| {
let mut hasher = DefaultHasher::new();
tag.hash(&mut hasher);
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
let unread = if needs_unread {
*unread_msg_cnt.get(&tag).unwrap_or(&0)
} else {
0
};
Tag {
name: tag,
fg_color: "white".to_string(),
bg_color: hex,
unread,
}
})
.collect();
info!("Fetching tags took {} seconds", now.elapsed().as_secs_f32());
Ok(tags)
}