public-appservice 0.2.2

An appservice to make Matrix spaces publicly accessible.
Documentation
use axum::{
    Json,
    extract::{Path, State},
    response::IntoResponse,
};

use ruma::RoomAliasId;
use ruma::api::client::space::SpaceHierarchyRoomsChunk;

use crate::error::AppserviceError;

use crate::AppState;
use serde_json::json;
use std::sync::Arc;

use crate::appservice::RoomSummary;

use crate::cache::CacheKey;

pub async fn spaces(
    State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppserviceError> {
    let default_spaces = state.config.spaces.default.clone();

    if default_spaces.is_empty() {
        return Err(AppserviceError::AppserviceError(
            "No default spaces configured".to_string(),
        ));
    }

    if !state.config.spaces.cache {
        // if caching is disabled, fetch directly
        let public_spaces = state.appservice.get_public_spaces().await.map_err(|e| {
            tracing::error!("Failed to get public spaces: {}", e);
            AppserviceError::AppserviceError("Failed to get public spaces".to_string())
        })?;

        return match public_spaces {
            Some(spaces) => Ok(Json(json!(spaces))),
            None => Err(AppserviceError::AppserviceError(
                "No public spaces found".to_string(),
            )),
        };
    }

    if let Ok(Some(cached_spaces)) = state
        .cache
        .get_cached_data::<Vec<RoomSummary>>("public_spaces")
        .await
    {
        if !cached_spaces.is_empty() {
            tracing::info!(
                "Returning cached public spaces ({} spaces)",
                cached_spaces.len()
            );
            return Ok(Json(json!(cached_spaces)));
        }
    }

    // cache missed
    let spaces = state
        .cache
        .cache_or_fetch("public_spaces", state.config.spaces.ttl, || async {
            tracing::info!("Cache miss for public spaces, fetching from appservice");

            let public_spaces = state.appservice.get_public_spaces().await.map_err(|e| {
                tracing::error!("Failed to get public spaces: {}", e);
                redis::RedisError::from((redis::ErrorKind::IoError, "Failed to get public spaces"))
            })?;

            match public_spaces {
                Some(spaces) => {
                    tracing::info!("Fetched and cached {} public spaces", spaces.len());
                    Ok(spaces)
                }
                None => {
                    tracing::warn!("No public spaces found");
                    Err(redis::RedisError::from((
                        redis::ErrorKind::ResponseError,
                        "No public spaces found",
                    )))
                }
            }
        })
        .await
        .map_err(|e| {
            tracing::error!("Failed to get public spaces: {}", e);
            AppserviceError::AppserviceError("Failed to get public spaces".to_string())
        })?;

    Ok(Json(json!(spaces)))
}

pub async fn space(
    State(state): State<Arc<AppState>>,
    Path(space): Path<String>,
) -> Result<impl IntoResponse, AppserviceError> {
    let server_name = state.config.matrix.server_name.clone();
    let raw_alias = format!("#{space}:{server_name}");

    let alias = RoomAliasId::parse(&raw_alias).map_err(|e| {
        tracing::error!("Failed to parse room alias: {}", e);
        AppserviceError::AppserviceError("No Alias".to_string())
    })?;

    if !state.config.spaces.cache {
        let room_id = state
            .appservice
            .room_id_from_alias(alias)
            .await
            .map_err(|e| {
                tracing::error!("Failed to get room ID from alias: {}", e);
                AppserviceError::AppserviceError("Space does not exist.".to_string())
            })?;

        let summary = state
            .appservice
            .get_room_summary(room_id)
            .await
            .map_err(|e| {
                tracing::error!("Failed to get room summary: {}", e);
                AppserviceError::AppserviceError("Failed to get space summary".to_string())
            })?;

        return Ok(Json(json!(summary)));
    }

    let cache_key = ("space_summary", space.clone()).cache_key();
    if let Ok(Some(cached_summary)) = state.cache.get_cached_data::<RoomSummary>(&cache_key).await {
        tracing::info!("Returning cached space summary for {}", space);
        return Ok(Json(json!(cached_summary)));
    }

    // cache missed
    let summary = state
        .cache
        .cache_or_fetch(&cache_key, state.config.spaces.ttl, || async {
            tracing::info!("Cache miss for space {}, fetching summary", space);

            let room_id = state
                .appservice
                .room_id_from_alias(alias)
                .await
                .map_err(|e| {
                    tracing::error!("Failed to get room ID from alias: {}", e);
                    redis::RedisError::from((redis::ErrorKind::IoError, "Space does not exist"))
                })?;

            let summary = state
                .appservice
                .get_room_summary(room_id)
                .await
                .map_err(|e| {
                    tracing::error!("Failed to get room summary: {}", e);
                    redis::RedisError::from((
                        redis::ErrorKind::IoError,
                        "Failed to get space summary",
                    ))
                })?;

            tracing::info!("Fetched and cached space summary for {}", space);
            Ok(summary)
        })
        .await
        .map_err(|e| {
            tracing::error!("Failed to get space summary: {}", e);
            AppserviceError::AppserviceError("Failed to get space summary".to_string())
        })?;

    Ok(Json(json!(summary)))
}

pub async fn space_rooms(
    State(state): State<Arc<AppState>>,
    Path(space): Path<String>,
) -> Result<impl IntoResponse, AppserviceError> {
    let server_name = state.config.matrix.server_name.clone();

    let raw_alias = format!("#{space}:{server_name}");

    let alias = RoomAliasId::parse(&raw_alias).map_err(|e| {
        tracing::error!("Failed to parse room alias: {}", e);
        AppserviceError::AppserviceError("No Alias".to_string())
    })?;

    let room_id = state
        .appservice
        .room_id_from_alias(alias)
        .await
        .map_err(|e| {
            tracing::error!("Failed to get room ID from alias: {}", e);
            AppserviceError::AppserviceError("Space does not exist.".to_string())
        })?;

    if state.config.spaces.cache {
        let hierarchy_key = ("space_hierarchy", space.clone()).cache_key();

        // check cache first
        if let Ok(Some(cached_hierarchy)) = state
            .cache
            .get_cached_data::<Vec<SpaceHierarchyRoomsChunk>>(&hierarchy_key)
            .await
        {
            tracing::info!(
                "Returning cached space hierarchy for {} ({} rooms)",
                space,
                cached_hierarchy.len()
            );
            return Ok(Json(json!(cached_hierarchy)));
        }

        let hierarchy = state
            .cache
            .cache_or_fetch(&hierarchy_key, state.config.spaces.ttl, || async {
                tracing::info!("Cache miss for space hierarchy {}, fetching", space);

                let hierarchy = state
                    .appservice
                    .get_room_hierarchy(room_id.clone())
                    .await
                    .map_err(|e| {
                        tracing::error!("Failed to get space hierarchy: {}", e);
                        redis::RedisError::from((
                            redis::ErrorKind::IoError,
                            "Failed to get space hierarchy",
                        ))
                    })?;

                tracing::info!(
                    "Fetched and cached space hierarchy for {} ({} rooms)",
                    space,
                    hierarchy.len()
                );
                Ok(hierarchy)
            })
            .await
            .map_err(|e| {
                tracing::error!("Failed to get space hierarchy: {}", e);
                AppserviceError::AppserviceError("Failed to get space hierarchy".to_string())
            })?;

        return Ok(Json(json!(hierarchy)));
    }

    let hierarchy = state
        .appservice
        .get_room_hierarchy(room_id.clone())
        .await
        .map_err(|e| {
            tracing::error!("Failed to get space hierarchy: {}", e);
            AppserviceError::AppserviceError("Failed to get space hierarchy".to_string())
        })?;

    Ok(Json(json!(hierarchy)))
}