From a317235bce4a9392dc82591961b93401ecdf5476 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sun, 14 Dec 2025 22:58:36 +0800 Subject: [PATCH] friends system (Testing WIP) --- src-tauri/src/lib.rs | 100 +++++- src-tauri/src/remotes/friends.rs | 305 ++++++++++++++++-- src-tauri/src/remotes/user.rs | 2 +- src/routes/app-menu/tabs/friends.svelte | 291 ++++++++++++++++- src/types/bindings/AppData.ts | 8 +- .../bindings/FriendRequestResponseDto.ts | 10 + src/types/bindings/FriendshipResponseDto.ts | 7 + src/types/bindings/SendFriendRequestDto.ts | 3 + src/types/bindings/UserBasicDto.ts | 5 + 9 files changed, 705 insertions(+), 26 deletions(-) create mode 100644 src/types/bindings/FriendRequestResponseDto.ts create mode 100644 src/types/bindings/FriendshipResponseDto.ts create mode 100644 src/types/bindings/SendFriendRequestDto.ts create mode 100644 src/types/bindings/UserBasicDto.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e69fc42..8501e6e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,12 @@ -use crate::{models::app_data::AppData, services::cursor::start_cursor_tracking, state::FDOLL}; +use crate::{ + models::app_data::AppData, + remotes::friends::{ + FriendRemote, FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, + UserBasicDto, + }, + services::cursor::start_cursor_tracking, + state::{init_app_data, FDOLL}, +}; use tauri::async_runtime; use tracing_subscriber; @@ -52,6 +60,87 @@ fn get_app_data() -> Result { return Ok(guard.app_data.clone()); } +#[tauri::command] +async fn refresh_app_data() -> Result { + init_app_data().await; + let guard = lock_r!(FDOLL); + Ok(guard.app_data.clone()) +} + +#[tauri::command] +async fn list_friends() -> Result, String> { + FriendRemote::new() + .get_friends() + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn search_users(username: Option) -> Result, String> { + tracing::info!( + "Tauri command search_users called with username: {:?}", + username + ); + let remote = FriendRemote::new(); + tracing::info!("FriendRemote instance created for search_users"); + let result = remote.search_users(username.as_deref()).await; + tracing::info!("FriendRemote::search_users result: {:?}", result); + result.map_err(|e| { + tracing::error!("search_users command error: {}", e); + e.to_string() + }) +} + +#[tauri::command] +async fn send_friend_request( + request: SendFriendRequestDto, +) -> Result { + FriendRemote::new() + .send_friend_request(request) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn received_friend_requests() -> Result, String> { + FriendRemote::new() + .get_received_requests() + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn sent_friend_requests() -> Result, String> { + FriendRemote::new() + .get_sent_requests() + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn accept_friend_request(request_id: String) -> Result { + FriendRemote::new() + .accept_friend_request(&request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn deny_friend_request(request_id: String) -> Result { + FriendRemote::new() + .deny_friend_request(&request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn unfriend(friend_id: String) -> Result<(), String> { + FriendRemote::new() + .unfriend(&friend_id) + .await + .map_err(|e| e.to_string()) +} + #[tauri::command] fn quit_app() -> Result<(), String> { let app_handle = get_app_handle(); @@ -68,6 +157,15 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ start_cursor_tracking, get_app_data, + refresh_app_data, + list_friends, + search_users, + send_friend_request, + received_friend_requests, + sent_friend_requests, + accept_friend_request, + deny_friend_request, + unfriend, quit_app ]) .setup(|app| { diff --git a/src-tauri/src/remotes/friends.rs b/src-tauri/src/remotes/friends.rs index 3386038..fba6184 100644 --- a/src-tauri/src/remotes/friends.rs +++ b/src-tauri/src/remotes/friends.rs @@ -1,17 +1,25 @@ -use reqwest::{Client, Error}; +use reqwest::Client; use serde::{Deserialize, Serialize}; +use thiserror::Error; use ts_rs::TS; use crate::{lock_r, services::auth::with_auth, state::FDOLL}; +#[derive(Error, Debug)] +pub enum RemoteError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + #[error("JSON parse error: {0}")] + Json(#[from] serde_json::Error), +} + #[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct UserBasicDto { pub id: String, pub name: String, - pub username: String, - pub picture: String, + pub username: Option, } #[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] @@ -66,75 +74,334 @@ impl FriendRemote { } } - pub async fn get_friends(&self) -> Result, Error> { + pub async fn get_friends(&self) -> Result, RemoteError> { let url = format!("{}/friends", self.base_url); + tracing::info!( + "FriendRemote::get_friends - Sending GET request to URL: {}", + url + ); let resp = with_auth(self.client.get(url)).await.send().await?; - let friends = resp.json().await?; + tracing::info!( + "FriendRemote::get_friends - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::get_friends - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::get_friends - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::get_friends - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!("FriendRemote::get_friends - Response body: {}", text); + let friends: Vec = serde_json::from_str(&text).map_err(|e| { + tracing::error!("FriendRemote::get_friends - Failed to parse JSON: {}", e); + e + })?; + tracing::info!( + "FriendRemote::get_friends - Successfully parsed {} friends", + friends.len() + ); Ok(friends) } - pub async fn search_users(&self, username: Option<&str>) -> Result, Error> { + pub async fn search_users( + &self, + username: Option<&str>, + ) -> Result, RemoteError> { let mut url = format!("{}/friends/search", self.base_url); if let Some(u) = username { url.push_str(&format!("?username={}", u)); } + tracing::info!( + "FriendRemote::search_users - Sending GET request to URL: {}", + url + ); let resp = with_auth(self.client.get(&url)).await.send().await?; - let users = resp.json().await?; + tracing::info!( + "FriendRemote::search_users - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::search_users - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::search_users - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::search_users - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!("FriendRemote::search_users - Response body: {}", text); + let users: Vec = serde_json::from_str(&text).map_err(|e| { + tracing::error!("FriendRemote::search_users - Failed to parse JSON: {}", e); + e + })?; + tracing::info!( + "FriendRemote::search_users - Successfully parsed {} users", + users.len() + ); Ok(users) } pub async fn send_friend_request( &self, request: SendFriendRequestDto, - ) -> Result { + ) -> Result { let url = format!("{}/friends/requests", self.base_url); + tracing::info!( + "FriendRemote::send_friend_request - Sending POST request to URL: {} with body: {:?}", + url, + request + ); let resp = with_auth(self.client.post(url)) .await .json(&request) .send() .await?; - let req_resp = resp.json().await?; + tracing::info!( + "FriendRemote::send_friend_request - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::send_friend_request - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::send_friend_request - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::send_friend_request - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::send_friend_request - Response body: {}", + text + ); + let req_resp: FriendRequestResponseDto = serde_json::from_str(&text).map_err(|e| { + tracing::error!( + "FriendRemote::send_friend_request - Failed to parse JSON: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::send_friend_request - Successfully parsed friend request response" + ); Ok(req_resp) } - pub async fn get_received_requests(&self) -> Result, Error> { + pub async fn get_received_requests( + &self, + ) -> Result, RemoteError> { let url = format!("{}/friends/requests/received", self.base_url); + tracing::info!( + "FriendRemote::get_received_requests - Sending GET request to URL: {}", + url + ); let resp = with_auth(self.client.get(url)).await.send().await?; - let requests = resp.json().await?; + tracing::info!( + "FriendRemote::get_received_requests - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::get_received_requests - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::get_received_requests - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::get_received_requests - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::get_received_requests - Response body: {}", + text + ); + let requests: Vec = serde_json::from_str(&text).map_err(|e| { + tracing::error!( + "FriendRemote::get_received_requests - Failed to parse JSON: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::get_received_requests - Successfully parsed {} received requests", + requests.len() + ); Ok(requests) } - pub async fn get_sent_requests(&self) -> Result, Error> { + pub async fn get_sent_requests(&self) -> Result, RemoteError> { let url = format!("{}/friends/requests/sent", self.base_url); + tracing::info!( + "FriendRemote::get_sent_requests - Sending GET request to URL: {}", + url + ); let resp = with_auth(self.client.get(url)).await.send().await?; - let requests = resp.json().await?; + tracing::info!( + "FriendRemote::get_sent_requests - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::get_sent_requests - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::get_sent_requests - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::get_sent_requests - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!("FriendRemote::get_sent_requests - Response body: {}", text); + let requests: Vec = serde_json::from_str(&text).map_err(|e| { + tracing::error!( + "FriendRemote::get_sent_requests - Failed to parse JSON: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::get_sent_requests - Successfully parsed {} sent requests", + requests.len() + ); Ok(requests) } pub async fn accept_friend_request( &self, request_id: &str, - ) -> Result { + ) -> Result { let url = format!("{}/friends/requests/{}/accept", self.base_url, request_id); + tracing::info!( + "FriendRemote::accept_friend_request - Sending POST request to URL: {}", + url + ); let resp = with_auth(self.client.post(url)).await.send().await?; - let req_resp = resp.json().await?; + tracing::info!( + "FriendRemote::accept_friend_request - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::accept_friend_request - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::accept_friend_request - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::accept_friend_request - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::accept_friend_request - Response body: {}", + text + ); + let req_resp: FriendRequestResponseDto = serde_json::from_str(&text).map_err(|e| { + tracing::error!( + "FriendRemote::accept_friend_request - Failed to parse JSON: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::accept_friend_request - Successfully parsed friend request response" + ); Ok(req_resp) } pub async fn deny_friend_request( &self, request_id: &str, - ) -> Result { + ) -> Result { let url = format!("{}/friends/requests/{}/deny", self.base_url, request_id); + tracing::info!( + "FriendRemote::deny_friend_request - Sending POST request to URL: {}", + url + ); let resp = with_auth(self.client.post(url)).await.send().await?; - let req_resp = resp.json().await?; + tracing::info!( + "FriendRemote::deny_friend_request - Received response with status: {}", + resp.status() + ); + let resp = resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::deny_friend_request - HTTP error: {}", e); + e + })?; + tracing::info!( + "FriendRemote::deny_friend_request - Response status after error_for_status: {}", + resp.status() + ); + let text = resp.text().await.map_err(|e| { + tracing::error!( + "FriendRemote::deny_friend_request - Failed to read response text: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::deny_friend_request - Response body: {}", + text + ); + let req_resp: FriendRequestResponseDto = serde_json::from_str(&text).map_err(|e| { + tracing::error!( + "FriendRemote::deny_friend_request - Failed to parse JSON: {}", + e + ); + e + })?; + tracing::info!( + "FriendRemote::deny_friend_request - Successfully parsed friend request response" + ); Ok(req_resp) } - pub async fn unfriend(&self, friend_id: &str) -> Result<(), Error> { + pub async fn unfriend(&self, friend_id: &str) -> Result<(), RemoteError> { let url = format!("{}/friends/{}", self.base_url, friend_id); + tracing::info!( + "FriendRemote::unfriend - Sending DELETE request to URL: {}", + url + ); let resp = with_auth(self.client.delete(url)).await.send().await?; - resp.error_for_status()?; + tracing::info!( + "FriendRemote::unfriend - Received response with status: {}", + resp.status() + ); + resp.error_for_status().map_err(|e| { + tracing::error!("FriendRemote::unfriend - HTTP error: {}", e); + e + })?; + tracing::info!("FriendRemote::unfriend - Successfully unfriended"); Ok(()) } } diff --git a/src-tauri/src/remotes/user.rs b/src-tauri/src/remotes/user.rs index 165b3c8..43691b3 100644 --- a/src-tauri/src/remotes/user.rs +++ b/src-tauri/src/remotes/user.rs @@ -13,7 +13,7 @@ pub struct UserProfile { pub name: String, pub email: String, pub username: Option, - pub picture: Option, + pub roles: Vec, pub created_at: String, pub updated_at: String, diff --git a/src/routes/app-menu/tabs/friends.svelte b/src/routes/app-menu/tabs/friends.svelte index b863325..1c92423 100644 --- a/src/routes/app-menu/tabs/friends.svelte +++ b/src/routes/app-menu/tabs/friends.svelte @@ -1,7 +1,292 @@ - -
-

