use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::Duration;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
use okbot_core::{CancellationToken, PeerError};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tokio::sync::{Notify, oneshot};
use crate::a2a::{self, code};
use crate::node::{Inner, Team, lock};
const POLL_WAIT: Duration = Duration::from_secs(20);
const FORWARD_WAIT: Duration = Duration::from_secs(25);
const MAX_QUEUED: usize = 64;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Item {
pub(crate) id: String,
pub(crate) authorization: String,
pub(crate) body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Answer {
pub(crate) status: u16,
#[serde(default)]
pub(crate) signature: Option<String>,
pub(crate) body: String,
}
type Queue = (VecDeque<Item>, Arc<Notify>);
#[derive(Default)]
pub(crate) struct Relay {
queues: std::sync::Mutex<HashMap<(String, String), Queue>>,
waiting: std::sync::Mutex<HashMap<String, oneshot::Sender<Answer>>>,
}
impl Relay {
fn notify(&self, key: &(String, String)) -> Arc<Notify> {
lock(&self.queues).entry(key.clone()).or_default().1.clone()
}
}
type Refusal = (i64, String);
fn refused(message: impl Into<String>) -> Refusal {
(code::REFUSED, message.into())
}
pub(crate) async fn handle(
inner: &Arc<Inner>,
team: &Team,
caller: &str,
method: &str,
params: Value,
) -> Option<Result<Value, Refusal>> {
let result = match method {
"okbot/relay.forward" => forward(inner, team, caller, params).await,
"okbot/relay.poll" => poll(inner, team, caller).await,
"okbot/relay.reply" => reply(inner, team, caller, params),
_ => return None,
};
Some(result)
}
fn relayed_by_me(team: &Team, member: &str) -> bool {
team.manifest()
.members
.get(member)
.and_then(|m| m.relay.as_deref())
== Some(team.config.member.as_str())
}
async fn forward(
inner: &Inner,
team: &Team,
caller: &str,
params: Value,
) -> Result<Value, Refusal> {
#[derive(Deserialize)]
struct Forward {
to: String,
authorization: String,
body: String,
}
let Ok(f) = serde_json::from_value::<Forward>(params) else {
return Err((code::INVALID_PARAMS, "invalid forward".into()));
};
if !relayed_by_me(team, &f.to) {
return Err(refused(format!("this bot does not relay `{}`", f.to)));
}
if !team.manifest().may_call(caller, &f.to) {
return Err(refused(format!("no link lets `{caller}` call `{}`", f.to)));
}
let key = (team.config.id.clone(), f.to.clone());
let id = hex::encode(rand::random::<[u8; 12]>());
let (tx, rx) = oneshot::channel();
{
let mut queues = lock(&inner.relay.queues);
let (queue, notify) = queues.entry(key).or_default();
if queue.len() >= MAX_QUEUED {
return Err(refused(format!("`{}` has too many requests waiting", f.to)));
}
queue.push_back(Item {
id: id.clone(),
authorization: f.authorization,
body: f.body,
});
lock(&inner.relay.waiting).insert(id.clone(), tx);
notify.notify_one();
}
let answer = tokio::time::timeout(FORWARD_WAIT, rx).await;
lock(&inner.relay.waiting).remove(&id);
match answer {
Ok(Ok(answer)) => Ok(serde_json::to_value(answer).expect("answers serialize")),
_ => {
let mut queues = lock(&inner.relay.queues);
if let Some((queue, _)) = queues.get_mut(&(team.config.id.clone(), f.to.clone())) {
queue.retain(|i| i.id != id);
}
Err(refused(format!(
"`{}` did not answer through the relay",
f.to
)))
}
}
}
async fn poll(inner: &Arc<Inner>, team: &Team, caller: &str) -> Result<Value, Refusal> {
if !relayed_by_me(team, caller) {
return Err(refused("this bot is not your relay"));
}
let key = (team.config.id.clone(), caller.to_string());
let notify = inner.relay.notify(&key);
let deadline = tokio::time::Instant::now() + POLL_WAIT;
loop {
let notified = notify.notified();
let taken: Vec<Item> = lock(&inner.relay.queues)
.get_mut(&key)
.map(|(q, _)| q.drain(..).collect())
.unwrap_or_default();
if !taken.is_empty() {
return Ok(json!({"requests": taken}));
}
if tokio::time::timeout_at(deadline, notified).await.is_err() {
return Ok(json!({"requests": []}));
}
}
}
fn reply(inner: &Arc<Inner>, team: &Team, caller: &str, params: Value) -> Result<Value, Refusal> {
#[derive(Deserialize)]
struct Reply {
id: String,
#[serde(flatten)]
answer: Answer,
}
if !relayed_by_me(team, caller) {
return Err(refused("this bot is not your relay"));
}
let Ok(r) = serde_json::from_value::<Reply>(params) else {
return Err((code::INVALID_PARAMS, "invalid reply".into()));
};
if let Some(tx) = lock(&inner.relay.waiting).remove(&r.id) {
let _ = tx.send(r.answer);
}
Ok(json!({}))
}
pub(crate) async fn send(
inner: &Inner,
team: &Team,
relay: &str,
to: &str,
authorization: &str,
body: &[u8],
) -> Result<(u16, Option<String>, Vec<u8>), PeerError> {
let manifest = team.manifest();
let target = manifest
.members
.get(to)
.ok_or_else(|| PeerError::NoRoute(to.to_string()))?;
let box_key = crate::node::member_box_key(target)
.map_err(|e| PeerError::Unreachable(format!("`{to}`: {e}")))?;
let sealed = B64.encode(crate::seal::seal(&box_key, body));
let params = json!({"to": to, "authorization": authorization, "body": sealed});
let serving = inner.local_server.load(std::sync::atomic::Ordering::SeqCst);
let value = if relay == team.config.member && serving {
forward(inner, team, &team.config.member, params)
.await
.map_err(|(_, message)| PeerError::Refused(message))?
} else {
let request = a2a::Request::new("okbot/relay.forward", params);
inner.call(team, relay, &request).await?
};
let answer: Answer = serde_json::from_value(value)
.map_err(|_| PeerError::Refused(format!("`{relay}` sent an invalid relay answer")))?;
let body = B64
.decode(&answer.body)
.ok()
.and_then(|sealed| crate::seal::open(&inner.box_key, &sealed).ok())
.ok_or_else(|| {
PeerError::Refused(format!(
"`{to}`'s answer through the relay is not sealed for this bot"
))
})?;
Ok((answer.status, answer.signature, body))
}
async fn run_sealed(
inner: &Arc<Inner>,
team_id: &str,
item: &Item,
) -> (u16, Option<String>, String) {
let refused = |status: u16, text: &str| (status, None, text.to_string());
let Some(plain) = B64
.decode(&item.body)
.ok()
.and_then(|sealed| crate::seal::open(&inner.box_key, &sealed).ok())
else {
return refused(400, "requests through a relay must be sealed for this bot");
};
let Some(caller_key) = item
.authorization
.strip_prefix(crate::node::SCHEME)
.and_then(|t| crate::token::peek(t).ok())
.and_then(|claims| {
let manifest = inner.teams.get(team_id)?.manifest();
crate::node::member_box_key(manifest.members.get(&claims.iss)?).ok()
})
else {
return refused(401, "the caller has no box_key to answer to");
};
let reply = crate::inbound::handle(
inner,
team_id,
"POST",
Some(&item.authorization),
None,
&plain,
)
.await;
let sealed = B64.encode(crate::seal::seal(&caller_key, &reply.body));
(reply.status, reply.signature, sealed)
}
pub(crate) async fn run(inner: Arc<Inner>, stop: CancellationToken) {
let mut tasks = tokio::task::JoinSet::new();
for id in inner.teams.keys().cloned() {
let inner = inner.clone();
let stop = stop.clone();
tasks.spawn(async move { poll_loop(inner, id, stop).await });
}
while tasks.join_next().await.is_some() {}
}
async fn poll_loop(inner: Arc<Inner>, team_id: String, stop: CancellationToken) {
let mut backoff = Duration::from_secs(1);
loop {
if stop.is_cancelled() {
return;
}
let team = &inner.teams[&team_id];
let relay = team
.manifest()
.members
.get(&team.config.member)
.and_then(|m| m.relay.clone());
let Some(relay) = relay else {
tokio::select! {
() = tokio::time::sleep(crate::node::REFRESH) => continue,
() = stop.cancelled() => return,
}
};
let request = a2a::Request::new("okbot/relay.poll", json!({}));
let polled = tokio::select! {
r = inner.call(team, &relay, &request) => r,
() = stop.cancelled() => return,
};
let items = match polled {
Ok(value) => {
backoff = Duration::from_secs(1);
serde_json::from_value::<Vec<Item>>(value["requests"].clone()).unwrap_or_default()
}
Err(error) => {
tracing::warn!(team = %team_id, %relay, %error, "cannot poll the relay");
tokio::select! {
() = tokio::time::sleep(backoff) => {}
() = stop.cancelled() => return,
}
backoff = (backoff * 2).min(Duration::from_secs(60));
continue;
}
};
for item in items {
let inner = inner.clone();
let team_id = team_id.clone();
let relay = relay.clone();
tokio::spawn(async move {
let (status, signature, body) = run_sealed(&inner, &team_id, &item).await;
let request = a2a::Request::new(
"okbot/relay.reply",
json!({"id": item.id, "status": status, "signature": signature, "body": body}),
);
let team = &inner.teams[&team_id];
if let Err(error) = inner.call(team, &relay, &request).await {
tracing::warn!(%error, "cannot pass an answer back through the relay");
}
});
}
}
}