Compare commits

..

No commits in common. "e8a9857e19a9d6b9a09ea41279a33cb1d47754d8" and "ac3e7e242b049b7402534e292c1c081701029211" have entirely different histories.

16 changed files with 38 additions and 484 deletions

View file

@ -1,4 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE friend_requests;
DROP FUNCTION check_friend_request;
DROP TABLE friends;

View file

@ -1,35 +0,0 @@
-- Your SQL goes here
CREATE TABLE friends (
uuid1 UUID REFERENCES users(uuid),
uuid2 UUID REFERENCES users(uuid),
accepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (uuid1, uuid2),
CHECK (uuid1 < uuid2)
);
CREATE TABLE friend_requests (
sender UUID REFERENCES users(uuid),
receiver UUID REFERENCES users(uuid),
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (sender, receiver),
CHECK (sender <> receiver)
);
-- Create a function to check for existing friendships
CREATE FUNCTION check_friend_request()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM friends
WHERE (uuid1, uuid2) = (LEAST(NEW.sender, NEW.receiver), GREATEST(NEW.sender, NEW.receiver))
) THEN
RAISE EXCEPTION 'Users are already friends';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create the trigger
CREATE TRIGGER prevent_friend_request_conflict
BEFORE INSERT OR UPDATE ON friend_requests
FOR EACH ROW EXECUTE FUNCTION check_friend_request();

View file

@ -38,9 +38,7 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
refresh_token_cookie.make_removal(); refresh_token_cookie.make_removal();
if deleted == 0 { if deleted == 0 {
return Ok(HttpResponse::NotFound() return Ok(HttpResponse::NotFound().cookie(refresh_token_cookie).finish())
.cookie(refresh_token_cookie)
.finish());
} }
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish())

View file

@ -83,9 +83,7 @@ pub async fn ws(
let message_body: MessageBody = serde_json::from_str(&text)?; let message_body: MessageBody = serde_json::from_str(&text)?;
let message = channel let message = channel.new_message(&data, uuid, message_body.message, message_body.reply_to).await?;
.new_message(&data, uuid, message_body.message, message_body.reply_to)
.await?;
redis::cmd("PUBLISH") redis::cmd("PUBLISH")
.arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?])

View file

@ -2,7 +2,7 @@ use crate::{
Data, Data,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Me, Member}, objects::Member,
utils::{get_auth_header, global_checks}, utils::{get_auth_header, global_checks},
}; };
use ::uuid::Uuid; use ::uuid::Uuid;
@ -28,9 +28,7 @@ pub async fn get(
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let members = Member::fetch_all(&data, guild_uuid).await?;
let members = Member::fetch_all(&data, &me, guild_uuid).await?;
Ok(HttpResponse::Ok().json(members)) Ok(HttpResponse::Ok().json(members))
} }

View file

@ -1,80 +0,0 @@
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
pub mod uuid;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
};
/// Returns a list of users that are your friends
#[get("/friends")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
let friends = me.get_friends(&data).await?;
Ok(HttpResponse::Ok().json(friends))
}
#[derive(Deserialize)]
struct UserReq {
uuid: Uuid,
}
/// `POST /api/v1/me/friends` Send friend request
///
/// requires auth? yes
///
/// ### Request Example:
/// ```
/// json!({
/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6",
/// });
/// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
///
/// ### Responses
/// 200 Success
///
/// 404 Not Found
///
/// 400 Bad Request (usually means users are already friends)
///
#[post("/friends")]
pub async fn post(
req: HttpRequest,
json: web::Json<UserReq>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
me.add_friend(&mut conn, json.uuid).await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,33 +0,0 @@
use actix_web::{HttpRequest, HttpResponse, delete, web};
use uuid::Uuid;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
};
#[delete("/friends/{uuid}")]
pub async fn delete(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
me.remove_friend(&mut conn, path.0).await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -10,7 +10,6 @@ use crate::{
utils::{get_auth_header, global_checks}, utils::{get_auth_header, global_checks},
}; };
mod friends;
mod guilds; mod guilds;
pub fn web() -> Scope { pub fn web() -> Scope {
@ -18,9 +17,6 @@ pub fn web() -> Scope {
.service(get) .service(get)
.service(update) .service(update)
.service(guilds::get) .service(guilds::get)
.service(friends::get)
.service(friends::post)
.service(friends::uuid::delete)
} }
#[get("")] #[get("")]

View file

@ -7,7 +7,7 @@ use crate::{
Data, Data,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Me, User}, objects::User,
utils::{get_auth_header, global_checks}, utils::{get_auth_header, global_checks},
}; };
@ -45,9 +45,7 @@ pub async fn get(
global_checks(&data, uuid).await?; global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let user = User::fetch_one(&data, user_uuid).await?;
let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?;
Ok(HttpResponse::Ok().json(user)) Ok(HttpResponse::Ok().json(user))
} }

