Skip to main content

cloudillo_profile/
list.rs

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