diff --git a/.editorconfig b/.editorconfig index 3f999da..3de629d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,7 @@ [*] indent_size = 2 -indent_style = space \ No newline at end of file +indent_style = space + +[Makefile] +indent_size = 4 +indent_style = tab \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 75e3796..de519be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,51 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-imap" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98892ebee4c05fc66757e600a7466f0d9bfcde338f645d64add323789f26cb36" +dependencies = [ + "async-channel 2.3.1", + "base64 0.21.7", + "bytes", + "chrono", + "futures", + "imap-proto", + "log", + "nom", + "once_cell", + "pin-utils", + "self_cell", + "stop-token", + "thiserror", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -558,6 +603,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -1097,6 +1151,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + [[package]] name = "fast-float" version = "0.2.0" @@ -1945,6 +2026,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "imap-proto" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856" +dependencies = [ + "nom", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2368,6 +2458,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -2480,6 +2576,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2784,6 +2890,7 @@ name = "panorama-daemon" version = "0.1.0" dependencies = [ "anyhow", + "async-imap", "axum", "chrono", "cozo", @@ -2805,6 +2912,12 @@ dependencies = [ name = "panorama-macros" version = "0.1.0" +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.3" @@ -3661,6 +3774,12 @@ dependencies = [ "thin-slice", ] +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + [[package]] name = "semver" version = "1.0.23" @@ -3987,6 +4106,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stop-token" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" +dependencies = [ + "async-channel 1.9.0", + "cfg-if", + "futures-core", + "pin-project-lite", +] + [[package]] name = "string_cache" version = "0.8.7" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e18d873 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +deploy-docs: + mdbook build docs + rsync -azrP docs/book/ root@veil:/home/blogDeploy/public/panorama \ No newline at end of file diff --git a/app/src-tauri/icons/128x128.png b/app/src-tauri/icons/128x128.png index 6be5e50..35270cb 100644 Binary files a/app/src-tauri/icons/128x128.png and b/app/src-tauri/icons/128x128.png differ diff --git a/app/src-tauri/icons/128x128@2x.png b/app/src-tauri/icons/128x128@2x.png index e81bece..977f127 100644 Binary files a/app/src-tauri/icons/128x128@2x.png and b/app/src-tauri/icons/128x128@2x.png differ diff --git a/app/src-tauri/icons/32x32.png b/app/src-tauri/icons/32x32.png index a437dd5..c43f6ad 100644 Binary files a/app/src-tauri/icons/32x32.png and b/app/src-tauri/icons/32x32.png differ diff --git a/app/src-tauri/icons/Square107x107Logo.png b/app/src-tauri/icons/Square107x107Logo.png index 0ca4f27..2fc3789 100644 Binary files a/app/src-tauri/icons/Square107x107Logo.png and b/app/src-tauri/icons/Square107x107Logo.png differ diff --git a/app/src-tauri/icons/Square142x142Logo.png b/app/src-tauri/icons/Square142x142Logo.png index b81f820..948d76e 100644 Binary files a/app/src-tauri/icons/Square142x142Logo.png and b/app/src-tauri/icons/Square142x142Logo.png differ diff --git a/app/src-tauri/icons/Square150x150Logo.png b/app/src-tauri/icons/Square150x150Logo.png index 624c7bf..407bf1c 100644 Binary files a/app/src-tauri/icons/Square150x150Logo.png and b/app/src-tauri/icons/Square150x150Logo.png differ diff --git a/app/src-tauri/icons/Square284x284Logo.png b/app/src-tauri/icons/Square284x284Logo.png index c021d2b..c3a323c 100644 Binary files a/app/src-tauri/icons/Square284x284Logo.png and b/app/src-tauri/icons/Square284x284Logo.png differ diff --git a/app/src-tauri/icons/Square30x30Logo.png b/app/src-tauri/icons/Square30x30Logo.png index 6219700..152ca34 100644 Binary files a/app/src-tauri/icons/Square30x30Logo.png and b/app/src-tauri/icons/Square30x30Logo.png differ diff --git a/app/src-tauri/icons/Square310x310Logo.png b/app/src-tauri/icons/Square310x310Logo.png index f9bc048..714f276 100644 Binary files a/app/src-tauri/icons/Square310x310Logo.png and b/app/src-tauri/icons/Square310x310Logo.png differ diff --git a/app/src-tauri/icons/Square44x44Logo.png b/app/src-tauri/icons/Square44x44Logo.png index d5fbfb2..3fed016 100644 Binary files a/app/src-tauri/icons/Square44x44Logo.png and b/app/src-tauri/icons/Square44x44Logo.png differ diff --git a/app/src-tauri/icons/Square71x71Logo.png b/app/src-tauri/icons/Square71x71Logo.png index 63440d7..ba1cd58 100644 Binary files a/app/src-tauri/icons/Square71x71Logo.png and b/app/src-tauri/icons/Square71x71Logo.png differ diff --git a/app/src-tauri/icons/Square89x89Logo.png b/app/src-tauri/icons/Square89x89Logo.png index f3f705a..46af11b 100644 Binary files a/app/src-tauri/icons/Square89x89Logo.png and b/app/src-tauri/icons/Square89x89Logo.png differ diff --git a/app/src-tauri/icons/StoreLogo.png b/app/src-tauri/icons/StoreLogo.png index 4556388..60f4726 100644 Binary files a/app/src-tauri/icons/StoreLogo.png and b/app/src-tauri/icons/StoreLogo.png differ diff --git a/app/src-tauri/icons/icon.icns b/app/src-tauri/icons/icon.icns index 12a5bce..0821eef 100644 Binary files a/app/src-tauri/icons/icon.icns and b/app/src-tauri/icons/icon.icns differ diff --git a/app/src-tauri/icons/icon.ico b/app/src-tauri/icons/icon.ico index b3636e4..987c040 100644 Binary files a/app/src-tauri/icons/icon.ico and b/app/src-tauri/icons/icon.ico differ diff --git a/app/src-tauri/icons/icon.png b/app/src-tauri/icons/icon.png index e1cd261..1fd567e 100644 Binary files a/app/src-tauri/icons/icon.png and b/app/src-tauri/icons/icon.png differ diff --git a/crates/panorama-daemon/Cargo.toml b/crates/panorama-daemon/Cargo.toml index c9bc6dc..ea52158 100644 --- a/crates/panorama-daemon/Cargo.toml +++ b/crates/panorama-daemon/Cargo.toml @@ -22,3 +22,8 @@ tokio = { version = "1.37.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.2", features = ["cors"] } uuid = { version = "1.8.0", features = ["v7"] } + +[dependencies.async-imap] +version = "0.9.7" +default-features = false +features = ["runtime-tokio"] diff --git a/crates/panorama-daemon/src/error.rs b/crates/panorama-daemon/src/error.rs index 3227b3f..b5cbb61 100644 --- a/crates/panorama-daemon/src/error.rs +++ b/crates/panorama-daemon/src/error.rs @@ -6,6 +6,7 @@ use axum::{ pub type AppResult = std::result::Result; // Make our own error that wraps `anyhow::Error`. +#[derive(Debug)] pub struct AppError(miette::Report); // Tell axum how to convert `AppError` into a response. diff --git a/crates/panorama-daemon/src/mail.rs b/crates/panorama-daemon/src/mail.rs index 3e8516e..51a8b92 100644 --- a/crates/panorama-daemon/src/mail.rs +++ b/crates/panorama-daemon/src/mail.rs @@ -1,6 +1,11 @@ +use std::time::Duration; + use axum::{extract::State, Json}; use cozo::{DbInstance, ScriptMutability}; +use futures::TryStreamExt; +use miette::IntoDiagnostic; use serde_json::Value; +use tokio::{net::TcpStream, time::sleep}; use crate::{error::AppResult, AppState}; @@ -14,10 +19,74 @@ pub async fn get_mail_config( } pub async fn mail_loop(db: DbInstance) { - // Fetch the mail configs + loop { + match mail_loop_inner(&db).await { + Ok(_) => sleep(Duration::from_secs(30)).await, + Err(err) => { + eprintln!("Fetch config error: {err:?}"); + // Back off, retry + // TODO: Exponential backoff + sleep(Duration::from_secs(5)).await; + continue; + } + } + } } -#[derive(Serialize)] +async fn mail_loop_inner(db: &DbInstance) -> AppResult<()> { + // Fetch the mail configs + let configs = fetch_mail_configs(&db)?; + + // TODO: Do all configs instead of just the first + let config = &configs[0]; + let stream = + TcpStream::connect((config.imap_hostname.as_str(), config.imap_port)) + .await + .into_diagnostic()?; + + let client = async_imap::Client::new(stream); + let mut session = client + .login(&config.imap_username, &config.imap_password) + .await + .map_err(|(err, _)| err) + .into_diagnostic()?; + + // println!("Session: {:?}", session); + let mailboxes = session + .list(None, Some("*")) + .await + .into_diagnostic()? + .try_collect::>() + .await + .into_diagnostic()?; + let mailbox_names = + mailboxes.iter().map(|name| name.name()).collect::>(); + println!("mailboxes: {mailbox_names:?}"); + + let inbox = session.select("INBOX").await.into_diagnostic()?; + println!("last unseen: {:?}", inbox.unseen); + + let messages = session + .fetch("1", "RFC822") + .await + .into_diagnostic()? + .try_collect::>() + .await + .into_diagnostic()?; + println!( + "messages {:?}", + messages + .iter() + .map(|f| f.body().and_then(|t| String::from_utf8(t.to_vec()).ok())) + .collect::>() + ); + + session.logout().await.into_diagnostic()?; + + Ok(()) +} + +#[derive(Debug, Serialize)] struct MailConfig { node_id: String, imap_hostname: String, diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..79b8d81 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Michael Zhang"] +language = "en" +multilingual = false +src = "src" +title = "Panorama Docs" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..af61f13 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,5 @@ +# Summary + +- [Nodes](./nodes.md) +- [Custom Apps](./custom_apps.md) +- [Sync](./sync.md) \ No newline at end of file diff --git a/docs/src/custom_apps.md b/docs/src/custom_apps.md new file mode 100644 index 0000000..305dc50 --- /dev/null +++ b/docs/src/custom_apps.md @@ -0,0 +1,70 @@ +# Custom Apps + +
+ +**WARNING:** This is documentation for a feature that is in development. + +Almost none of this is implemented and most of it will probably change in the future. + +
+ +Custom apps allow third parties to develop functionality for panorama. +After this rolls out, most of the built-in panorama apps will also be converted into custom apps, and this feature will just be renamed "apps". + +## API + +To develop a custom app, you will need to provide: + +- + App metadata. This contains: + + - App display name. + - Version + License. + - Description + Keywords. + - Compatible panorama versions (TODO). + - Authors + Maintainers. + - Repository + Issues. + - Extra data fields for whatever + + This also includes relationships with other apps. For example: + + - Field read dependencies. If your app needs to read for example `panorama/std/time`, then it needs to list it. + - Field write dependencies. This breaks down to: + - any: the app is allowed to write to the specified field on any node + - owned: the app is allowed to write to the specified field on nodes it owns (**TODO** flesh out app ownership of nodes) + - none: the app isn't allowed to write to the specified field + +- + A list of relations your app will use. + + For example, the journal app will use `journal` for keeping track of regular pages, but may use another relation `journal_day` for keeping track of mapping days to journals. (**TODO:** not a good example, these could be combined) + + The indexes for the relations should also be listed. +- + A list of services your app will run in the background. + +## App ownership of nodes + +Apps automatically own nodes they create. + +**TODO:** is multiple ownership allowed? + +## Design notes + +- + Maybe it's best to generate the actual db relation names and have their symbolic names be mapped? This will require an extra layer of indirection but it should still make querying be doable in 2 queries. + + For example, the journal app specifies that it wants a `journal` relation. The db generates something like `journal_a41e`, registers that as a mapping for the "journal" app, and all queries will actually involve that name. + + This avoids name conflicts for separate third parties that use the same name for a relation. + + +## Built-in apps + +### Journal + +### Mail + +### Calendar + +### Contacts \ No newline at end of file diff --git a/docs/src/nodes.md b/docs/src/nodes.md new file mode 100644 index 0000000..6a941f5 --- /dev/null +++ b/docs/src/nodes.md @@ -0,0 +1,28 @@ +# Nodes + +Everything is organized into nodes. + +Each app (journal, mail, etc.) creates relations from node IDs to their information. + +For example, in a journal, there would be 2 database entries: + +- `node { id: "12345" => type: "panorama/journal/page", created_at: (...), ... }` +- `journal { node_id: "12345" => content: "blah blah blah" }` + +When retrieving its contents, a join relation is conducted and all the fields are returned. + +## Field mapping + +In the database, there is a relation mapping field names that the frontend knows about, such as `panorama/journal/page/content` to the actual relation (`journal`) + field name (`content`). These are currently all hard-coded into the migrations, but when custom apps are added they will be able to be registered. + +## Types + +The node type tells the frontend how to render it. + +**TODO:** when custom apps hit, what's the best way to package frontend React code? + +## Synthetic nodes + +These nodes basically only exist on the frontend. For example, `panorama/mail` is a special ID that renders the mail page. + +**TODO:** consider replacing these with short-circuiting the query instead of having special IDs? \ No newline at end of file diff --git a/docs/src/sync.md b/docs/src/sync.md new file mode 100644 index 0000000..63f74b5 --- /dev/null +++ b/docs/src/sync.md @@ -0,0 +1,20 @@ +# Sync + +
+ +**WARNING:** This is documentation for a feature that is in development. + +Almost none of this is implemented and most of it will probably change in the future. + +
+ +This **only** deals with syncing nodes and files between devices owned by the same person. Permissions are not considered here. + +## Design notes + +- + Devices need to have some kind of knowledge of each other's existence. This may not necessarily be exposed to apps, but the thing that's responsible for syncing needs to know which nodes have which files. +- + Slow internet connections and largely offline usage patterns need to be considered. +- + **TODO:** does this need to be deeply integrated within the panorama daemon itself or is there a way to expose enough APIs for this to just be an app?