docs
|
@ -1,3 +1,7 @@
|
|||
[*]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[Makefile]
|
||||
indent_size = 4
|
||||
indent_style = tab
|
131
Cargo.lock
generated
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
deploy-docs:
|
||||
mdbook build docs
|
||||
rsync -azrP docs/book/ root@veil:/home/blogDeploy/public/panorama
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 965 B |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 50 KiB |
|
@ -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"]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
book
|
6
docs/book.toml
Normal 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
|
@ -0,0 +1,5 @@
|
|||
# Summary
|
||||
|
||||
- [Nodes](./nodes.md)
|
||||
- [Custom Apps](./custom_apps.md)
|
||||
- [Sync](./sync.md)
|
70
docs/src/custom_apps.md
Normal 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
|
@ -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
|
@ -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?
|