mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-12 19:56:48 +00:00
213 lines
5.1 KiB
Rust
213 lines
5.1 KiB
Rust
use std::{collections::BTreeSet, time::SystemTime};
|
|
|
|
use askama::{Template, filters::HtmlSafe};
|
|
use conduwuit_core::utils;
|
|
use conduwuit_service::{
|
|
Services,
|
|
oauth::{client_metadata::ClientMetadata, grant::Scope},
|
|
};
|
|
use ruma::{OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device};
|
|
|
|
pub(super) mod form;
|
|
|
|
#[derive(Debug)]
|
|
pub(super) enum AvatarType {
|
|
Initial(char),
|
|
Image(String),
|
|
}
|
|
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "_components/avatar.html.j2")]
|
|
pub(super) struct Avatar {
|
|
pub(super) avatar_type: AvatarType,
|
|
}
|
|
|
|
impl HtmlSafe for Avatar {}
|
|
|
|
impl Avatar {
|
|
pub(super) async fn for_local_user(services: &Services, user_id: &UserId) -> Self {
|
|
let display_name = services.users.displayname(user_id).await.ok();
|
|
|
|
let avatar_type = if services.users.avatar_url(user_id).await.is_ok() {
|
|
AvatarType::Image(profile_media_url(user_id, "avatar_url"))
|
|
} else if let Some(initial) = display_name
|
|
.as_ref()
|
|
.and_then(|display_name| display_name.chars().next())
|
|
{
|
|
AvatarType::Initial(initial)
|
|
} else {
|
|
AvatarType::Initial(user_id.localpart().chars().next().unwrap())
|
|
};
|
|
|
|
Self { avatar_type }
|
|
}
|
|
|
|
pub(super) fn for_device(
|
|
client_metadata: Option<&ClientMetadata>,
|
|
display_name: Option<&str>,
|
|
dehydrated: bool,
|
|
) -> Self {
|
|
let avatar_src = client_metadata
|
|
.and_then(|metadata| metadata.logo_uri.as_ref())
|
|
.map(|uri| uri.as_str().to_owned());
|
|
|
|
let avatar_type = if dehydrated {
|
|
AvatarType::Initial('⊡')
|
|
} else if let Some(avatar_src) = avatar_src {
|
|
AvatarType::Image(avatar_src)
|
|
} else if let Some(initial) = display_name.and_then(|name| name.chars().next()) {
|
|
if client_metadata.is_some() {
|
|
AvatarType::Initial(initial)
|
|
} else {
|
|
AvatarType::Initial('❖')
|
|
}
|
|
} else {
|
|
AvatarType::Initial('?')
|
|
};
|
|
|
|
Self { avatar_type }
|
|
}
|
|
|
|
pub(super) fn for_client(client_metadata: &ClientMetadata) -> Self {
|
|
let avatar_type = if let Some(logo) = &client_metadata.logo_uri {
|
|
AvatarType::Image(logo.to_string())
|
|
} else if let Some(name) = &client_metadata.client_name
|
|
&& let Some(char) = name.chars().next()
|
|
{
|
|
AvatarType::Initial(char)
|
|
} else {
|
|
AvatarType::Initial('?')
|
|
};
|
|
|
|
Self { avatar_type }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "_components/user_card.html.j2")]
|
|
pub(super) struct UserCard {
|
|
pub user_id: OwnedUserId,
|
|
pub display_name: Option<String>,
|
|
pub avatar: Avatar,
|
|
pub banner: Option<String>,
|
|
}
|
|
|
|
impl HtmlSafe for UserCard {}
|
|
|
|
impl UserCard {
|
|
const BANNER_KEY: &str = "chat.commet.profile_banner";
|
|
|
|
pub(super) async fn for_local_user(services: &Services, user_id: OwnedUserId) -> Self {
|
|
let display_name = services.users.displayname(&user_id).await.ok();
|
|
let avatar = Avatar::for_local_user(services, &user_id).await;
|
|
let banner = if services
|
|
.users
|
|
.get_local_profile_field(&user_id, Self::BANNER_KEY.into())
|
|
.await
|
|
.is_some()
|
|
{
|
|
Some(profile_media_url(&user_id, Self::BANNER_KEY))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Self { user_id, display_name, avatar, banner }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "_components/device_card.html.j2")]
|
|
pub(super) struct DeviceCard {
|
|
pub device_id: OwnedDeviceId,
|
|
pub display_name: Option<String>,
|
|
pub avatar: Avatar,
|
|
pub last_active: String,
|
|
pub oauth_metadata: Option<ClientMetadata>,
|
|
pub style: DeviceCardStyle,
|
|
pub dehydrated: bool,
|
|
}
|
|
|
|
impl HtmlSafe for DeviceCard {}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub(super) enum DeviceCardStyle {
|
|
Minimal,
|
|
Detailed,
|
|
}
|
|
|
|
impl DeviceCard {
|
|
pub(super) async fn for_device(
|
|
services: &Services,
|
|
user_id: &UserId,
|
|
device: Device,
|
|
style: DeviceCardStyle,
|
|
) -> Self {
|
|
let oauth_metadata = async {
|
|
let session_info = services
|
|
.oauth
|
|
.get_session_info_for_device(user_id, &device.device_id)
|
|
.await?;
|
|
|
|
Some(
|
|
services
|
|
.oauth
|
|
.get_client_metadata(&session_info.client_id)
|
|
.await
|
|
.expect("client should exist"),
|
|
)
|
|
}
|
|
.await;
|
|
|
|
let dehydrated_device_id = services
|
|
.users
|
|
.get_dehydrated_device(user_id)
|
|
.await
|
|
.ok()
|
|
.map(|device| device.device_id);
|
|
let dehydrated = dehydrated_device_id.as_ref() == Some(&device.device_id);
|
|
|
|
let display_name = oauth_metadata
|
|
.as_ref()
|
|
.and_then(|metadata| metadata.client_name.clone())
|
|
.or_else(|| device.display_name.clone());
|
|
|
|
let avatar =
|
|
Avatar::for_device(oauth_metadata.as_ref(), display_name.as_deref(), dehydrated);
|
|
|
|
let last_active = device.last_seen_ts.map_or_else(
|
|
|| "unknown".to_owned(),
|
|
|last_seen_ts| {
|
|
last_seen_ts
|
|
.to_system_time()
|
|
.and_then(|t| SystemTime::now().duration_since(t).ok())
|
|
.map_or_else(
|
|
|| "now".to_owned(),
|
|
|duration| format!("{} ago", utils::time::pretty(duration)),
|
|
)
|
|
},
|
|
);
|
|
|
|
Self {
|
|
device_id: device.device_id,
|
|
display_name,
|
|
avatar,
|
|
last_active,
|
|
oauth_metadata,
|
|
style,
|
|
dehydrated,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "_components/client_scopes.html.j2")]
|
|
pub(super) struct ClientScopes {
|
|
pub scopes: BTreeSet<Scope>,
|
|
}
|
|
|
|
impl HtmlSafe for ClientScopes {}
|
|
|
|
fn profile_media_url(user_id: &UserId, profile_key: &str) -> String {
|
|
format!("{}/media/profile/{user_id}/{profile_key}", crate::ROUTE_PREFIX)
|
|
}
|