use vti_common::error::AppError;
use vti_rooms::mls::RoomGroup;
use crate::auth::AuthClaims;
use crate::operations::room_groups::{self, Member, RoomGroupRecord};
use crate::server::AppState;
async fn may_act_as(state: &AppState, auth: &AuthClaims, did: &str) -> Result<bool, AppError> {
let stores = crate::operations::persona::PersonaStores::from_state(state);
Ok(
crate::operations::persona::persona_signing_key(stores, auth, did)
.await?
.is_some(),
)
}
async fn attribute_legacy(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
record: &RoomGroupRecord,
) -> Result<Option<String>, AppError> {
let leaf_is_member = RoomGroup::restore(&record.snapshot)
.map(|g| g.leaf_of(&record.member_did).is_some())
.unwrap_or(false);
if leaf_is_member && may_act_as(state, auth, &record.member_did).await? {
return Ok(Some(record.member_did.clone()));
}
if record.member_did == auth.did {
return match crate::operations::room_oracle::principal(state, auth, room_id).await {
Ok(p) => Ok(Some(p.did)),
Err(e @ AppError::Internal(_)) => Err(e),
Err(_) => Ok(None),
};
}
Ok(None)
}
pub async fn migrate_for(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
) -> Result<(), AppError> {
let groups = &state.room_groups_ks;
let Some(record) = room_groups::legacy_group(groups, room_id).await? else {
return Ok(());
};
let Some(persona) = attribute_legacy(state, auth, room_id, &record).await? else {
return Ok(());
};
room_groups::migrate_legacy(groups, &Member::new(room_id, &persona)?).await?;
Ok(())
}
pub async fn member_as(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
persona: &str,
) -> Result<Member, AppError> {
migrate_for(state, auth, room_id).await?;
Member::new(room_id, persona)
}
pub async fn acting(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
) -> Result<Option<Member>, AppError> {
migrate_for(state, auth, room_id).await?;
let mut mine = Vec::new();
for m in room_groups::holders(&state.room_groups_ks, room_id).await? {
if may_act_as(state, auth, m.persona()).await? {
mine.push(m);
}
}
match mine.len() {
0 => Ok(None),
1 => Ok(mine.pop()),
n => Err(AppError::Conflict(format!(
"you may act as {n} personas holding group state for room `{room_id}`; which one \
is acting is not a question this can answer safely — act from the context of \
the one you mean"
))),
}
}
pub async fn require_acting(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
) -> Result<Member, AppError> {
require_acting_as(state, auth, room_id, None).await
}
pub const MEMBER_EXT: &str = "org.openvtc.rooms";
pub fn named_member(payload: &serde_json::Value) -> Result<Option<String>, String> {
let Some(ns) = payload.get("ext").and_then(|e| e.get(MEMBER_EXT)) else {
return Ok(None);
};
match ns.get("member") {
None => Ok(None),
Some(serde_json::Value::String(did)) if did.starts_with("did:") && !did.contains('#') => {
Ok(Some(did.clone()))
}
Some(_) => Err(format!(
"`ext[\"{MEMBER_EXT}\"].member` must be the DID of the member to act as"
)),
}
}
pub async fn acting_as(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
named: Option<&str>,
) -> Result<Option<Member>, AppError> {
let Some(named) = named else {
return acting(state, auth, room_id).await;
};
migrate_for(state, auth, room_id).await?;
for m in room_groups::holders(&state.room_groups_ks, room_id).await? {
if m.persona() == named && may_act_as(state, auth, named).await? {
return Ok(Some(m));
}
}
Ok(None)
}
pub async fn require_acting_as(
state: &AppState,
auth: &AuthClaims,
room_id: &str,
named: Option<&str>,
) -> Result<Member, AppError> {
acting_as(state, auth, room_id, named)
.await?
.ok_or_else(|| match named {
Some(did) => AppError::NotFound(format!(
"this VTA holds no group state for room `{room_id}` as `{did}`"
)),
None => AppError::NotFound(format!(
"this VTA holds no group state for room `{room_id}`"
)),
})
}
pub async fn held_by_caller(state: &AppState, auth: &AuthClaims) -> Result<Vec<Member>, AppError> {
let groups = &state.room_groups_ks;
for room_id in room_groups::legacy_rooms(groups).await? {
migrate_for(state, auth, &room_id).await?;
}
let mut mine = Vec::new();
for m in room_groups::held_members(groups).await? {
if may_act_as(state, auth, m.persona()).await? {
mine.push(m);
}
}
Ok(mine)
}