server: add mutation to mark messages as read

This commit is contained in:
Bill Thiede 2024-02-11 19:43:34 -08:00
parent 81ed3a8ca2
commit 5451dd2056
3 changed files with 35 additions and 18 deletions

View File

@ -2,7 +2,7 @@
extern crate rocket; extern crate rocket;
use std::{error::Error, io::Cursor, str::FromStr}; use std::{error::Error, io::Cursor, str::FromStr};
use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema};
use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse};
use glog::Flags; use glog::Flags;
use notmuch::{Notmuch, NotmuchError, ThreadSet}; use notmuch::{Notmuch, NotmuchError, ThreadSet};
@ -16,7 +16,7 @@ use rocket::{
use rocket_cors::{AllowedHeaders, AllowedOrigins}; use rocket_cors::{AllowedHeaders, AllowedOrigins};
use server::{ use server::{
error::ServerError, error::ServerError,
graphql::{GraphqlSchema, QueryRoot}, graphql::{GraphqlSchema, Mutation, QueryRoot},
}; };
#[get("/refresh")] #[get("/refresh")]
@ -182,7 +182,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
} }
.to_cors()?; .to_cors()?;
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) let schema = Schema::build(QueryRoot, Mutation, EmptySubscription)
.data(Notmuch::default()) .data(Notmuch::default())
.extension(async_graphql::extensions::Logger) .extension(async_graphql::extensions::Logger)
.finish(); .finish();

View File

@ -7,8 +7,7 @@ use std::{
use async_graphql::{ use async_graphql::{
connection::{self, Connection, Edge}, connection::{self, Connection, Edge},
Context, EmptyMutation, EmptySubscription, Enum, Error, FieldResult, Object, Schema, Context, EmptySubscription, Enum, Error, FieldResult, Object, Schema, SimpleObject, Union,
SimpleObject, Union,
}; };
use log::{error, info, warn}; use log::{error, info, warn};
use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail}; use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail};
@ -18,8 +17,6 @@ use rocket::time::Instant;
use crate::{linkify_html, sanitize_html}; use crate::{linkify_html, sanitize_html};
pub struct QueryRoot;
/// # Number of seconds since the Epoch /// # Number of seconds since the Epoch
pub type UnixTime = isize; pub type UnixTime = isize;
@ -44,6 +41,7 @@ pub struct ThreadSummary {
#[derive(Debug, SimpleObject)] #[derive(Debug, SimpleObject)]
pub struct Thread { pub struct Thread {
thread_id: String,
subject: String, subject: String,
messages: Vec<Message>, messages: Vec<Message>,
} }
@ -192,6 +190,7 @@ struct Tag {
unread: usize, unread: usize,
} }
pub struct QueryRoot;
#[Object] #[Object]
impl QueryRoot { impl QueryRoot {
async fn count<'ctx>(&self, ctx: &Context<'ctx>, query: String) -> Result<usize, Error> { async fn count<'ctx>(&self, ctx: &Context<'ctx>, query: String) -> Result<usize, Error> {
@ -316,13 +315,8 @@ impl QueryRoot {
.exists(); .exists();
let mut messages = Vec::new(); let mut messages = Vec::new();
for (path, id) in std::iter::zip(nm.files(&thread_id)?, nm.message_ids(&thread_id)?) { for (path, id) in std::iter::zip(nm.files(&thread_id)?, nm.message_ids(&thread_id)?) {
info!("{id}\nfile: {path}"); let tags = nm.tags_for_query(&format!("id:{id}"))?;
let msg = nm.show(&format!("id:{id}"))?; info!("{id}: {tags:?}\nfile: {path}");
let tags = msg.0[0].0[0]
.0
.as_ref()
.map(|m| m.tags.clone())
.unwrap_or_else(Vec::default);
let file = File::open(&path)?; let file = File::open(&path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? }; let mmap = unsafe { MmapOptions::new().map(&file)? };
let m = parse_mail(&mmap)?; let m = parse_mail(&mmap)?;
@ -401,7 +395,31 @@ impl QueryRoot {
.next() .next()
.and_then(|m| m.subject.clone()) .and_then(|m| m.subject.clone())
.unwrap_or("(NO SUBJECT)".to_string()); .unwrap_or("(NO SUBJECT)".to_string());
Ok(Thread { subject, messages }) Ok(Thread {
thread_id,
subject,
messages,
})
}
}
pub struct Mutation;
#[Object]
impl Mutation {
async fn set_read_status<'ctx>(
&self,
ctx: &Context<'ctx>,
query: String,
unread: bool,
) -> Result<bool, Error> {
let nm = ctx.data_unchecked::<Notmuch>();
info!("set_read_status({unread})");
if unread {
nm.tag_add("unread", &format!("{query}"))?;
} else {
nm.tag_remove("unread", &format!("{query}"))?;
}
Ok(true)
} }
} }
@ -587,7 +605,7 @@ fn render_content_type_tree(m: &ParsedMail) -> String {
render_rec(m, 1) render_rec(m, 1)
} }
pub type GraphqlSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>; pub type GraphqlSchema = Schema<QueryRoot, Mutation, EmptySubscription>;
fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<Email>, Error> { fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<Email>, Error> {
let mut addrs = Vec::new(); let mut addrs = Vec::new();

View File

@ -19,7 +19,7 @@ pub enum SanitizeError {
pub fn linkify_html(text: &str) -> String { pub fn linkify_html(text: &str) -> String {
let mut finder = LinkFinder::new(); let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false); let finder = finder.url_must_have_scheme(false).kinds(&[LinkKind::Url]);
let mut parts = Vec::new(); let mut parts = Vec::new();
for span in finder.spans(text) { for span in finder.spans(text) {
// TODO(wathiede): use Cow<str>? // TODO(wathiede): use Cow<str>?
@ -35,7 +35,6 @@ pub fn linkify_html(text: &str) -> String {
"http://" "http://"
}; };
let a = format!(r#"<a href="{schema}{0}">{0}</a>"#, text); let a = format!(r#"<a href="{schema}{0}">{0}</a>"#, text);
log::info!("link {} {a}", span.as_str());
parts.push(a); parts.push(a);
} }
_ => todo!("unhandled kind: {:?}", span.kind().unwrap()), _ => todo!("unhandled kind: {:?}", span.kind().unwrap()),