twonly-app/rust/src/api/groups.rs
2026-08-27 01:47:10 +02:00

166 lines
5.1 KiB
Rust

/*
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
*
*/
use crate::api::proto::client::EncryptedGroupState;
use crate::api::proto::http_requests::update_group_state;
use crate::api::proto::http_requests::{
AppendGroupState, GroupState, NewGroupState, UpdateGroupState,
};
use crate::bridge::api::RustApi;
use crate::error::{Result, TwonlyError};
use crate::services::groups::crypto;
use crate::services::groups::model::GroupRecord;
use prost::Message;
use rand::SeedableRng;
pub struct GroupApi;
impl GroupApi {
pub(crate) async fn fetch_group_state(group_id: &str) -> Result<Option<GroupState>> {
let response = client()?
.get(url(&format!("state/{group_id}")))
.send()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(None);
}
if !response.status().is_success() {
return Err(TwonlyError::Generic(format!(
"group state returned {}",
response.status()
)));
}
Ok(Some(
GroupState::decode(
response
.bytes()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?,
)
.map_err(|e| TwonlyError::Generic(e.to_string()))?,
))
}
pub(crate) async fn update_remote(
group: &GroupRecord,
version: u64,
state: &EncryptedGroupState,
add_admin: Option<Vec<u8>>,
remove_admin: Option<Vec<u8>>,
) -> Result<()> {
let identity = group.identity()?;
let public_key = identity.identity_key().serialize().to_vec();
let update = update_group_state::UpdateTbs {
version_id: version + 1,
encrypted_group_state: crypto::encrypt(group.state_key()?, &state.encode_to_vec())?,
public_key: public_key.clone(),
remove_admin,
add_admin,
nonce: Self::get_challenge(&public_key).await?,
};
let mut rng = rand::rngs::StdRng::from_os_rng();
let signature = identity
.private_key()
.calculate_signature(&update.encode_to_vec(), &mut rng)
.map_err(|e| TwonlyError::Signal(e.to_string()))?
.into_vec();
update_state(UpdateGroupState {
update: Some(update),
signature,
})
.await
}
pub(crate) async fn load_state(group: &GroupRecord) -> Result<(u64, EncryptedGroupState)> {
let server = GroupApi::fetch_group_state(&group.group_id)
.await?
.ok_or_else(|| TwonlyError::Generic("group state does not exist".into()))?;
let raw = crypto::decrypt(group.state_key()?, &server.encrypted_group_state)?;
Ok((
server.version_id,
EncryptedGroupState::decode(raw.as_slice())
.map_err(|error| TwonlyError::Generic(error.to_string()))?,
))
}
pub(crate) async fn create(value: NewGroupState) -> Result<()> {
success(
client()?
.post(url("state"))
.body(value.encode_to_vec())
.send()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?,
)
.await
}
pub(crate) async fn append(value: AppendGroupState) -> Result<()> {
success(
client()?
.post(url("state/append"))
.body(value.encode_to_vec())
.send()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?,
)
.await
}
pub(crate) async fn get_challenge(public_key: &[u8]) -> Result<Vec<u8>> {
let response = client()?
.get(url(&format!("challenge/{}", hex::encode(public_key))))
.send()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?;
if !response.status().is_success() {
return Err(TwonlyError::Generic(format!(
"group challenge returned {}",
response.status()
)));
}
response
.bytes()
.await
.map(|v| v.to_vec())
.map_err(|e| TwonlyError::Generic(e.to_string()))
}
}
fn url(path: &str) -> String {
format!("{}group/{path}", RustApi::api_base_url("https".into()))
}
fn client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| TwonlyError::Generic(e.to_string()))
}
async fn success(response: reqwest::Response) -> Result<()> {
if response.status().is_success() {
Ok(())
} else {
Err(TwonlyError::Generic(format!(
"group server returned {}",
response.status()
)))
}
}
async fn update_state(value: UpdateGroupState) -> Result<()> {
success(
client()?
.patch(url("state"))
.body(value.encode_to_vec())
.send()
.await
.map_err(|e| TwonlyError::Generic(e.to_string()))?,
)
.await
}