View file

@ -1,24 +0,0 @@
use chrono::{DateTime, Utc};
use diesel::{Queryable, Selectable};
use serde::Serialize;
use uuid::Uuid;
use crate::schema::{friend_requests, friends};
#[derive(Serialize, Queryable, Selectable, Clone)]
#[diesel(table_name = friends)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Friend {
pub uuid1: Uuid,
pub uuid2: Uuid,
pub accepted_at: DateTime<Utc>,
}
#[derive(Serialize, Queryable, Selectable, Clone)]
#[diesel(table_name = friend_requests)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct FriendRequest {
pub sender: Uuid,
pub receiver: Uuid,
pub requested_at: DateTime<Utc>,
}

View file

@ -1,8 +1,5 @@
use actix_web::web::BytesMut; use actix_web::web::BytesMut;
use diesel::{ use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, update};
ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into,
update,
};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use tokio::task; use tokio::task;
@ -12,8 +9,7 @@ use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn, Data,
error::Error, error::Error,
objects::{Friend, FriendRequest, User}, schema::{guild_members, guilds, users},
schema::{friend_requests, friends, guild_members, guilds, users},
utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, utils::{EMAIL_REGEX, USERNAME_REGEX, image_check},
}; };
@ -128,10 +124,7 @@ impl Me {
} }
pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> {
if !USERNAME_REGEX.is_match(&new_username) if !USERNAME_REGEX.is_match(&new_username) || new_username.len() < 3 || new_username.len() > 32 {
|| new_username.len() < 3
|| new_username.len() > 32
{
return Err(Error::BadRequest("Invalid username".to_string())); return Err(Error::BadRequest("Invalid username".to_string()));
} }
@ -160,11 +153,13 @@ impl Me {
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = data.pool.get().await?; let mut conn = data.pool.get().await?;
let new_display_name_option = if new_display_name.is_empty() { let new_display_name_option;
None
if new_display_name.is_empty() {
new_display_name_option = None;
} else { } else {
Some(new_display_name) new_display_name_option = Some(new_display_name)
}; }
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -241,179 +236,4 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn friends_with(
&self,
conn: &mut Conn,
user_uuid: Uuid,
) -> Result<Option<Friend>, Error> {
use friends::dsl;
let friends: Vec<Friend> = if self.uuid < user_uuid {
load_or_empty(
dsl::friends
.filter(dsl::uuid1.eq(self.uuid))
.filter(dsl::uuid2.eq(user_uuid))
.load(conn)
.await,
)?
} else {
load_or_empty(
dsl::friends
.filter(dsl::uuid1.eq(user_uuid))
.filter(dsl::uuid2.eq(self.uuid))
.load(conn)
.await,
)?
};
if friends.is_empty() {
return Ok(None);
}
Ok(Some(friends[0].clone()))
}
pub async fn add_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> {
if self.friends_with(conn, user_uuid).await?.is_some() {
// TODO: Check if another error should be used
return Err(Error::BadRequest("Already friends with user".to_string()));
}
use friend_requests::dsl;
let friend_request: Vec<FriendRequest> = load_or_empty(
dsl::friend_requests
.filter(dsl::sender.eq(user_uuid))
.filter(dsl::receiver.eq(self.uuid))
.load(conn)
.await,
)?;
#[allow(clippy::get_first)]
if let Some(friend_request) = friend_request.get(0) {
use friends::dsl;
if self.uuid < user_uuid {
insert_into(friends::table)
.values((dsl::uuid1.eq(self.uuid), dsl::uuid2.eq(user_uuid)))
.execute(conn)
.await?;
} else {
insert_into(friends::table)
.values((dsl::uuid1.eq(user_uuid), dsl::uuid2.eq(self.uuid)))
.execute(conn)
.await?;
}
use friend_requests::dsl as frdsl;
delete(friend_requests::table)
.filter(frdsl::sender.eq(friend_request.sender))
.filter(frdsl::receiver.eq(friend_request.receiver))
.execute(conn)
.await?;
Ok(())
} else {
use friend_requests::dsl;
insert_into(friend_requests::table)
.values((dsl::sender.eq(self.uuid), dsl::receiver.eq(user_uuid)))
.execute(conn)
.await?;
Ok(())
}
}
pub async fn remove_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> {
if self.friends_with(conn, user_uuid).await?.is_none() {
// TODO: Check if another error should be used
return Err(Error::BadRequest("Not friends with user".to_string()));
}
use friends::dsl;
if self.uuid < user_uuid {
delete(friends::table)
.filter(dsl::uuid1.eq(self.uuid))
.filter(dsl::uuid2.eq(user_uuid))
.execute(conn)
.await?;
} else {
delete(friends::table)
.filter(dsl::uuid1.eq(user_uuid))
.filter(dsl::uuid2.eq(self.uuid))
.execute(conn)
.await?;
}
Ok(())
}
pub async fn get_friends(&self, data: &Data) -> Result<Vec<User>, Error> {
use friends::dsl;
let mut conn = data.pool.get().await?;
let friends1 = load_or_empty(
dsl::friends
.filter(dsl::uuid1.eq(self.uuid))
.select(Friend::as_select())
.load(&mut conn)
.await,
)?;
let friends2 = load_or_empty(
dsl::friends
.filter(dsl::uuid2.eq(self.uuid))
.select(Friend::as_select())
.load(&mut conn)
.await,
)?;
let friend_futures = friends1.iter().map(async move |friend| {
User::fetch_one_with_friendship(data, self, friend.uuid2).await
});
let mut friends = futures::future::try_join_all(friend_futures).await?;
let friend_futures = friends2.iter().map(async move |friend| {
User::fetch_one_with_friendship(data, self, friend.uuid1).await
});
friends.append(&mut futures::future::try_join_all(friend_futures).await?);
Ok(friends)
}
/* TODO
pub async fn get_friend_requests(&self, conn: &mut Conn) -> Result<Vec<FriendRequest>, Error> {
use friend_requests::dsl;
let friend_request: Vec<FriendRequest> = load_or_empty(
dsl::friend_requests
.filter(dsl::receiver.eq(self.uuid))
.load(conn)
.await
)?;
Ok()
}
pub async fn delete_friend_request(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<Vec<FriendRequest>, Error> {
use friend_requests::dsl;
let friend_request: Vec<FriendRequest> = load_or_empty(
dsl::friend_requests
.filter(dsl::sender.eq(user_uuid))
.filter(dsl::receiver.eq(self.uuid))
.load(conn)
.await
)?;
Ok()
}
*/
} }

View file

@ -8,7 +8,7 @@ use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn, Data,
error::Error, error::Error,
objects::{Me, Permissions, Role}, objects::{Permissions, Role},
schema::guild_members, schema::guild_members,
}; };
@ -26,14 +26,8 @@ pub struct MemberBuilder {
} }
impl MemberBuilder { impl MemberBuilder {
pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result<Member, Error> { pub async fn build(&self, data: &Data) -> Result<Member, Error> {
let user; let user = User::fetch_one(data, self.user_uuid).await?;
if let Some(me) = me {
user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?;
} else {
user = User::fetch_one(data, self.user_uuid).await?;
}
Ok(Member { Ok(Member {
uuid: self.uuid, uuid: self.uuid,
@ -100,12 +94,7 @@ impl Member {
Ok(member_builder) Ok(member_builder)
} }
pub async fn fetch_one( pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result<Self, Error> {
data: &Data,
me: &Me,
user_uuid: Uuid,
guild_uuid: Uuid,
) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let mut conn = data.pool.get().await?;
use guild_members::dsl; use guild_members::dsl;
@ -116,10 +105,10 @@ impl Member {
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await?;
member.build(data, Some(me)).await member.build(data).await
} }
pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result<Vec<Self>, Error> { pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result<Vec<Self>, Error> {
let mut conn = data.pool.get().await?; let mut conn = data.pool.get().await?;
use guild_members::dsl; use guild_members::dsl;
@ -133,7 +122,7 @@ impl Member {
let member_futures = member_builders let member_futures = member_builders
.iter() .iter()
.map(async move |m| m.build(data, Some(me)).await); .map(async move |m| m.build(data).await);
futures::future::try_join_all(member_futures).await futures::future::try_join_all(member_futures).await
} }
@ -156,6 +145,6 @@ impl Member {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
member.build(data, None).await member.build(data).await
} }
} }

View file

@ -9,7 +9,6 @@ use uuid::Uuid;
mod channel; mod channel;
mod email_token; mod email_token;
mod friends;
mod guild; mod guild;
mod invite; mod invite;
mod me; mod me;
@ -21,8 +20,6 @@ mod user;
pub use channel::Channel; pub use channel::Channel;
pub use email_token::EmailToken; pub use email_token::EmailToken;
pub use friends::Friend;
pub use friends::FriendRequest;
pub use guild::Guild; pub use guild::Guild;
pub use invite::Invite; pub use invite::Invite;
pub use me::Me; pub use me::Me;

View file

@ -1,40 +1,15 @@
use chrono::{DateTime, Utc};
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, Data, error::Error, objects::Me, schema::users}; use crate::{Conn, Data, error::Error, schema::users};
use super::load_or_empty; use super::load_or_empty;
#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] #[derive(Deserialize, Serialize, Clone, Queryable, Selectable)]
#[diesel(table_name = users)] #[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct UserBuilder {
uuid: Uuid,
username: String,
display_name: Option<String>,
avatar: Option<String>,
pronouns: Option<String>,
about: Option<String>,
}
impl UserBuilder {
fn build(self) -> User {
User {
uuid: self.uuid,
username: self.username,
display_name: self.display_name,
avatar: self.avatar,
pronouns: self.pronouns,
about: self.about,
friends_since: None,
}
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct User { pub struct User {
uuid: Uuid, uuid: Uuid,
username: String, username: String,
@ -42,7 +17,6 @@ pub struct User {
avatar: Option<String>, avatar: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
friends_since: Option<DateTime<Utc>>,
} }
impl User { impl User {
@ -54,53 +28,33 @@ impl User {
} }
use users::dsl; use users::dsl;
let user_builder: UserBuilder = dsl::users let user: User = dsl::users
.filter(dsl::uuid.eq(user_uuid)) .filter(dsl::uuid.eq(user_uuid))
.select(UserBuilder::as_select()) .select(User::as_select())
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await?;
let user = user_builder.build();
data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) data.set_cache_key(user_uuid.to_string(), user.clone(), 1800)
.await?; .await?;
Ok(user) Ok(user)
} }
pub async fn fetch_one_with_friendship(
data: &Data,
me: &Me,
user_uuid: Uuid,
) -> Result<Self, Error> {
let mut conn = data.pool.get().await?;
let mut user = Self::fetch_one(data, user_uuid).await?;
if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? {
user.friends_since = Some(friend.accepted_at);
}
Ok(user)
}
pub async fn fetch_amount( pub async fn fetch_amount(
conn: &mut Conn, conn: &mut Conn,
offset: i64, offset: i64,
amount: i64, amount: i64,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
use users::dsl; use users::dsl;
let user_builders: Vec<UserBuilder> = load_or_empty( let users: Vec<User> = load_or_empty(
dsl::users dsl::users
.limit(amount) .limit(amount)
.offset(offset) .offset(offset)
.select(UserBuilder::as_select()) .select(User::as_select())
.load(conn) .load(conn)
.await, .await,
)?; )?;
let users: Vec<User> = user_builders.iter().map(|u| u.clone().build()).collect();
Ok(users) Ok(users)
} }
} }

View file

@ -31,22 +31,6 @@ diesel::table! {
} }
} }
diesel::table! {
friend_requests (sender, receiver) {
sender -> Uuid,
receiver -> Uuid,
requested_at -> Timestamptz,
}
}
diesel::table! {
friends (uuid1, uuid2) {
uuid1 -> Uuid,
uuid2 -> Uuid,
accepted_at -> Timestamptz,
}
}
diesel::table! { diesel::table! {
guild_members (uuid) { guild_members (uuid) {
uuid -> Uuid, uuid -> Uuid,
@ -169,8 +153,6 @@ diesel::allow_tables_to_appear_in_same_query!(
access_tokens, access_tokens,
channel_permissions, channel_permissions,
channels, channels,
friend_requests,
friends,
guild_members, guild_members,
guilds, guilds,
instance_permissions, instance_permissions,