From f02524b59288d68e78a108e9c9e2ca4c7f662f07 Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Tue, 14 Feb 2023 19:28:10 -0500 Subject: [PATCH] friends --- public/css/profile.css | 34 +++++++++- public/js/api.js | 12 ++++ public/js/people.js | 15 ----- public/js/profile.js | 139 +++++++++++++++++++++++++++++++------- public/profile.html | 1 + src/api/mod.rs | 2 +- src/api/posts.rs | 2 +- src/api/users.rs | 143 +++++++++++++++++++++++++++++++++++++++- src/database/friends.rs | 97 +++++++++++++++++++++++++++ src/database/likes.rs | 8 +-- src/database/mod.rs | 2 + src/database/users.rs | 2 +- src/public/admin.rs | 10 ++- src/public/console.rs | 33 ++++++---- src/public/docs.rs | 6 +- src/public/mod.rs | 2 +- src/public/pages.rs | 6 +- src/types/extract.rs | 6 +- src/types/like.rs | 3 +- src/types/user.rs | 42 ++++++++++++ 20 files changed, 490 insertions(+), 75 deletions(-) create mode 100644 src/database/friends.rs diff --git a/public/css/profile.css b/public/css/profile.css index 8f212dd..112b1bf 100644 --- a/public/css/profile.css +++ b/public/css/profile.css @@ -119,7 +119,7 @@ body { border-bottom: 3px solid var(--logo) !important; } -#about { +#about, #friends { margin-top: 2em; align-self: center; padding: 0; @@ -160,4 +160,36 @@ body { .logout { flex: 1; +} + +.follow { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + width: 175px; + background-color: var(--secondary); + border-radius: 10px; + cursor: pointer; +} + +.follow>span { + color: var(--medium); +} + +.friend { + background-color: var(--logo); + border: 1px solid var(#ffffff) +} + +.friend>span { + color: #ffffff; +} + +.right { + flex: 1; + display: flex; + justify-content: end; + align-items: center; + padding-right: 50px; } \ No newline at end of file diff --git a/public/js/api.js b/public/js/api.js index 176ae59..5a55460 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -79,6 +79,18 @@ export const loadself = async () => { return await request("/users/self", {}) } +export const follow = async (state, user_id) => { + return await request('/users/follow', {state, user_id}, 'PUT') +} + +export const follow_status = async (user_id) => { + return await request('/users/follow', {user_id}) +} + +export const friends = async (user_id) => { + return await request('/users/friends', {user_id}) +} + export const postcomment = async (post_id, content) => { return await request('/posts/comment', {post_id, content}, 'PATCH') } diff --git a/public/js/people.js b/public/js/people.js index 0a97e93..99890d9 100644 --- a/public/js/people.js +++ b/public/js/people.js @@ -34,22 +34,7 @@ const data = { } async function load() { - let request = await loadself() - if (request.status === 429) { - let new_body = - body({}, - ...header(false, true) - ) - - document.body.replaceWith(new_body) - throw new Error("Rate limited"); - } - - - const self = request.json - - header(false, true, self.user_id) const users = (await loaduserspage(page)).json if (users.length === 0) { diff --git a/public/js/profile.js b/public/js/profile.js index dd5216d..4e4f44f 100644 --- a/public/js/profile.js +++ b/public/js/profile.js @@ -1,31 +1,55 @@ import { div, pfp, banner, parse, button, body, a, span, crawl, parseDate, parseMonth } from './main.js' -import { loadself, loadusers, loadusersposts, updateavatar, updatebanner, logout } from './api.js' -import { parsePost, header } from './components.js' +import { loadself, loadusers, loadusersposts, updateavatar, updatebanner, logout, follow, follow_status, friends } from './api.js' +import { parsePost, parseUser, header } from './components.js' -function swap(value) { - let postsb = document.getElementById("profilepostbutton"); - let aboutb = document.getElementById("profileaboutbutton"); - let posts = document.getElementById("posts"); - let about = document.getElementById("about"); - let load = document.getElementsByClassName("loadp")[0]; +function swap(tab) { + let post_button = document.querySelector("#profilepostbutton"); + let about_button = document.querySelector("#profileaboutbutton"); + let friends_button = document.querySelector("#profilefriendsbutton"); - if (value) { + let posts_section = document.querySelector("#posts"); + let about_section = document.querySelector("#about"); + let friends_section = document.querySelector("#friends"); + + let load = document.querySelector(".loadp"); - postsb.classList.add("selected") - aboutb.classList.remove("selected") - about.classList.add("hidden") - posts.classList.remove("hidden") + if (tab === 0) { + + post_button.classList.add("selected") + about_button.classList.remove("selected") + friends_button.classList.remove("selected") + + posts_section.classList.remove("hidden") + about_section.classList.add("hidden") + friends_section.classList.add("hidden") if (load) { load.classList.remove("hidden") } - } else { + } else if (tab === 1) { - postsb.classList.remove("selected") - aboutb.classList.add("selected") - about.classList.remove("hidden") - posts.classList.add("hidden") + post_button.classList.remove("selected") + about_button.classList.add("selected") + friends_button.classList.remove("selected") + + posts_section.classList.add("hidden") + about_section.classList.remove("hidden") + friends_section.classList.add("hidden") + + if (load) { + load.classList.add("hidden") + } + + } else if (tab === 2) { + + post_button.classList.remove("selected") + about_button.classList.remove("selected") + friends_button.classList.add("selected") + + posts_section.classList.add("hidden") + about_section.classList.add("hidden") + friends_section.classList.remove("hidden") if (load) { load.classList.add("hidden") @@ -67,9 +91,37 @@ function changeimage(fn) { input.click(); } -function render() { +function status_text(status) { + switch (status) { + case 1: + return 'Following ✓' + case 2: + return 'Follow Back' + case 3: + return 'Friends ✓' + default: + return 'Follow' + } +} - let new_body = +async function render() { + + let status; + if (!isself) { + let response = await follow_status(data.user.user_id) + if (response.status == 200) { + status = parseInt(response.msg) + } else { + status = 0; + } + } + + let friends_arr = (await friends(data.user.user_id)).json + if (friends_arr == undefined) { + friends_arr = [] + } + + let new_body = body({}, ...header(false, false, data.self.user_id), div({id: 'top'}, @@ -91,16 +143,53 @@ function render() { span({class: 'gtext'}, parse('Joined ' + parseDate(new Date(data.user.date))) ) + ), + !isself ? + div({class: 'right'}, + div({class: `follow ${status == 3 ? 'friend' : ''}`, onclick: async (event) => { + let button = event.target + if (button.tagName == 'SPAN') { + button = button.parentElement + } + + let response + if (status % 2 == 0) { + response = await follow(true, data.user.user_id); + } else { + response = await follow(false, data.user.user_id); + } + + if (response.status == 200) { + status = parseInt(response.msg) + } else { + return + } + + button.firstChild.innerHTML = status_text(status) + if (status == 3) { + button.classList.add('friend') + } else { + button.classList.remove('friend') + } + }}, + span({class: 'gtext'}, + parse(status_text(status)) + ) + ) ) + : parse('') ), div({class: 'fullline', style: 'width: 80em; margin-bottom: 0; z-index: 0;'}), div({class: 'profilebuttons'}, - button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(true)}, + button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(0)}, parse('Posts') ), - button({id: 'profileaboutbutton', onclick: () => swap(false)}, + button({id: 'profileaboutbutton', onclick: () => swap(1)}, parse('About') ), + button({id: 'profilefriendsbutton', onclick: () => swap(2)}, + parse('Friends') + ), div({style: 'flex: 20'}), isself ? button({class: 'logout', onclick: async () => { const response = await logout() @@ -154,6 +243,9 @@ function render() { ) ) ), + div({id: 'friends', class: 'hidden'}, + ...friends_arr.map(u => parseUser(u)) + ), div({id: 'popup', class: 'hidden'}, div({class: 'createpost'}, div({class: 'close', onclick: () => document.getElementById('popup').classList.add('hidden')}), @@ -207,7 +299,6 @@ async function load(id) { if (el) { el.remove() } - return [] } else { page++ } @@ -269,7 +360,7 @@ async function init() { const posts = await load(id) data.posts.push(... posts) - + data.user = data.users[id] render() diff --git a/public/profile.html b/public/profile.html index 35debf8..6d4b117 100644 --- a/public/profile.html +++ b/public/profile.html @@ -18,6 +18,7 @@ + diff --git a/src/api/mod.rs b/src/api/mod.rs index 12563e3..cd2190c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -16,7 +16,7 @@ pub use auth::RegistrationRequet; pub fn router() -> Router { let governor_conf = Box::new( GovernorConfigBuilder::default() - .burst_size(10) + .burst_size(15) .per_second(1) .key_extractor(SmartIpKeyExtractor) .finish() diff --git a/src/api/posts.rs b/src/api/posts.rs index ca459cd..ee590ec 100644 --- a/src/api/posts.rs +++ b/src/api/posts.rs @@ -138,7 +138,7 @@ pub const COMMENTS_PAGE: EndpointDocumentation = EndpointDocumentation { #[derive(Deserialize)] struct CommentsPageRequest { page: u64, - post_id: u64 + post_id: u64, } impl Check for CommentsPageRequest { diff --git a/src/api/users.rs b/src/api/users.rs index 0ce9988..082926e 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,7 +1,7 @@ use crate::{ public::docs::{EndpointDocumentation, EndpointMethod}, types::{ - extract::{AuthorizedUser, Check, CheckResult, Json, Png}, + extract::{AuthorizedUser, Check, CheckResult, Json, Log, Png}, http::ResponseCode, user::User, }, @@ -116,7 +116,7 @@ pub const USERS_SELF: EndpointDocumentation = EndpointDocumentation { cookie: Some("auth"), }; -async fn load_self(AuthorizedUser(user): AuthorizedUser) -> Response { +async fn load_self(AuthorizedUser(user): AuthorizedUser, _: Log) -> Response { let Ok(json) = serde_json::to_string(&user) else { return ResponseCode::InternalServerError.text("Failed to fetch user") }; @@ -172,6 +172,143 @@ async fn banner(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response ResponseCode::Success.text("Successfully updated banner") } +pub const USERS_FOLLOW: EndpointDocumentation = EndpointDocumentation { + uri: "/api/users/follow", + method: EndpointMethod::Put, + description: "Set following status of another user", + body: Some( + r#" + { + "user_id": 13, + "status": false + } + "#, + ), + responses: &[ + (200, "Returns new follow status if successfull, see below"), + (400, "Body does not match parameters"), + (401, "Unauthorized"), + (500, "Failed to change follow status"), + ], + cookie: Some("auth"), +}; + +#[derive(Deserialize)] +struct UserFollowRequest { + user_id: u64, + state: bool, +} + +impl Check for UserFollowRequest { + fn check(&self) -> CheckResult { + Ok(()) + } +} + +async fn follow( + AuthorizedUser(user): AuthorizedUser, + Json(body): Json, +) -> Response { + if body.state { + if let Err(err) = User::add_following(user.user_id, body.user_id) { + return err; + } + } else if let Err(err) = User::remove_following(user.user_id, body.user_id) { + return err; + } + + match User::get_following(user.user_id, body.user_id) { + Ok(status) => ResponseCode::Success.text(&format!("{status}")), + Err(err) => err, + } +} + +pub const USERS_FOLLOW_STATUS: EndpointDocumentation = EndpointDocumentation { + uri: "/api/users/follow", + method: EndpointMethod::Post, + description: "Get following status of another user", + body: Some( + r#" + { + "user_id": 13 + } + "#, + ), + responses: &[ + ( + 200, + "Returns 0 if no relation, 1 if following, 2 if followed, 3 if both", + ), + (400, "Body does not match parameters"), + (401, "Unauthorized"), + (500, "Failed to retrieve follow status"), + ], + cookie: Some("auth"), +}; + +#[derive(Deserialize)] +struct UserFollowStatusRequest { + user_id: u64, +} + +impl Check for UserFollowStatusRequest { + fn check(&self) -> CheckResult { + Ok(()) + } +} + +async fn follow_status( + AuthorizedUser(user): AuthorizedUser, + Json(body): Json, +) -> Response { + match User::get_following(user.user_id, body.user_id) { + Ok(status) => ResponseCode::Success.text(&format!("{status}")), + Err(err) => err, + } +} + +pub const USERS_FRIENDS: EndpointDocumentation = EndpointDocumentation { + uri: "/api/users/friends", + method: EndpointMethod::Post, + description: "Returns friends of a user", + body: Some( + r#" + { + "user_id": 13 + } + "#, + ), + responses: &[ + (200, "Returns users in application/json"), + (401, "Unauthorized"), + (500, "Failed to fetch friends"), + ], + cookie: Some("auth"), +}; + +#[derive(Deserialize)] +struct UserFriendsRequest { + user_id: u64, +} + +impl Check for UserFriendsRequest { + fn check(&self) -> CheckResult { + Ok(()) + } +} + +async fn friends(AuthorizedUser(_user): AuthorizedUser, Json(body): Json) -> Response { + let Ok(users) = User::get_friends(body.user_id) else { + return ResponseCode::InternalServerError.text("Failed to fetch user") + }; + + let Ok(json) = serde_json::to_string(&users) else { + return ResponseCode::InternalServerError.text("Failed to fetch user") + }; + + ResponseCode::Success.json(&json) +} + pub fn router() -> Router { Router::new() .route("/load", post(load_batch)) @@ -179,4 +316,6 @@ pub fn router() -> Router { .route("/page", post(load_page)) .route("/avatar", put(avatar)) .route("/banner", put(banner)) + .route("/follow", put(follow).post(follow_status)) + .route("/friends", post(friends)) } diff --git a/src/database/friends.rs b/src/database/friends.rs new file mode 100644 index 0000000..0b78488 --- /dev/null +++ b/src/database/friends.rs @@ -0,0 +1,97 @@ +use tracing::instrument; + +use crate::{ + database::{self, users::user_from_row}, + types::user::{User, FOLLOWED, FOLLOWING, NO_RELATION}, +}; + +pub fn init() -> Result<(), rusqlite::Error> { + let sql = " + CREATE TABLE IF NOT EXISTS friends ( + follower_id INTEGER NOT NULL, + followee_id INTEGER NOT NULL, + FOREIGN KEY(follower_id) REFERENCES users(user_id), + FOREIGN KEY(followee_id) REFERENCES users(user_id), + PRIMARY KEY (follower_id, followee_id) + ); + "; + let conn = database::connect()?; + conn.execute(sql, ())?; + Ok(()) +} + +#[instrument()] +pub fn get_friend_status(user_id_1: u64, user_id_2: u64) -> Result { + tracing::trace!("Retrieving friend status"); + let conn = database::connect()?; + let mut stmt = conn.prepare("SELECT * FROM friends WHERE (follower_id = ? AND followee_id = ?) OR (follower_id = ? AND followee_id = ?);")?; + let mut status = NO_RELATION; + let rows: Vec = stmt + .query_map([user_id_1, user_id_2, user_id_2, user_id_1], |row| { + let id: u64 = row.get(0)?; + Ok(id) + })? + .into_iter() + .flatten() + .collect(); + + for follower in rows { + if follower == user_id_1 { + status |= FOLLOWING; + } + + if follower == user_id_2 { + status |= FOLLOWED; + } + } + + Ok(status) +} + +#[instrument()] +pub fn get_friends(user_id: u64) -> Result, rusqlite::Error> { + tracing::trace!("Retrieving friends"); + let conn = database::connect()?; + let mut stmt = conn.prepare( + " + SELECT * + FROM users u + WHERE EXISTS ( + SELECT NULL + FROM friends f + WHERE u.user_id = f.follower_id + AND f.followee_id = ? + ) + AND EXISTS ( + SELECT NULL + FROM friends f + WHERE u.user_id = f.followee_id + AND f.follower_id = ? + ) + ", + )?; + let row = stmt.query_map([user_id, user_id], |row| { + let row = user_from_row(row, true)?; + Ok(row) + })?; + Ok(row.into_iter().flatten().collect()) +} + +#[instrument()] +pub fn set_following(user_id_1: u64, user_id_2: u64) -> Result { + tracing::trace!("Setting following"); + let conn = database::connect()?; + let mut stmt = + conn.prepare("INSERT OR REPLACE INTO friends (follower_id, followee_id) VALUES (?,?)")?; + let changes = stmt.execute([user_id_1, user_id_2])?; + Ok(changes == 1) +} + +#[instrument()] +pub fn remove_following(user_id_1: u64, user_id_2: u64) -> Result { + tracing::trace!("Removing following"); + let conn = database::connect()?; + let mut stmt = conn.prepare("DELETE FROM friends WHERE follower_id = ? AND followee_id = ?")?; + let changes = stmt.execute([user_id_1, user_id_2])?; + Ok(changes == 1) +} diff --git a/src/database/likes.rs b/src/database/likes.rs index 6f6939e..f6a130b 100644 --- a/src/database/likes.rs +++ b/src/database/likes.rs @@ -37,9 +37,7 @@ pub fn get_liked(user_id: u64, post_id: u64) -> Result { tracing::trace!("Retrieving if liked"); let conn = database::connect()?; let mut stmt = conn.prepare("SELECT * FROM likes WHERE user_id = ? AND post_id = ?")?; - let liked = stmt.query_row([user_id, post_id], |_| { - Ok(()) - }).optional()?; + let liked = stmt.query_row([user_id, post_id], |_| Ok(())).optional()?; Ok(liked.is_some()) } @@ -49,7 +47,7 @@ pub fn add_liked(user_id: u64, post_id: u64) -> Result { let conn = database::connect()?; let mut stmt = conn.prepare("INSERT OR REPLACE INTO likes (user_id, post_id) VALUES (?,?)")?; let changes = stmt.execute([user_id, post_id])?; - Ok(changes == 1) + Ok(changes == 1) } #[instrument()] @@ -69,7 +67,7 @@ pub fn get_all_likes() -> Result, rusqlite::Error> { let row = stmt.query_map([], |row| { let like = Like { user_id: row.get(0)?, - post_id: row.get(1)? + post_id: row.get(1)?, }; Ok(like) })?; diff --git a/src/database/mod.rs b/src/database/mod.rs index 6d4853a..d22a350 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,6 +1,7 @@ use tracing::instrument; pub mod comments; +pub mod friends; pub mod likes; pub mod posts; pub mod sessions; @@ -16,6 +17,7 @@ pub fn init() -> Result<(), rusqlite::Error> { sessions::init()?; likes::init()?; comments::init()?; + friends::init()?; Ok(()) } diff --git a/src/database/users.rs b/src/database/users.rs index 15565f1..6062ea8 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -31,7 +31,7 @@ pub fn init() -> Result<(), rusqlite::Error> { Ok(()) } -fn user_from_row(row: &Row, hide_password: bool) -> Result { +pub fn user_from_row(row: &Row, hide_password: bool) -> Result { let user_id = row.get(0)?; let firstname = row.get(1)?; let lastname = row.get(2)?; diff --git a/src/public/admin.rs b/src/public/admin.rs index 25941f1..bf0a155 100644 --- a/src/public/admin.rs +++ b/src/public/admin.rs @@ -5,7 +5,9 @@ use tokio::sync::Mutex; use crate::{ console::sanatize, - types::{http::ResponseCode, post::Post, session::Session, user::User, comment::Comment, like::Like}, + types::{ + comment::Comment, http::ResponseCode, like::Like, post::Post, session::Session, user::User, + }, }; lazy_static! { @@ -141,7 +143,11 @@ pub fn generate_comments() -> Response { for comment in comments { html.push_str(&format!( "{}{}{}{}{}", - comment.comment_id, comment.user_id, comment.post_id, sanatize(&comment.content), comment.date + comment.comment_id, + comment.user_id, + comment.post_id, + sanatize(&comment.content), + comment.date )); } diff --git a/src/public/console.rs b/src/public/console.rs index 16bf4a3..251dbc1 100644 --- a/src/public/console.rs +++ b/src/public/console.rs @@ -84,9 +84,9 @@ impl Formatter for HtmlFormatter { W: ?Sized + io::Write, { let s = if value { - b"true" as &[u8] + b" true " as &[u8] } else { - b"false" as &[u8] + b" false " as &[u8] }; writer.write_all(s) } @@ -95,7 +95,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -103,7 +103,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -111,7 +111,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -119,7 +119,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -127,7 +127,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -135,7 +135,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -143,7 +143,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -151,7 +151,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -159,7 +159,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -167,7 +167,7 @@ impl Formatter for HtmlFormatter { where W: ?Sized + io::Write, { - let buff = format!("{value}"); + let buff = format!(" {value} "); writer.write_all(buff.as_bytes()) } @@ -192,7 +192,7 @@ impl Formatter for HtmlFormatter { if first { writer.write_all(b"") } else { - writer.write_all(b",") + writer.write_all(b",") } } @@ -202,6 +202,13 @@ impl Formatter for HtmlFormatter { { writer.write_all(b"") } + + fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b" : ") + } } pub fn sanatize(input: &str) -> String { diff --git a/src/public/docs.rs b/src/public/docs.rs index f4e26be..397e696 100644 --- a/src/public/docs.rs +++ b/src/public/docs.rs @@ -49,6 +49,7 @@ fn generate_body(body: Option<&'static str>) -> String { "# .to_string(); + let body = body.trim(); if body.starts_with('{') { return html.replace( @@ -135,13 +136,16 @@ pub async fn init() { users::USERS_SELF, users::USERS_AVATAR, users::USERS_BANNER, + users::USERS_FOLLOW, + users::USERS_FOLLOW_STATUS, + users::USERS_FRIENDS, admin::ADMIN_AUTH, admin::ADMIN_QUERY, admin::ADMIN_POSTS, admin::ADMIN_USERS, admin::ADMIN_SESSIONS, admin::ADMIN_COMMENTS, - admin::ADMIN_LIKES + admin::ADMIN_LIKES, ]; let mut endpoints = ENDPOINTS.lock().await; for doc in docs { diff --git a/src/public/mod.rs b/src/public/mod.rs index 76796ea..bb75ef0 100644 --- a/src/public/mod.rs +++ b/src/public/mod.rs @@ -25,7 +25,7 @@ pub mod pages; pub fn router() -> Router { let governor_conf = Box::new( GovernorConfigBuilder::default() - .burst_size(20) + .burst_size(30) .per_second(1) .key_extractor(SmartIpKeyExtractor) .finish() diff --git a/src/public/pages.rs b/src/public/pages.rs index 6d5c0de..426727e 100644 --- a/src/public/pages.rs +++ b/src/public/pages.rs @@ -1,6 +1,7 @@ use axum::{ response::{IntoResponse, Redirect, Response}, - routing::get, Router + routing::get, + Router, }; use crate::{ @@ -58,9 +59,8 @@ async fn wordpress(_: Log) -> Response { } async fn forgot(UserAgent(agent): UserAgent, _: Log) -> Response { - if agent.starts_with("curl") { - return super::serve("/404.html").await + return super::serve("/404.html").await; } Redirect::to("https://www.youtube.com/watch?v=dQw4w9WgXcQ").into_response() diff --git a/src/types/extract.rs b/src/types/extract.rs index 6a01ad2..65d9f1a 100644 --- a/src/types/extract.rs +++ b/src/types/extract.rs @@ -7,7 +7,7 @@ use axum::{ async_trait, body::HttpBody, extract::{ConnectInfo, FromRequest, FromRequestParts}, - http::{request::Parts, Request, header::USER_AGENT}, + http::{header::USER_AGENT, request::Parts, Request}, response::Response, BoxError, RequestExt, }; @@ -205,7 +205,7 @@ where }; let Ok(value) = serde_json::from_str::(&body) else { - return Err(ResponseCode::BadRequest.text("Invalid request body")) + return Err(ResponseCode::BadRequest.text("Body does not match paramaters")) }; if let Err(msg) = value.check() { @@ -256,7 +256,7 @@ where return Err(ResponseCode::BadRequest.text("Bad Request")); }; - Ok(UserAgent(agent.to_string())) + Ok(Self(agent.to_string())) } } diff --git a/src/types/like.rs b/src/types/like.rs index bf10b2d..1c113c1 100644 --- a/src/types/like.rs +++ b/src/types/like.rs @@ -7,13 +7,12 @@ use crate::types::http::{ResponseCode, Result}; #[derive(Serialize)] pub struct Like { pub user_id: u64, - pub post_id: u64 + pub post_id: u64, } impl Like { #[instrument()] pub fn add_liked(user_id: u64, post_id: u64) -> Result<()> { - let Ok(liked) = database::likes::add_liked(user_id, post_id) else { return Err(ResponseCode::BadRequest.text("Failed to add like status")) }; diff --git a/src/types/user.rs b/src/types/user.rs index 835b675..245e9b7 100644 --- a/src/types/user.rs +++ b/src/types/user.rs @@ -19,6 +19,10 @@ pub struct User { pub year: u32, } +pub const NO_RELATION: u8 = 0; +pub const FOLLOWING: u8 = 1; +pub const FOLLOWED: u8 = 2; + impl User { #[instrument()] pub fn from_user_id(user_id: u64, hide_password: bool) -> Result { @@ -95,4 +99,42 @@ impl User { Ok(user) } + + pub fn add_following(user_id_1: u64, user_id_2: u64) -> Result<()> { + let Ok(followed) = database::friends::set_following(user_id_1, user_id_2) else { + return Err(ResponseCode::BadRequest.text("Failed to add follow status")) + }; + + if !followed { + return Err(ResponseCode::InternalServerError.text("Failed to add follow status")); + } + + Ok(()) + } + + pub fn remove_following(user_id_1: u64, user_id_2: u64) -> Result<()> { + let Ok(followed) = database::friends::remove_following(user_id_1, user_id_2) else { + return Err(ResponseCode::BadRequest.text("Failed to remove follow status")) + }; + + if !followed { + return Err(ResponseCode::InternalServerError.text("Failed to remove follow status")); + } + + Ok(()) + } + + pub fn get_following(user_id_1: u64, user_id_2: u64) -> Result { + let Ok(followed) = database::friends::get_friend_status(user_id_1, user_id_2) else { + return Err(ResponseCode::InternalServerError.text("Failed to get follow status")) + }; + Ok(followed) + } + + pub fn get_friends(user_id: u64) -> Result> { + let Ok(users) = database::friends::get_friends(user_id) else { + return Err(ResponseCode::InternalServerError.text("Failed to fetch friends")) + }; + Ok(users) + } }