WIP reading news from app
This commit is contained in:
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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
13
server/sql/tags.sql
Normal 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;
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ pub enum ServerError {
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("attachement not found")]
|
||||
PartNotFound,
|
||||
#[error("sqlx error")]
|
||||
SQLXError(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
30
server/src/newsreader.rs
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user