{$appData?.user?.name}'s friends

+
+ {#if error} +
{error}
+ {/if} + +
+
+
+
+
+ e.key === "Enter" && handleAddFriend()} + /> + {#if searchTerm.trim().length} + + {/if} +
+ +
+
+
+
+ +
Friend requests
+
+
+ {#if loading.received || loading.sent} +

Loading requests...

+ {:else if combinedRequests.length === 0} +

+ No pending friend requests. +

+ {:else} +
+ {#each combinedRequests as entry (entry.id)} +
+
+
+
+ {entry.type === "incoming" + ? entry.request.sender.name + : entry.request.receiver.name} +
+
+ @{entry.type === "incoming" + ? (entry.request.sender.username ?? "unknown") + : (entry.request.receiver.username ?? "unknown")} +
+
+ {#if entry.type === "incoming"} +
+ + +
+ {:else} +
+ {entry.request.status} +
+ {/if} +
+
+ {/each} +
+ {/if} +
+
+
+ +
+ +
Friends
+
+
+ {#if friends.length === 0} +

No friends yet.

+ {:else} +
+ {#each friends as friend (friend.id)} +
+
+
+
{friend.friend.name}
+
+ @{friend.friend.username ?? "unknown"} +
+
+ +
+
+ {/each} +
+ {/if} +
+
+
+
diff --git a/src/types/bindings/AppData.ts b/src/types/bindings/AppData.ts index 81fc52c..cba307b 100644 --- a/src/types/bindings/AppData.ts +++ b/src/types/bindings/AppData.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UserProfile } from "./UserProfile"; +import type { UserProfile } from "./UserProfile.js"; +import type { FriendshipResponseDto } from "./FriendshipResponseDto.js"; -export type AppData = { user: UserProfile | null, }; +export type AppData = { + user: UserProfile | null; + friends: Array | null; +}; diff --git a/src/types/bindings/FriendRequestResponseDto.ts b/src/types/bindings/FriendRequestResponseDto.ts new file mode 100644 index 0000000..c94af2a --- /dev/null +++ b/src/types/bindings/FriendRequestResponseDto.ts @@ -0,0 +1,10 @@ +import type { UserBasicDto } from "./UserBasicDto.js"; + +export type FriendRequestResponseDto = { + id: string; + sender: UserBasicDto; + receiver: UserBasicDto; + status: string; + createdAt: string; + updatedAt: string; +}; diff --git a/src/types/bindings/FriendshipResponseDto.ts b/src/types/bindings/FriendshipResponseDto.ts new file mode 100644 index 0000000..366ae09 --- /dev/null +++ b/src/types/bindings/FriendshipResponseDto.ts @@ -0,0 +1,7 @@ +import type { UserBasicDto } from "./UserBasicDto.js"; + +export type FriendshipResponseDto = { + id: string; + friend: UserBasicDto; + createdAt: string; +}; diff --git a/src/types/bindings/SendFriendRequestDto.ts b/src/types/bindings/SendFriendRequestDto.ts new file mode 100644 index 0000000..c2be6ae --- /dev/null +++ b/src/types/bindings/SendFriendRequestDto.ts @@ -0,0 +1,3 @@ +export type SendFriendRequestDto = { + receiverId: string; +}; diff --git a/src/types/bindings/UserBasicDto.ts b/src/types/bindings/UserBasicDto.ts new file mode 100644 index 0000000..3b796c6 --- /dev/null +++ b/src/types/bindings/UserBasicDto.ts @@ -0,0 +1,5 @@ +export type UserBasicDto = { + id: string; + name: string; + username: string | null; +};