friends
This commit is contained in:
parent
b6fbeb5124
commit
f02524b592
20 changed files with 490 additions and 75 deletions
|
@ -119,7 +119,7 @@ body {
|
||||||
border-bottom: 3px solid var(--logo) !important;
|
border-bottom: 3px solid var(--logo) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#about {
|
#about, #friends {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -161,3 +161,35 @@ body {
|
||||||
.logout {
|
.logout {
|
||||||
flex: 1;
|
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;
|
||||||
|
}
|
|
@ -79,6 +79,18 @@ export const loadself = async () => {
|
||||||
return await request("/users/self", {})
|
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) => {
|
export const postcomment = async (post_id, content) => {
|
||||||
return await request('/posts/comment', {post_id, content}, 'PATCH')
|
return await request('/posts/comment', {post_id, content}, 'PATCH')
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,22 +34,7 @@ const data = {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
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
|
const users = (await loaduserspage(page)).json
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
|
|
|
@ -1,31 +1,55 @@
|
||||||
import { div, pfp, banner, parse, button, body, a, span, crawl, parseDate, parseMonth } from './main.js'
|
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 { loadself, loadusers, loadusersposts, updateavatar, updatebanner, logout, follow, follow_status, friends } from './api.js'
|
||||||
import { parsePost, header } from './components.js'
|
import { parsePost, parseUser, header } from './components.js'
|
||||||
|
|
||||||
function swap(value) {
|
function swap(tab) {
|
||||||
let postsb = document.getElementById("profilepostbutton");
|
let post_button = document.querySelector("#profilepostbutton");
|
||||||
let aboutb = document.getElementById("profileaboutbutton");
|
let about_button = document.querySelector("#profileaboutbutton");
|
||||||
let posts = document.getElementById("posts");
|
let friends_button = document.querySelector("#profilefriendsbutton");
|
||||||
let about = document.getElementById("about");
|
|
||||||
let load = document.getElementsByClassName("loadp")[0];
|
|
||||||
|
|
||||||
if (value) {
|
let posts_section = document.querySelector("#posts");
|
||||||
|
let about_section = document.querySelector("#about");
|
||||||
|
let friends_section = document.querySelector("#friends");
|
||||||
|
|
||||||
postsb.classList.add("selected")
|
let load = document.querySelector(".loadp");
|
||||||
aboutb.classList.remove("selected")
|
|
||||||
about.classList.add("hidden")
|
if (tab === 0) {
|
||||||
posts.classList.remove("hidden")
|
|
||||||
|
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) {
|
if (load) {
|
||||||
load.classList.remove("hidden")
|
load.classList.remove("hidden")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else if (tab === 1) {
|
||||||
|
|
||||||
postsb.classList.remove("selected")
|
post_button.classList.remove("selected")
|
||||||
aboutb.classList.add("selected")
|
about_button.classList.add("selected")
|
||||||
about.classList.remove("hidden")
|
friends_button.classList.remove("selected")
|
||||||
posts.classList.add("hidden")
|
|
||||||
|
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) {
|
if (load) {
|
||||||
load.classList.add("hidden")
|
load.classList.add("hidden")
|
||||||
|
@ -67,7 +91,35 @@ function changeimage(fn) {
|
||||||
input.click();
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
let new_body =
|
||||||
body({},
|
body({},
|
||||||
|
@ -91,16 +143,53 @@ function render() {
|
||||||
span({class: 'gtext'},
|
span({class: 'gtext'},
|
||||||
parse('Joined ' + parseDate(new Date(data.user.date)))
|
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: 'fullline', style: 'width: 80em; margin-bottom: 0; z-index: 0;'}),
|
||||||
div({class: 'profilebuttons'},
|
div({class: 'profilebuttons'},
|
||||||
button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(true)},
|
button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(0)},
|
||||||
parse('Posts')
|
parse('Posts')
|
||||||
),
|
),
|
||||||
button({id: 'profileaboutbutton', onclick: () => swap(false)},
|
button({id: 'profileaboutbutton', onclick: () => swap(1)},
|
||||||
parse('About')
|
parse('About')
|
||||||
),
|
),
|
||||||
|
button({id: 'profilefriendsbutton', onclick: () => swap(2)},
|
||||||
|
parse('Friends')
|
||||||
|
),
|
||||||
div({style: 'flex: 20'}),
|
div({style: 'flex: 20'}),
|
||||||
isself ? button({class: 'logout', onclick: async () => {
|
isself ? button({class: 'logout', onclick: async () => {
|
||||||
const response = await logout()
|
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({id: 'popup', class: 'hidden'},
|
||||||
div({class: 'createpost'},
|
div({class: 'createpost'},
|
||||||
div({class: 'close', onclick: () => document.getElementById('popup').classList.add('hidden')}),
|
div({class: 'close', onclick: () => document.getElementById('popup').classList.add('hidden')}),
|
||||||
|
@ -207,7 +299,6 @@ async function load(id) {
|
||||||
if (el) {
|
if (el) {
|
||||||
el.remove()
|
el.remove()
|
||||||
}
|
}
|
||||||
return []
|
|
||||||
} else {
|
} else {
|
||||||
page++
|
page++
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
<link rel="stylesheet" href="/css/header.css">
|
<link rel="stylesheet" href="/css/header.css">
|
||||||
|
<link rel="stylesheet" href="/css/people.css">
|
||||||
<link rel="stylesheet" href="/css/profile.css">
|
<link rel="stylesheet" href="/css/profile.css">
|
||||||
<link rel="stylesheet" href="/css/home.css">
|
<link rel="stylesheet" href="/css/home.css">
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub use auth::RegistrationRequet;
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
let governor_conf = Box::new(
|
let governor_conf = Box::new(
|
||||||
GovernorConfigBuilder::default()
|
GovernorConfigBuilder::default()
|
||||||
.burst_size(10)
|
.burst_size(15)
|
||||||
.per_second(1)
|
.per_second(1)
|
||||||
.key_extractor(SmartIpKeyExtractor)
|
.key_extractor(SmartIpKeyExtractor)
|
||||||
.finish()
|
.finish()
|
||||||
|
|
|
@ -138,7 +138,7 @@ pub const COMMENTS_PAGE: EndpointDocumentation = EndpointDocumentation {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CommentsPageRequest {
|
struct CommentsPageRequest {
|
||||||
page: u64,
|
page: u64,
|
||||||
post_id: u64
|
post_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Check for CommentsPageRequest {
|
impl Check for CommentsPageRequest {
|
||||||
|
|
143
src/api/users.rs
143
src/api/users.rs
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
public::docs::{EndpointDocumentation, EndpointMethod},
|
public::docs::{EndpointDocumentation, EndpointMethod},
|
||||||
types::{
|
types::{
|
||||||
extract::{AuthorizedUser, Check, CheckResult, Json, Png},
|
extract::{AuthorizedUser, Check, CheckResult, Json, Log, Png},
|
||||||
http::ResponseCode,
|
http::ResponseCode,
|
||||||
user::User,
|
user::User,
|
||||||
},
|
},
|
||||||
|
@ -116,7 +116,7 @@ pub const USERS_SELF: EndpointDocumentation = EndpointDocumentation {
|
||||||
cookie: Some("auth"),
|
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 {
|
let Ok(json) = serde_json::to_string(&user) else {
|
||||||
return ResponseCode::InternalServerError.text("Failed to fetch user")
|
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")
|
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<UserFollowRequest>,
|
||||||
|
) -> 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<UserFollowStatusRequest>,
|
||||||
|
) -> 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 <span>application/json<span>"),
|
||||||
|
(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<UserFriendsRequest>) -> 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 {
|
pub fn router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/load", post(load_batch))
|
.route("/load", post(load_batch))
|
||||||
|
@ -179,4 +316,6 @@ pub fn router() -> Router {
|
||||||
.route("/page", post(load_page))
|
.route("/page", post(load_page))
|
||||||
.route("/avatar", put(avatar))
|
.route("/avatar", put(avatar))
|
||||||
.route("/banner", put(banner))
|
.route("/banner", put(banner))
|
||||||
|
.route("/follow", put(follow).post(follow_status))
|
||||||
|
.route("/friends", post(friends))
|
||||||
}
|
}
|
||||||
|
|
97
src/database/friends.rs
Normal file
97
src/database/friends.rs
Normal file
|
@ -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<u8, rusqlite::Error> {
|
||||||
|
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<u64> = 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<Vec<User>, 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<bool, rusqlite::Error> {
|
||||||
|
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<bool, rusqlite::Error> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -37,9 +37,7 @@ pub fn get_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> {
|
||||||
tracing::trace!("Retrieving if liked");
|
tracing::trace!("Retrieving if liked");
|
||||||
let conn = database::connect()?;
|
let conn = database::connect()?;
|
||||||
let mut stmt = conn.prepare("SELECT * FROM likes WHERE user_id = ? AND post_id = ?")?;
|
let mut stmt = conn.prepare("SELECT * FROM likes WHERE user_id = ? AND post_id = ?")?;
|
||||||
let liked = stmt.query_row([user_id, post_id], |_| {
|
let liked = stmt.query_row([user_id, post_id], |_| Ok(())).optional()?;
|
||||||
Ok(())
|
|
||||||
}).optional()?;
|
|
||||||
Ok(liked.is_some())
|
Ok(liked.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ pub fn get_all_likes() -> Result<Vec<Like>, rusqlite::Error> {
|
||||||
let row = stmt.query_map([], |row| {
|
let row = stmt.query_map([], |row| {
|
||||||
let like = Like {
|
let like = Like {
|
||||||
user_id: row.get(0)?,
|
user_id: row.get(0)?,
|
||||||
post_id: row.get(1)?
|
post_id: row.get(1)?,
|
||||||
};
|
};
|
||||||
Ok(like)
|
Ok(like)
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
|
pub mod friends;
|
||||||
pub mod likes;
|
pub mod likes;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
|
@ -16,6 +17,7 @@ pub fn init() -> Result<(), rusqlite::Error> {
|
||||||
sessions::init()?;
|
sessions::init()?;
|
||||||
likes::init()?;
|
likes::init()?;
|
||||||
comments::init()?;
|
comments::init()?;
|
||||||
|
friends::init()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ pub fn init() -> Result<(), rusqlite::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_from_row(row: &Row, hide_password: bool) -> Result<User, rusqlite::Error> {
|
pub fn user_from_row(row: &Row, hide_password: bool) -> Result<User, rusqlite::Error> {
|
||||||
let user_id = row.get(0)?;
|
let user_id = row.get(0)?;
|
||||||
let firstname = row.get(1)?;
|
let firstname = row.get(1)?;
|
||||||
let lastname = row.get(2)?;
|
let lastname = row.get(2)?;
|
||||||
|
|
|
@ -5,7 +5,9 @@ use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
console::sanatize,
|
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! {
|
lazy_static! {
|
||||||
|
@ -141,7 +143,11 @@ pub fn generate_comments() -> Response {
|
||||||
for comment in comments {
|
for comment in comments {
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
|
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
|
||||||
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
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,9 +84,9 @@ impl Formatter for HtmlFormatter {
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let s = if value {
|
let s = if value {
|
||||||
b"<span class='bool'>true</span>" as &[u8]
|
b"<span class='bool'> true </span>" as &[u8]
|
||||||
} else {
|
} else {
|
||||||
b"<span class='bool'>false</span>" as &[u8]
|
b"<span class='bool'> false </span>" as &[u8]
|
||||||
};
|
};
|
||||||
writer.write_all(s)
|
writer.write_all(s)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ impl Formatter for HtmlFormatter {
|
||||||
where
|
where
|
||||||
W: ?Sized + io::Write,
|
W: ?Sized + io::Write,
|
||||||
{
|
{
|
||||||
let buff = format!("<span class='number'>{value}</span>");
|
let buff = format!("<span class='number'> {value} </span>");
|
||||||
writer.write_all(buff.as_bytes())
|
writer.write_all(buff.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ impl Formatter for HtmlFormatter {
|
||||||
if first {
|
if first {
|
||||||
writer.write_all(b"<span class='key'>")
|
writer.write_all(b"<span class='key'>")
|
||||||
} else {
|
} else {
|
||||||
writer.write_all(b"<span class='key'>,")
|
writer.write_all(b",<span class='key'>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +202,13 @@ impl Formatter for HtmlFormatter {
|
||||||
{
|
{
|
||||||
writer.write_all(b"</span>")
|
writer.write_all(b"</span>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn begin_object_value<W>(&mut self, writer: &mut W) -> io::Result<()>
|
||||||
|
where
|
||||||
|
W: ?Sized + io::Write,
|
||||||
|
{
|
||||||
|
writer.write_all(b" : ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sanatize(input: &str) -> String {
|
pub fn sanatize(input: &str) -> String {
|
||||||
|
|
|
@ -49,6 +49,7 @@ fn generate_body(body: Option<&'static str>) -> String {
|
||||||
</div>
|
</div>
|
||||||
"#
|
"#
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let body = body.trim();
|
let body = body.trim();
|
||||||
if body.starts_with('{') {
|
if body.starts_with('{') {
|
||||||
return html.replace(
|
return html.replace(
|
||||||
|
@ -135,13 +136,16 @@ pub async fn init() {
|
||||||
users::USERS_SELF,
|
users::USERS_SELF,
|
||||||
users::USERS_AVATAR,
|
users::USERS_AVATAR,
|
||||||
users::USERS_BANNER,
|
users::USERS_BANNER,
|
||||||
|
users::USERS_FOLLOW,
|
||||||
|
users::USERS_FOLLOW_STATUS,
|
||||||
|
users::USERS_FRIENDS,
|
||||||
admin::ADMIN_AUTH,
|
admin::ADMIN_AUTH,
|
||||||
admin::ADMIN_QUERY,
|
admin::ADMIN_QUERY,
|
||||||
admin::ADMIN_POSTS,
|
admin::ADMIN_POSTS,
|
||||||
admin::ADMIN_USERS,
|
admin::ADMIN_USERS,
|
||||||
admin::ADMIN_SESSIONS,
|
admin::ADMIN_SESSIONS,
|
||||||
admin::ADMIN_COMMENTS,
|
admin::ADMIN_COMMENTS,
|
||||||
admin::ADMIN_LIKES
|
admin::ADMIN_LIKES,
|
||||||
];
|
];
|
||||||
let mut endpoints = ENDPOINTS.lock().await;
|
let mut endpoints = ENDPOINTS.lock().await;
|
||||||
for doc in docs {
|
for doc in docs {
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub mod pages;
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
let governor_conf = Box::new(
|
let governor_conf = Box::new(
|
||||||
GovernorConfigBuilder::default()
|
GovernorConfigBuilder::default()
|
||||||
.burst_size(20)
|
.burst_size(30)
|
||||||
.per_second(1)
|
.per_second(1)
|
||||||
.key_extractor(SmartIpKeyExtractor)
|
.key_extractor(SmartIpKeyExtractor)
|
||||||
.finish()
|
.finish()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::get, Router
|
routing::get,
|
||||||
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -58,9 +59,8 @@ async fn wordpress(_: Log) -> Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn forgot(UserAgent(agent): UserAgent, _: Log) -> Response {
|
async fn forgot(UserAgent(agent): UserAgent, _: Log) -> Response {
|
||||||
|
|
||||||
if agent.starts_with("curl") {
|
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()
|
Redirect::to("https://www.youtube.com/watch?v=dQw4w9WgXcQ").into_response()
|
||||||
|
|
|
@ -7,7 +7,7 @@ use axum::{
|
||||||
async_trait,
|
async_trait,
|
||||||
body::HttpBody,
|
body::HttpBody,
|
||||||
extract::{ConnectInfo, FromRequest, FromRequestParts},
|
extract::{ConnectInfo, FromRequest, FromRequestParts},
|
||||||
http::{request::Parts, Request, header::USER_AGENT},
|
http::{header::USER_AGENT, request::Parts, Request},
|
||||||
response::Response,
|
response::Response,
|
||||||
BoxError, RequestExt,
|
BoxError, RequestExt,
|
||||||
};
|
};
|
||||||
|
@ -205,7 +205,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(value) = serde_json::from_str::<T>(&body) else {
|
let Ok(value) = serde_json::from_str::<T>(&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() {
|
if let Err(msg) = value.check() {
|
||||||
|
@ -256,7 +256,7 @@ where
|
||||||
return Err(ResponseCode::BadRequest.text("Bad Request"));
|
return Err(ResponseCode::BadRequest.text("Bad Request"));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(UserAgent(agent.to_string()))
|
Ok(Self(agent.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,12 @@ use crate::types::http::{ResponseCode, Result};
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Like {
|
pub struct Like {
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
pub post_id: u64
|
pub post_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Like {
|
impl Like {
|
||||||
#[instrument()]
|
#[instrument()]
|
||||||
pub fn add_liked(user_id: u64, post_id: u64) -> Result<()> {
|
pub fn add_liked(user_id: u64, post_id: u64) -> Result<()> {
|
||||||
|
|
||||||
let Ok(liked) = database::likes::add_liked(user_id, post_id) else {
|
let Ok(liked) = database::likes::add_liked(user_id, post_id) else {
|
||||||
return Err(ResponseCode::BadRequest.text("Failed to add like status"))
|
return Err(ResponseCode::BadRequest.text("Failed to add like status"))
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,10 @@ pub struct User {
|
||||||
pub year: u32,
|
pub year: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const NO_RELATION: u8 = 0;
|
||||||
|
pub const FOLLOWING: u8 = 1;
|
||||||
|
pub const FOLLOWED: u8 = 2;
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
#[instrument()]
|
#[instrument()]
|
||||||
pub fn from_user_id(user_id: u64, hide_password: bool) -> Result<Self> {
|
pub fn from_user_id(user_id: u64, hide_password: bool) -> Result<Self> {
|
||||||
|
@ -95,4 +99,42 @@ impl User {
|
||||||
|
|
||||||
Ok(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<u8> {
|
||||||
|
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<Vec<Self>> {
|
||||||
|
let Ok(users) = database::friends::get_friends(user_id) else {
|
||||||
|
return Err(ResponseCode::InternalServerError.text("Failed to fetch friends"))
|
||||||
|
};
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue