Compare commits
2 commits
ac3e7e242b
...
e8a9857e19
Author | SHA1 | Date | |
---|---|---|---|
e8a9857e19 | |||
e8b8b49643 |
16 changed files with 484 additions and 38 deletions
4
migrations/2025-07-07-131320_add_friends/down.sql
Normal file
4
migrations/2025-07-07-131320_add_friends/down.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE friend_requests;
|
||||||
|
DROP FUNCTION check_friend_request;
|
||||||
|
DROP TABLE friends;
|
35
migrations/2025-07-07-131320_add_friends/up.sql
Normal file
35
migrations/2025-07-07-131320_add_friends/up.sql
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
-- 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();
|
|
@ -13,11 +13,11 @@ use crate::{
|
||||||
/// requires auth: kinda, needs refresh token set but no access token is technically required
|
/// requires auth: kinda, needs refresh token set but no access token is technically required
|
||||||
///
|
///
|
||||||
/// ### Responses
|
/// ### Responses
|
||||||
///
|
///
|
||||||
/// 200 Logged out
|
/// 200 Logged out
|
||||||
///
|
///
|
||||||
/// 404 Refresh token is invalid
|
/// 404 Refresh token is invalid
|
||||||
///
|
///
|
||||||
/// 401 Unauthorized (no refresh token found)
|
/// 401 Unauthorized (no refresh token found)
|
||||||
///
|
///
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
|
@ -38,7 +38,9 @@ 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().cookie(refresh_token_cookie).finish())
|
return Ok(HttpResponse::NotFound()
|
||||||
|
.cookie(refresh_token_cookie)
|
||||||
|
.finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish())
|
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish())
|
||||||
|
|
|
@ -26,13 +26,13 @@ struct Query {
|
||||||
///
|
///
|
||||||
/// ### Responses
|
/// ### Responses
|
||||||
/// 200 Success
|
/// 200 Success
|
||||||
///
|
///
|
||||||
/// 204 Already verified
|
/// 204 Already verified
|
||||||
///
|
///
|
||||||
/// 410 Token Expired
|
/// 410 Token Expired
|
||||||
///
|
///
|
||||||
/// 404 Not Found
|
/// 404 Not Found
|
||||||
///
|
///
|
||||||
/// 401 Unauthorized
|
/// 401 Unauthorized
|
||||||
///
|
///
|
||||||
#[get("/verify-email")]
|
#[get("/verify-email")]
|
||||||
|
@ -74,11 +74,11 @@ pub async fn get(
|
||||||
///
|
///
|
||||||
/// ### Responses
|
/// ### Responses
|
||||||
/// 200 Email sent
|
/// 200 Email sent
|
||||||
///
|
///
|
||||||
/// 204 Already verified
|
/// 204 Already verified
|
||||||
///
|
///
|
||||||
/// 429 Too Many Requests
|
/// 429 Too Many Requests
|
||||||
///
|
///
|
||||||
/// 401 Unauthorized
|
/// 401 Unauthorized
|
||||||
///
|
///
|
||||||
#[post("/verify-email")]
|
#[post("/verify-email")]
|
||||||
|
|
|
@ -83,7 +83,9 @@ 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.new_message(&data, uuid, message_body.message, message_body.reply_to).await?;
|
let message = channel
|
||||||
|
.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)?])
|
||||||
|
|
|
@ -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::Member,
|
objects::{Me, Member},
|
||||||
utils::{get_auth_header, global_checks},
|
utils::{get_auth_header, global_checks},
|
||||||
};
|
};
|
||||||
use ::uuid::Uuid;
|
use ::uuid::Uuid;
|
||||||
|
@ -28,7 +28,9 @@ pub async fn get(
|
||||||
|
|
||||||
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
|
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
|
||||||
|
|
||||||
let members = Member::fetch_all(&data, guild_uuid).await?;
|
let me = Me::get(&mut conn, uuid).await?;
|
||||||
|
|
||||||
|
let members = Member::fetch_all(&data, &me, guild_uuid).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(members))
|
Ok(HttpResponse::Ok().json(members))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
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())
|
||||||
|
}
|
33
src/api/v1/me/friends/uuid.rs
Normal file
33
src/api/v1/me/friends/uuid.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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())
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ 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 {
|
||||||
|
@ -17,6 +18,9 @@ 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("")]
|
||||||
|
|
|
@ -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::User,
|
objects::{Me, User},
|
||||||
utils::{get_auth_header, global_checks},
|
utils::{get_auth_header, global_checks},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +45,9 @@ pub async fn get(
|
||||||
|
|
||||||
global_checks(&data, uuid).await?;
|
global_checks(&data, uuid).await?;
|
||||||
|
|
||||||
let user = User::fetch_one(&data, user_uuid).await?;
|
let me = Me::get(&mut conn, uuid).await?;
|
||||||
|
|
||||||
|
let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(user))
|
Ok(HttpResponse::Ok().json(user))
|
||||||
}
|
}
|
||||||
|
|
24
src/objects/friends.rs
Normal file
24
src/objects/friends.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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>,
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
use actix_web::web::BytesMut;
|
use actix_web::web::BytesMut;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, update};
|
use diesel::{
|
||||||
|
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;
|
||||||
|
@ -9,7 +12,8 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
Conn, Data,
|
Conn, Data,
|
||||||
error::Error,
|
error::Error,
|
||||||
schema::{guild_members, guilds, users},
|
objects::{Friend, FriendRequest, User},
|
||||||
|
schema::{friend_requests, friends, guild_members, guilds, users},
|
||||||
utils::{EMAIL_REGEX, USERNAME_REGEX, image_check},
|
utils::{EMAIL_REGEX, USERNAME_REGEX, image_check},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +128,10 @@ 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) || new_username.len() < 3 || new_username.len() > 32 {
|
if !USERNAME_REGEX.is_match(&new_username)
|
||||||
|
|| new_username.len() < 3
|
||||||
|
|| new_username.len() > 32
|
||||||
|
{
|
||||||
return Err(Error::BadRequest("Invalid username".to_string()));
|
return Err(Error::BadRequest("Invalid username".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,13 +160,11 @@ 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;
|
let new_display_name_option = if new_display_name.is_empty() {
|
||||||
|
None
|
||||||
if new_display_name.is_empty() {
|
|
||||||
new_display_name_option = None;
|
|
||||||
} else {
|
} else {
|
||||||
new_display_name_option = Some(new_display_name)
|
Some(new_display_name)
|
||||||
}
|
};
|
||||||
|
|
||||||
use users::dsl;
|
use users::dsl;
|
||||||
update(users::table)
|
update(users::table)
|
||||||
|
@ -236,4 +241,179 @@ 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()
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
Conn, Data,
|
Conn, Data,
|
||||||
error::Error,
|
error::Error,
|
||||||
objects::{Permissions, Role},
|
objects::{Me, Permissions, Role},
|
||||||
schema::guild_members,
|
schema::guild_members,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,8 +26,14 @@ pub struct MemberBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemberBuilder {
|
impl MemberBuilder {
|
||||||
pub async fn build(&self, data: &Data) -> Result<Member, Error> {
|
pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result<Member, Error> {
|
||||||
let user = User::fetch_one(data, self.user_uuid).await?;
|
let user;
|
||||||
|
|
||||||
|
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,
|
||||||
|
@ -94,7 +100,12 @@ impl Member {
|
||||||
Ok(member_builder)
|
Ok(member_builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result<Self, Error> {
|
pub async fn fetch_one(
|
||||||
|
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;
|
||||||
|
@ -105,10 +116,10 @@ impl Member {
|
||||||
.get_result(&mut conn)
|
.get_result(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
member.build(data).await
|
member.build(data, Some(me)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result<Vec<Self>, Error> {
|
pub async fn fetch_all(data: &Data, me: &Me, 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;
|
||||||
|
@ -122,7 +133,7 @@ impl Member {
|
||||||
|
|
||||||
let member_futures = member_builders
|
let member_futures = member_builders
|
||||||
.iter()
|
.iter()
|
||||||
.map(async move |m| m.build(data).await);
|
.map(async move |m| m.build(data, Some(me)).await);
|
||||||
|
|
||||||
futures::future::try_join_all(member_futures).await
|
futures::future::try_join_all(member_futures).await
|
||||||
}
|
}
|
||||||
|
@ -145,6 +156,6 @@ impl Member {
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
member.build(data).await
|
member.build(data, None).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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;
|
||||||
|
@ -20,6 +21,8 @@ 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;
|
||||||
|
|
|
@ -1,15 +1,40 @@
|
||||||
|
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, schema::users};
|
use crate::{Conn, Data, error::Error, objects::Me, 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,
|
||||||
|
@ -17,6 +42,7 @@ 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 {
|
||||||
|
@ -28,33 +54,53 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
use users::dsl;
|
use users::dsl;
|
||||||
let user: User = dsl::users
|
let user_builder: UserBuilder = dsl::users
|
||||||
.filter(dsl::uuid.eq(user_uuid))
|
.filter(dsl::uuid.eq(user_uuid))
|
||||||
.select(User::as_select())
|
.select(UserBuilder::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 users: Vec<User> = load_or_empty(
|
let user_builders: Vec<UserBuilder> = load_or_empty(
|
||||||
dsl::users
|
dsl::users
|
||||||
.limit(amount)
|
.limit(amount)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.select(User::as_select())
|
.select(UserBuilder::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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,22 @@ 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,
|
||||||
|
@ -153,6 +169,8 @@ 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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue