use super::*;
use crate::peer::{self, Content, Identity, PairState, Peer, Waiting};
use std::collections::HashMap;
#[derive(Default)]
pub(crate) struct Peers {
pub identity: std::sync::Mutex<Option<Identity>>,
pub pairings: std::sync::Mutex<HashMap<String, Pairing>>,
pub wake: tokio::sync::Notify,
}
#[derive(Clone, Debug)]
pub(crate) struct Pairing {
pub state: PairState,
pub until: i64,
}
const PAIRINGS_MAX: usize = 5;
const PAIRING_KEPT: i64 = 600;
const TRIES_MAX: i64 = 20;
const TOO_LARGE: &str = "too large to send to a friend (over 8 MB)";
pub(crate) fn identity_blocking(app: &App) -> anyhow::Result<Identity> {
if let Some(id) = app.peers.identity.lock().unwrap().clone() {
return Ok(id);
}
let id = Identity::load_or_mint(&app.secrets)?;
*app.peers.identity.lock().unwrap() = Some(id.clone());
Ok(id)
}
pub(crate) fn identity_if_any(app: &App) -> Option<Identity> {
if let Some(id) = app.peers.identity.lock().unwrap().clone() {
return Some(id);
}
let id = Identity::load(&app.secrets)?;
*app.peers.identity.lock().unwrap() = Some(id.clone());
Some(id)
}
pub(crate) fn my_name(paths: &Paths) -> String {
std::fs::read_to_string(paths.config_dir.join("peer-name"))
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "a friend".to_string())
})
}
fn set_my_name(paths: &Paths, name: &str) {
let name: String = name.trim().chars().take(60).collect();
if name.is_empty() {
return;
}
let _ = std::fs::create_dir_all(&paths.config_dir);
let _ = std::fs::write(paths.config_dir.join("peer-name"), name);
}
fn peers_moved(app: &App) {
emit(app, "peers", json!({}));
}
pub(crate) async fn peers_list(State(app): S) -> Response {
let friends = app.store.peers().unwrap_or_default();
let notes = app.store.peer_notes().unwrap_or_default();
let offers = app.store.peer_offers().unwrap_or_default();
Json(json!({
"friends": friends,
"notes": notes,
"offers": offers,
"me": { "name": my_name(&app.paths) },
"relay": peer::relay(),
}))
.into_response()
}
#[derive(Deserialize, Default)]
pub(crate) struct PairBody {
#[serde(default)]
pub(crate) name: String,
#[serde(default)]
pub(crate) code: String,
}
fn too_many(app: &App) -> bool {
let now = crate::store::now();
let mut p = app.peers.pairings.lock().unwrap();
p.retain(|_, x| x.until + PAIRING_KEPT > now);
p.values().filter(|x| x.state == PairState::Waiting).count() >= PAIRINGS_MAX
}
pub(crate) async fn pair_start(
State(app): S,
headers: HeaderMap,
Json(b): Json<PairBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
if too_many(&app) {
return (
StatusCode::TOO_MANY_REQUESTS,
Json(json!({ "error": "a few codes are already waiting; let one run out" })),
)
.into_response();
}
let code = match peer::mint_code() {
Ok(c) => c,
Err(e) => return err(e),
};
set_my_name(&app.paths, &b.name);
let until = start_pairing(&app, code.clone(), my_name(&app.paths));
Json(json!({ "code": code, "until": until })).into_response()
}
pub(crate) async fn pair_join(
State(app): S,
headers: HeaderMap,
Json(b): Json<PairBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
let code = match peer::normalize_code(&b.code) {
Ok(c) => c,
Err(why) => {
return (
StatusCode::UNPROCESSABLE_ENTITY,
Json(json!({ "error": why })),
)
.into_response()
}
};
if too_many(&app) {
return (
StatusCode::TOO_MANY_REQUESTS,
Json(json!({ "error": "a few codes are already waiting; let one run out" })),
)
.into_response();
}
if app.peers.pairings.lock().unwrap().contains_key(&code) {
return (
StatusCode::CONFLICT,
Json(json!({ "error": "that is this snyvi's own code" })),
)
.into_response();
}
set_my_name(&app.paths, &b.name);
let until = start_pairing(&app, code.clone(), my_name(&app.paths));
Json(json!({ "code": code, "until": until })).into_response()
}
pub(crate) async fn pair_state(State(app): S, Path(code): Path<String>) -> Response {
let p = app.peers.pairings.lock().unwrap().get(&code).cloned();
match p {
Some(p) => Json(json!({ "pairing": p.state, "until": p.until })).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
fn start_pairing(app: &Arc<App>, code: String, name: String) -> i64 {
let until = crate::store::now() + peer::CODE_TTL;
app.peers.pairings.lock().unwrap().insert(
code.clone(),
Pairing {
state: PairState::Waiting,
until,
},
);
let app = app.clone();
tokio::spawn(async move {
let app2 = app.clone();
let c = code.clone();
let r = tokio::task::spawn_blocking(move || -> anyhow::Result<(Peer, String)> {
let me = identity_blocking(&app2)?;
peer::pair(&me, &c, &name, until)
})
.await;
let state = match r {
Ok(Ok((p, emoji))) => match app.store.pin_peer(&p) {
Ok(pinned) => {
eprintln!("snyvi: paired with {}", pinned.name);
peers_moved(&app);
app.peers.wake.notify_one();
PairState::Done {
emoji,
name: pinned.name,
peer: pinned.id,
}
}
Err(e) => PairState::Failed { why: e.to_string() },
},
Ok(Err(e)) => PairState::Failed {
why: format!("{e:#}"),
},
Err(e) => PairState::Failed { why: e.to_string() },
};
if let Some(p) = app.peers.pairings.lock().unwrap().get_mut(&code) {
p.state = state.clone();
}
emit(&app, "pairing", json!({ "code": code, "pairing": state }));
});
until
}
#[derive(Deserialize, Default)]
pub(crate) struct NameBody {
#[serde(default)]
pub(crate) name: String,
}
pub(crate) async fn peer_rename(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
Json(b): Json<NameBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
match app.store.rename_peer(id, &b.name) {
Ok(true) => {
if let Ok(Some(p)) = app.store.peer(id) {
let _ = app
.store
.rename_project_by_root(&p.project_root(), &p.project_name());
}
peers_moved(&app);
StatusCode::NO_CONTENT.into_response()
}
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => err(e),
}
}
#[derive(Deserialize, Default)]
pub(crate) struct MuteBody {
#[serde(default)]
pub(crate) muted: bool,
}
pub(crate) async fn peer_mute(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
Json(b): Json<MuteBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
match app.store.mute_peer(id, b.muted) {
Ok(true) => {
peers_moved(&app);
StatusCode::NO_CONTENT.into_response()
}
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => err(e),
}
}
pub(crate) async fn peer_remove(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
match app.store.remove_peer(id) {
Ok(true) => {
peers_moved(&app);
app.peers.wake.notify_one();
StatusCode::NO_CONTENT.into_response()
}
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => err(e),
}
}
pub(crate) async fn peer_restore(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
match app.store.restore_peer(id) {
Ok(true) => {
peers_moved(&app);
app.peers.wake.notify_one();
StatusCode::NO_CONTENT.into_response()
}
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => err(e),
}
}
#[derive(Deserialize, Default)]
pub(crate) struct TextBody {
#[serde(default)]
pub(crate) text: String,
}
pub(crate) async fn peer_note(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
Json(b): Json<TextBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
let text: String = b.text.trim().chars().take(peer::NOTE_CHARS).collect();
if text.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "a line of text" })),
)
.into_response();
}
let Ok(Some(p)) = app.store.peer(id) else {
return StatusCode::NOT_FOUND.into_response();
};
if p.removed_at != 0 {
return (
StatusCode::CONFLICT,
Json(json!({ "error": "this friend was removed; Restore them first" })),
)
.into_response();
}
let app2 = app.clone();
let r = tokio::task::spawn_blocking(move || -> anyhow::Result<bool> {
let me = identity_blocking(&app2)?;
let content = Content::Note {
text: text.clone(),
name: my_name(&app2.paths),
};
let frame = peer::seal(&me, &p, &content, b"")?;
let mut nonce = [0u8; 16];
getrandom::fill(&mut nonce).map_err(|e| anyhow::anyhow!("{e}"))?;
let id = blake3::hash(&nonce).to_hex().to_string();
let sent = peer::deposit(&p.sign_key, &id, &frame)?;
if sent {
let _ = app2.store.touch_peer(p.id, false);
}
Ok(sent)
})
.await;
match r {
Ok(Ok(true)) => {
peers_moved(&app);
Json(json!({ "sent": true })).into_response()
}
Ok(Ok(false)) => (
StatusCode::CONFLICT,
Json(json!({ "error": "their mailbox is full; try later" })),
)
.into_response(),
Ok(Err(e)) => (
StatusCode::BAD_GATEWAY,
Json(json!({ "error": format!("{e:#}") })),
)
.into_response(),
Err(e) => err(anyhow::anyhow!(e)),
}
}
#[derive(Deserialize, Default)]
pub(crate) struct SettleBody {
#[serde(default)]
pub(crate) what: String,
}
pub(crate) async fn peer_note_settle(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
Json(b): Json<SettleBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
match app.store.settle_peer_note(id, &b.what) {
Ok(true) => {
emit(&app, "peernotes", json!({}));
StatusCode::NO_CONTENT.into_response()
}
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => err(e),
}
}
#[derive(Deserialize, Default)]
pub(crate) struct SendBody {
#[serde(default)]
pub(crate) peer: i64,
}
pub(crate) async fn doc_send(
State(app): S,
headers: HeaderMap,
Path(id): Path<String>,
Json(b): Json<SendBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
let Ok(Some(doc)) = app.store.get(&id) else {
return StatusCode::NOT_FOUND.into_response();
};
let Ok(Some(p)) = app.store.peer(b.peer) else {
return (
StatusCode::NOT_FOUND,
Json(json!({ "error": "no such friend" })),
)
.into_response();
};
if p.removed_at != 0 {
return (
StatusCode::CONFLICT,
Json(json!({ "error": "this friend was removed; Restore them first" })),
)
.into_response();
}
if doc.size as usize > peer::SEND_MAX {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({ "error": TOO_LARGE })),
)
.into_response();
}
let frame = match app.store.peer_queue(&p, &id) {
Ok(f) => f,
Err(e) => return err(e),
};
let app2 = app.clone();
let sent = tokio::task::spawn_blocking(move || flush_outbox(&app2, Some(&frame)))
.await
.unwrap_or(false);
peers_moved(&app);
Json(json!({ "sent": sent, "to": p.name })).into_response()
}
#[derive(Deserialize, Default)]
pub(crate) struct AnswerBody {
#[serde(default)]
pub(crate) send: bool,
}
pub(crate) async fn offer_answer(
State(app): S,
headers: HeaderMap,
Path(id): Path<i64>,
Json(b): Json<AnswerBody>,
) -> Response {
if let Some(no) = refuse_reader(&app, &headers) {
return no;
}
let Ok(Some(o)) = app.store.peer_offer_get(id) else {
return StatusCode::NOT_FOUND.into_response();
};
if b.send {
if let Ok(Some(doc)) = app.store.get(&o.doc_id) {
if doc.size as usize > peer::SEND_MAX {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({ "error": TOO_LARGE })),
)
.into_response();
}
}
}
if let Err(e) = app.store.answer_peer_offer(id, b.send) {
return err(e);
}
emit(&app, "peeroffers", json!({}));
if !b.send {
return Json(json!({ "sent": false })).into_response();
}
let Ok(Some(p)) = app.store.peer(o.peer_id) else {
return StatusCode::NOT_FOUND.into_response();
};
let frame = match app.store.peer_queue(&p, &o.doc_id) {
Ok(f) => f,
Err(e) => return err(e),
};
let app2 = app.clone();
let sent = tokio::task::spawn_blocking(move || flush_outbox(&app2, Some(&frame)))
.await
.unwrap_or(false);
peers_moved(&app);
Json(json!({ "sent": sent, "to": p.name })).into_response()
}
#[derive(Deserialize, Default)]
pub(crate) struct OfferBody {
#[serde(default)]
pub(crate) to: String,
#[serde(default)]
pub(crate) doc: String,
#[serde(default)]
pub(crate) by: String,
}
pub(crate) async fn pane_offer(
State(app): S,
headers: HeaderMap,
Path(id): Path<String>,
Json(b): Json<OfferBody>,
) -> Response {
let placed = match agent_pane(&app, &headers, &id) {
Ok(p) => p,
Err(no) => return *no,
};
let to = b.to.trim();
if to.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "offer_document needs the friend's name" })),
)
.into_response();
}
let Ok(Some(p)) = app.store.peer_by_name(to) else {
return (StatusCode::NOT_FOUND, Json(json!({ "error": format!("the user has no friend called {to}; they pair from Home") }))).into_response();
};
let Ok(Some(doc)) = app.store.get(b.doc.trim()) else {
return (
StatusCode::NOT_FOUND,
Json(json!({ "error": "no document by that id; send_document answers with one" })),
)
.into_response();
};
match app.store.peer_offer(p.id, &doc.id, &id, &b.by) {
Ok(offer_id) => {
emit(
&app,
"peeroffers",
json!({ "offer": offer_id, "to": p.name, "title": doc.title, "by": b.by, "desk": placed.desk_name }),
);
(
StatusCode::CREATED,
Json(json!({ "to": p.name, "title": doc.title })),
)
.into_response()
}
Err(e) => err(e),
}
}
pub(crate) fn flush_outbox(app: &App, only: Option<&str>) -> bool {
let Ok(rows) = app.store.peer_unsent() else {
return false;
};
let Ok(me) = identity_blocking(app) else {
return false;
};
let name = my_name(&app.paths);
let mut all = true;
let mut moved = false;
for (frame_id, peer_id, doc_id, tries) in rows {
if only.is_some_and(|o| o != frame_id) {
continue;
}
if tries >= TRIES_MAX {
all = false;
continue;
}
let went = match send_one(app, &me, &name, &frame_id, peer_id, &doc_id) {
Ok(true) => {
let _ = app.store.peer_sent(&frame_id);
let _ = app.store.touch_peer(peer_id, false);
moved = true;
true
}
Ok(false) => {
let _ = app.store.peer_failed(&frame_id, "their mailbox is full");
false
}
Err(e) => {
let _ = app.store.peer_failed(&frame_id, &format!("{e:#}"));
false
}
};
all &= went;
}
if moved && only.is_none() {
peers_moved(app);
}
all
}
fn send_one(
app: &App,
me: &Identity,
name: &str,
frame_id: &str,
peer_id: i64,
doc_id: &str,
) -> anyhow::Result<bool> {
let p = app
.store
.peer(peer_id)?
.ok_or_else(|| anyhow::anyhow!("no such friend"))?;
let Some(doc) = app.store.get(doc_id)? else {
app.store.peer_sent(frame_id)?;
return Ok(true);
};
let bytes = std::fs::read(app.store.src_path(doc_id))?;
if bytes.len() > peer::SEND_MAX {
anyhow::bail!("{TOO_LARGE}");
}
let content = Content::Document {
title: doc.title.clone(),
lang: doc.lang.clone(),
file: doc
.source_path
.as_deref()
.and_then(|sp| std::path::Path::new(sp).file_name())
.map(|f| f.to_string_lossy().to_string()),
name: name.to_string(),
id: doc.id.clone(),
};
let frame = peer::seal(me, &p, &content, &bytes)?;
peer::deposit(&p.sign_key, frame_id, &frame)
}
pub(crate) fn take_in(
app: &Arc<App>,
me: &Identity,
w: &Waiting,
bytes: Vec<u8>,
) -> anyhow::Result<bool> {
let friend = app
.store
.peer_by_key(&w.sender)?
.filter(|p| p.removed_at == 0);
let Some(p) = friend else {
return Ok(false);
};
if app.store.peer_taken(&w.id)? {
return Ok(false);
}
if w.size as usize > peer::FRAME_MAX || bytes.len() > peer::FRAME_MAX {
return Ok(false);
}
match peer::open(me, &p, &bytes) {
Ok((content, body)) => {
if let Err(e) = arrived(app, &p, content, body) {
eprintln!("snyvi: a document from {} could not be kept: {e:#}", p.name);
return Ok(false);
}
app.store.peer_take(&w.id)?;
Ok(true)
}
Err(e) => {
eprintln!("snyvi: a frame from {} was dropped: {e:#}", p.name);
Ok(false)
}
}
}
pub(crate) fn bring_in(app: &Arc<App>, me: &Identity) -> anyhow::Result<usize> {
let waiting = peer::inbox(me)?;
let mut n = 0;
for w in waiting {
let friend = app
.store
.peer_by_key(&w.sender)?
.filter(|p| p.removed_at == 0);
let fetch =
friend.is_some() && !app.store.peer_taken(&w.id)? && w.size as usize <= peer::FRAME_MAX;
if fetch {
let bytes = peer::fetch(me, &w.id)?;
if take_in(app, me, &w, bytes)? {
n += 1;
}
}
peer::ack(me, &w.id)?;
}
Ok(n)
}
fn arrived(app: &Arc<App>, p: &Peer, content: Content, body: Vec<u8>) -> anyhow::Result<()> {
let _ = app.store.touch_peer(p.id, true);
match content {
Content::Document {
title, lang, file, ..
} => {
let payload = Payload {
title: Some(title),
lang,
origin: Some("peer".into()),
sender: Some(p.name.clone()),
peer: Some(crate::receive::FromPeer {
name: p.name.clone(),
sign_key: p.sign_key.clone(),
bytes: body,
file,
}),
..Default::default()
};
let received = receive::receive(&app.store, &app.renderer, payload)?;
if p.muted {
let _ = app.store.mark_read(&received.doc.id);
}
let mut ev = doc_event(app, &received);
ev["from"] = json!(p.name);
ev["quiet"] = json!(p.muted);
emit(app, "doc", ev);
if !p.muted && !received.existing {
eprintln!("snyvi: {} sent \"{}\"", p.name, received.doc.title);
}
}
Content::Note { text, .. } => {
app.store.peer_note_arrived(p.id, &text)?;
emit(
app,
"peernotes",
json!({ "from": p.name, "quiet": p.muted }),
);
}
}
Ok(())
}
pub(crate) fn sweep_offers(app: &App) {
let Ok(offers) = app.store.peer_offers() else {
return;
};
let mut dropped = 0;
for o in offers {
if !o.pane.is_empty() && !app.panes.is_running(&o.pane) {
dropped += app.store.drop_peer_offers_of(&o.pane).unwrap_or(0);
}
}
if dropped > 0 {
emit(app, "peeroffers", json!({}));
}
}