This commit is contained in:
Michael Zhang 2024-05-27 15:56:39 -05:00
parent 63618b7dce
commit abeb41857f
28 changed files with 346 additions and 3 deletions

View file

@ -1,3 +1,7 @@
[*]
indent_size = 2
indent_style = space
indent_style = space
[Makefile]
indent_size = 4
indent_style = tab

131
Cargo.lock generated
View file

@ -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"

3
Makefile Normal file
View file

@ -0,0 +1,3 @@
deploy-docs:
mdbook build docs
rsync -azrP docs/book/ root@veil:/home/blogDeploy/public/panorama

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -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"]

View file

@ -6,6 +6,7 @@ use axum::{
pub type AppResult<T, E = AppError> = std::result::Result<T, E>;
// Make our own error that wraps `anyhow::Error`.
#[derive(Debug)]
pub struct AppError(miette::Report);
// Tell axum how to convert `AppError` into a response.

View file

@ -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::<Vec<_>>()
.await
.into_diagnostic()?;
let mailbox_names =
mailboxes.iter().map(|name| name.name()).collect::<Vec<_>>();
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::<Vec<_>>()
.await
.into_diagnostic()?;
println!(
"messages {:?}",
messages
.iter()
.map(|f| f.body().and_then(|t| String::from_utf8(t.to_vec()).ok()))
.collect::<Vec<_>>()
);
session.logout().await.into_diagnostic()?;
Ok(())
}
#[derive(Debug, Serialize)]
struct MailConfig {
node_id: String,
imap_hostname: String,

1
docs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
book

6
docs/book.toml Normal file
View file

@ -0,0 +1,6 @@
[book]
authors = ["Michael Zhang"]
language = "en"
multilingual = false
src = "src"
title = "Panorama Docs"

5
docs/src/SUMMARY.md Normal file
View file

@ -0,0 +1,5 @@
# Summary
- [Nodes](./nodes.md)
- [Custom Apps](./custom_apps.md)
- [Sync](./sync.md)

70
docs/src/custom_apps.md Normal file
View file

@ -0,0 +1,70 @@
# Custom Apps
<div class="warning">
**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.
</div>
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

28
docs/src/nodes.md Normal file
View file

@ -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?

20
docs/src/sync.md Normal file
View file

@ -0,0 +1,20 @@
# Sync
<div class="warning">
**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.
</div>
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?