Skip to main content

cloudillo_profile/
list.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Profile listing and retrieval handlers
5
6use axum::{
7	extract::{Path, Query, State},
8	http::StatusCode,
9	Json,
10};
11use serde::{Deserialize, Serialize};
12use serde_with::skip_serializing_none;
13
14use crate::prelude::*;
15use cloudillo_core::extract::OptionalRequestId;
16use cloudillo_types::meta_adapter::ListProfileOptions;
17use cloudillo_types::types::{ApiResponse, ProfileInfo};
18
19/// Profile with relationship status (for GET /api/profiles/:idTag)
20#[skip_serializing_none]
21#[derive(Debug, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct ProfileWithStatus {
24	pub id_tag: String,
25	pub name: String,
26	#[serde(rename = "type")]
27	pub r#type: Option<String>,
28	pub profile_pic: Option<String>,
29	pub status: Option<String>,
30	pub connected: Option<bool>,
31	pub following: Option<bool>,
32}
33
34#[derive(Debug, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ListProfilesQuery {
37	#[serde(alias = "q")]
38	search: Option<String>,
39	#[serde(rename = "type")]
40	typ: Option<cloudillo_types::meta_adapter::ProfileType>,
41}
42
43/// GET /profile - List all profiles or search profiles
44/// Query parameters:
45///   type: Optional filter by profile type ("person" or "community")
46///   search: Optional search term to filter profiles by id_tag or name
47///   limit: Results per page (default 20, max 100)
48///   offset: Pagination offset (default 0)
49pub async fn list_profiles(
50	State(app): State<App>,
51	tn_id: TnId,
52	OptionalRequestId(req_id): OptionalRequestId,
53	Query(params): Query<ListProfilesQuery>,
54) -> ClResult<(StatusCode, Json<ApiResponse<Vec<ProfileInfo>>>)> {
55	// Build options for list_profiles
56	let opts = ListProfileOptions {
57		typ: params.typ,
58		status: None,
59		connected: None,
60		following: None,
61		q: params.search.as_ref().map(|s| s.to_lowercase()),
62		id_tag: None,
63	};
64
65	// Fetch profiles with optional search
66	let profiles_list = app.meta_adapter.list_profiles(tn_id, &opts).await?;
67
68	// Convert Profile to ProfileInfo
69	let profiles: Vec<ProfileInfo> = profiles_list
70		.into_iter()
71		.map(|p| ProfileInfo {
72			id_tag: p.id_tag.to_string(),
73			name: p.name.to_string(),
74			r#type: Some(
75				match p.typ {
76					cloudillo_types::meta_adapter::ProfileType::Person => "person",
77					cloudillo_types::meta_adapter::ProfileType::Community => "community",
78				}
79				.to_string(),
80			),
81			profile_pic: p.profile_pic.map(|s| s.to_string()),
82			status: None, // Not available in Profile type
83			connected: Some(p.connected.is_connected()),
84			following: Some(p.following),
85			roles: p.roles.map(|r| r.iter().map(ToString::to_string).collect()),
86			created_at: None, // Not available in Profile type
87		})
88		.collect();
89
90	let response = ApiResponse::new(profiles).with_req_id(req_id.unwrap_or_default());
91
92	Ok((StatusCode::OK, Json(response)))
93}
94
95/// GET /profile/:idTag - Get specific profile's local relationship state
96/// Returns the locally cached relationship data (connected, following, status)
97/// Returns empty/null if the profile is not known locally
98pub async fn get_profile_by_id_tag(
99	State(app): State<App>,
100	tn_id: TnId,
101	OptionalRequestId(req_id): OptionalRequestId,
102	Path(id_tag): Path<String>,
103) -> ClResult<(StatusCode, Json<ApiResponse<Option<ProfileWithStatus>>>)> {
104	// Lookup profile in local profiles table (relationship data)
105	let profile = match app.meta_adapter.read_profile(tn_id, &id_tag).await {
106		Ok((_etag, p)) => {
107			let typ = match p.typ {
108				cloudillo_types::meta_adapter::ProfileType::Person => None,
109				cloudillo_types::meta_adapter::ProfileType::Community => {
110					Some("community".to_string())
111				}
112			};
113			Some(ProfileWithStatus {
114				id_tag: p.id_tag.to_string(),
115				name: p.name.to_string(),
116				r#type: typ,
117				profile_pic: p.profile_pic.map(|s| s.to_string()),
118				status: None, // TODO: Add status to Profile struct
119				connected: Some(p.connected.is_connected()),
120				following: Some(p.following),
121			})
122		}
123		Err(Error::NotFound) => None, // Return empty when not found locally
124		Err(e) => return Err(e),
125	};
126
127	let response = ApiResponse::new(profile).with_req_id(req_id.unwrap_or_default());
128
129	Ok((StatusCode::OK, Json(response)))
130}
131
132// vim: ts=4