feat: add global & server leveling, quests, achievements, guilds land-claiming, social & toggleable configs
This commit is contained in:
1 parent
4b79f69c20
commit
b4cef85b1f
40 files changed
+10261
-27
No files matched your search
@@ -0,0 +1,623 @@
|
||||
//! Friends, Direct Messages, Game Invites, Player Profiles, and Social Feed.
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::state::AppState;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use scopenet_shared::{DirectMessage, FriendInfo, GameInvite, UserPost, UserProfileView};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Friends System
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_friends(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<FriendInfo>>> {
|
||||
let rows: Vec<(String, String, String, String, bool, Option<String>)> = sqlx::query_as(
|
||||
"SELECT
|
||||
(CASE WHEN f.user_uuid = ? THEN f.friend_uuid ELSE f.user_uuid END) as target_uuid,
|
||||
u.username,
|
||||
f.status,
|
||||
f.action_uuid,
|
||||
EXISTS(SELECT 1 FROM server_online so WHERE so.uuid = u.uuid) as online,
|
||||
(SELECT gs.name FROM server_online so JOIN game_servers gs ON gs.id = so.server_id WHERE so.uuid = u.uuid LIMIT 1) as playing_on
|
||||
FROM friendships f
|
||||
JOIN users u ON u.uuid = (CASE WHEN f.user_uuid = ? THEN f.friend_uuid ELSE f.user_uuid END)
|
||||
WHERE f.user_uuid = ? OR f.friend_uuid = ?
|
||||
ORDER BY online DESC, u.username ASC",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.uuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(uuid, username, status, action_uuid, online, playing_on)| {
|
||||
let final_status = if status == "accepted" {
|
||||
"accepted".to_string()
|
||||
} else if action_uuid == auth.uuid {
|
||||
"pending_outgoing".to_string()
|
||||
} else {
|
||||
"pending_incoming".to_string()
|
||||
};
|
||||
|
||||
FriendInfo {
|
||||
uuid,
|
||||
username,
|
||||
status: final_status,
|
||||
online,
|
||||
playing_on,
|
||||
last_seen: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FriendRequestPayload {
|
||||
pub username: Option<String>,
|
||||
pub friend_username: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn send_friend_request(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<FriendRequestPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let name = payload
|
||||
.username
|
||||
.as_deref()
|
||||
.or(payload.friend_username.as_deref())
|
||||
.ok_or_else(|| AppError::bad_request("Username is required"))?
|
||||
.trim();
|
||||
|
||||
let target: Option<(String, String)> = sqlx::query_as(
|
||||
"SELECT uuid, username FROM users WHERE username = ? COLLATE NOCASE",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let (target_uuid, target_name) = target.ok_or_else(|| AppError::not_found("Player not found"))?;
|
||||
|
||||
if target_uuid == auth.uuid {
|
||||
return Err(AppError::bad_request("You cannot friend yourself"));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Check existing
|
||||
let existing: Option<(i64, String, String)> = sqlx::query_as(
|
||||
"SELECT id, status, action_uuid FROM friendships
|
||||
WHERE (user_uuid = ? AND friend_uuid = ?) OR (user_uuid = ? AND friend_uuid = ?)",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
if let Some((_id, status, action)) = existing {
|
||||
if status == "accepted" {
|
||||
return Err(AppError::bad_request("Already friends with this player"));
|
||||
}
|
||||
if status == "pending" && action != auth.uuid {
|
||||
// Reciprocal request -> Auto-accept!
|
||||
sqlx::query("UPDATE friendships SET status = 'accepted', action_uuid = ?, updated_at = ? WHERE (user_uuid = ? AND friend_uuid = ?) OR (user_uuid = ? AND friend_uuid = ?)")
|
||||
.bind(&auth.uuid)
|
||||
.bind(&now)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
return Ok(Json(serde_json::json!({ "ok": true, "status": "accepted", "friend": target_name })));
|
||||
}
|
||||
return Err(AppError::bad_request("Friend request is already pending"));
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO friendships (user_uuid, friend_uuid, status, action_uuid, created_at, updated_at)
|
||||
VALUES (?, ?, 'pending', ?, ?, ?)",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "status": "pending_outgoing", "friend": target_name })))
|
||||
}
|
||||
|
||||
pub async fn accept_friend_request(
|
||||
auth: AuthUser,
|
||||
Path(target_uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let res = sqlx::query(
|
||||
"UPDATE friendships SET status = 'accepted', updated_at = ?
|
||||
WHERE ((user_uuid = ? AND friend_uuid = ?) OR (user_uuid = ? AND friend_uuid = ?))
|
||||
AND status = 'pending' AND action_uuid <> ?",
|
||||
)
|
||||
.bind(&now)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
if res.rows_affected() == 0 {
|
||||
return Err(AppError::bad_request("No pending request from this user found"));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
pub async fn remove_friend(
|
||||
auth: AuthUser,
|
||||
Path(target_uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query(
|
||||
"DELETE FROM friendships
|
||||
WHERE (user_uuid = ? AND friend_uuid = ?) OR (user_uuid = ? AND friend_uuid = ?)",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RespondFriendPayload {
|
||||
pub target_uuid: Option<String>,
|
||||
pub friend_uuid: Option<String>,
|
||||
pub accept: bool,
|
||||
}
|
||||
|
||||
pub async fn respond_friend_request(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RespondFriendPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let target = payload
|
||||
.target_uuid
|
||||
.or(payload.friend_uuid)
|
||||
.ok_or_else(|| AppError::bad_request("Friend UUID is required"))?;
|
||||
|
||||
if payload.accept {
|
||||
accept_friend_request(auth, Path(target), State(state)).await
|
||||
} else {
|
||||
remove_friend(auth, Path(target), State(state)).await
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direct Messaging (DMs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn get_direct_messages(
|
||||
auth: AuthUser,
|
||||
Path(target_uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<DirectMessage>>> {
|
||||
let rows: Vec<(i64, String, String, String, String, bool, String)> = sqlx::query_as(
|
||||
"SELECT id, sender_uuid, sender_name, recipient_uuid, content, is_read, created_at
|
||||
FROM direct_messages
|
||||
WHERE (sender_uuid = ? AND recipient_uuid = ?) OR (sender_uuid = ? AND recipient_uuid = ?)
|
||||
ORDER BY id ASC LIMIT 100",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&target_uuid)
|
||||
.bind(&auth.uuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
// Mark unread messages sent to me as read
|
||||
sqlx::query(
|
||||
"UPDATE direct_messages SET is_read = 1
|
||||
WHERE recipient_uuid = ? AND sender_uuid = ? AND is_read = 0",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&target_uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(id, suuid, sname, ruuid, content, is_read, created)| DirectMessage {
|
||||
id,
|
||||
sender_uuid: suuid,
|
||||
sender_name: sname,
|
||||
recipient_uuid: ruuid,
|
||||
content,
|
||||
is_read,
|
||||
created_at: created,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SendMessagePayload {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub async fn send_direct_message(
|
||||
auth: AuthUser,
|
||||
Path(target_uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SendMessagePayload>,
|
||||
) -> AppResult<Json<DirectMessage>> {
|
||||
let content = payload.content.trim();
|
||||
if content.is_empty() {
|
||||
return Err(AppError::bad_request("Message cannot be empty"));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO direct_messages (sender_uuid, sender_name, recipient_uuid, content, is_read, created_at)
|
||||
VALUES (?, ?, ?, ?, 0, ?)
|
||||
RETURNING id",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.username)
|
||||
.bind(&target_uuid)
|
||||
.bind(content)
|
||||
.bind(&now)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(DirectMessage {
|
||||
id,
|
||||
sender_uuid: auth.uuid.clone(),
|
||||
sender_name: auth.username.clone(),
|
||||
recipient_uuid: target_uuid,
|
||||
content: content.to_string(),
|
||||
is_read: false,
|
||||
created_at: now,
|
||||
}))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Game Invites
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_my_game_invites(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<GameInvite>>> {
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
)> = sqlx::query_as(
|
||||
"SELECT gi.id, gi.sender_uuid, gi.sender_name, gi.recipient_uuid,
|
||||
gi.instance_id, i.name as instance_name,
|
||||
gi.server_id, gs.name as server_name,
|
||||
gi.status, gi.created_at, gi.expires_at
|
||||
FROM game_invites gi
|
||||
JOIN instances i ON i.id = gi.instance_id
|
||||
LEFT JOIN game_servers gs ON gs.id = gi.server_id
|
||||
WHERE gi.recipient_uuid = ? AND gi.status = 'pending' AND gi.expires_at > ?
|
||||
ORDER BY gi.created_at DESC",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(chrono::Utc::now().to_rfc3339())
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(id, suuid, sname, ruuid, iid, iname, sid, sname_opt, status, cat, eat)| GameInvite {
|
||||
id,
|
||||
sender_uuid: suuid,
|
||||
sender_name: sname,
|
||||
recipient_uuid: ruuid,
|
||||
instance_id: iid,
|
||||
instance_name: iname,
|
||||
server_id: sid,
|
||||
server_name: sname_opt,
|
||||
status,
|
||||
created_at: cat,
|
||||
expires_at: eat,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SendInvitePayload {
|
||||
pub recipient_uuid: String,
|
||||
pub instance_id: String,
|
||||
pub server_id: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn send_game_invite(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SendInvitePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let invite_id = format!("inv_{}", uuid::Uuid::new_v4().simple());
|
||||
let now = chrono::Utc::now();
|
||||
let expires = now + chrono::Duration::minutes(30);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO game_invites (id, sender_uuid, sender_name, recipient_uuid, instance_id, server_id, status, created_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)",
|
||||
)
|
||||
.bind(&invite_id)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.username)
|
||||
.bind(payload.recipient_uuid)
|
||||
.bind(payload.instance_id)
|
||||
.bind(payload.server_id)
|
||||
.bind(now.to_rfc3339())
|
||||
.bind(expires.to_rfc3339())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "id": invite_id, "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RespondInvitePayload {
|
||||
pub action: String, // "accept" or "decline"
|
||||
}
|
||||
|
||||
pub async fn respond_game_invite(
|
||||
auth: AuthUser,
|
||||
Path(invite_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RespondInvitePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let status = if payload.action == "accept" {
|
||||
"accepted"
|
||||
} else {
|
||||
"declined"
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE game_invites SET status = ? WHERE id = ? AND recipient_uuid = ?",
|
||||
)
|
||||
.bind(status)
|
||||
.bind(&invite_id)
|
||||
.bind(&auth.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "status": status })))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Viewable Player Profiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn get_player_profile(
|
||||
Path(uuid): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<UserProfileView>> {
|
||||
let user_row: Option<(String, String)> = sqlx::query_as(
|
||||
"SELECT uuid, username FROM users WHERE uuid = ?",
|
||||
)
|
||||
.bind(&uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let (puuid, username) = user_row.ok_or_else(|| AppError::not_found("Player profile not found"))?;
|
||||
|
||||
let prof_row: Option<(String, Option<String>, Option<String>, Option<String>)> = sqlx::query_as(
|
||||
"SELECT bio, banner_url, custom_badge, featured_achievement_id FROM user_profiles WHERE uuid = ?",
|
||||
)
|
||||
.bind(&puuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let (bio, banner_url, custom_badge, feat_ach_id) = prof_row.unwrap_or((String::new(), None, None, None));
|
||||
|
||||
// Levels
|
||||
let levels = crate::routes::leveling::get_user_levels_by_uuid(&state, &puuid).await?;
|
||||
|
||||
// Featured achievement if set
|
||||
let featured_achievement = if let Some(fid) = feat_ach_id {
|
||||
let ach_list = crate::routes::achievements::get_achievements_for_user(&state, &puuid, false).await?;
|
||||
ach_list.into_iter().find(|a| a.id == fid)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Unlocked achievements count
|
||||
let achievements_count: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM user_achievements WHERE user_uuid = ?",
|
||||
)
|
||||
.bind(&puuid)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
// Recent posts
|
||||
let post_rows: Vec<(i64, String, String, String, Option<String>, i64, String)> = sqlx::query_as(
|
||||
"SELECT id, user_uuid, author_name, content, image_url, likes_count, created_at
|
||||
FROM user_posts WHERE user_uuid = ? ORDER BY id DESC LIMIT 10",
|
||||
)
|
||||
.bind(&puuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let posts = post_rows
|
||||
.into_iter()
|
||||
.map(|(id, u_uuid, aname, content, img, likes, cat)| UserPost {
|
||||
id,
|
||||
user_uuid: u_uuid,
|
||||
author_name: aname,
|
||||
content,
|
||||
image_url: img,
|
||||
likes_count: likes,
|
||||
created_at: cat,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(UserProfileView {
|
||||
uuid: puuid,
|
||||
username,
|
||||
bio,
|
||||
banner_url,
|
||||
custom_badge,
|
||||
title: levels.title,
|
||||
global_level: levels.global_level,
|
||||
global_xp: levels.global_xp,
|
||||
server_levels: levels.server_levels,
|
||||
featured_achievement,
|
||||
achievements_count: achievements_count as usize,
|
||||
posts,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateProfilePayload {
|
||||
pub bio: Option<String>,
|
||||
pub banner_url: Option<String>,
|
||||
pub custom_badge: Option<String>,
|
||||
pub featured_achievement_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update_my_profile(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<UpdateProfilePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO user_profiles (uuid, bio, banner_url, custom_badge, featured_achievement_id, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (uuid) DO UPDATE SET
|
||||
bio = COALESCE(?, bio),
|
||||
banner_url = COALESCE(?, banner_url),
|
||||
custom_badge = COALESCE(?, custom_badge),
|
||||
featured_achievement_id = COALESCE(?, featured_achievement_id),
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(payload.bio.clone().unwrap_or_default())
|
||||
.bind(payload.banner_url.clone())
|
||||
.bind(payload.custom_badge.clone())
|
||||
.bind(payload.featured_achievement_id.clone())
|
||||
.bind(&now)
|
||||
.bind(payload.bio)
|
||||
.bind(payload.banner_url)
|
||||
.bind(payload.custom_badge)
|
||||
.bind(payload.featured_achievement_id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Social Posts Feed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreatePostInput {
|
||||
pub content: String,
|
||||
pub image_url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_user_post(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreatePostInput>,
|
||||
) -> AppResult<Json<UserPost>> {
|
||||
let content = payload.content.trim();
|
||||
if content.is_empty() {
|
||||
return Err(AppError::bad_request("Post content cannot be empty"));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO user_posts (user_uuid, author_name, content, image_url, likes_count, created_at)
|
||||
VALUES (?, ?, ?, ?, 0, ?)
|
||||
RETURNING id",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.username)
|
||||
.bind(content)
|
||||
.bind(payload.image_url.as_deref())
|
||||
.bind(&now)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(UserPost {
|
||||
id,
|
||||
user_uuid: auth.uuid.clone(),
|
||||
author_name: auth.username.clone(),
|
||||
content: content.to_string(),
|
||||
image_url: payload.image_url,
|
||||
likes_count: 0,
|
||||
created_at: now,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn delete_user_post(
|
||||
auth: AuthUser,
|
||||
Path(post_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query("DELETE FROM user_posts WHERE id = ? AND user_uuid = ?")
|
||||
.bind(post_id)
|
||||
.bind(&auth.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
pub async fn like_user_post(
|
||||
_auth: AuthUser,
|
||||
Path(post_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query("UPDATE user_posts SET likes_count = likes_count + 1 WHERE id = ?")
|
||||
.bind(post_id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
Reference in new issue
Block a user