Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/labrinth/migrations/20260722120000_blocked-users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE blocked_users (
user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
blocked_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY (user_id, blocked_id)
);
94 changes: 94 additions & 0 deletions apps/labrinth/src/database/models/blocked_user_item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::database::models::DBUserId;

pub struct DBBlockedUser {
pub user_id: DBUserId,
pub blocked_id: DBUserId,
}

impl DBBlockedUser {
pub async fn insert<'a, E>(&self, exec: E) -> Result<(), sqlx::Error>
where
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
{
sqlx::query!(
"
INSERT INTO blocked_users (user_id, blocked_id)
VALUES ($1, $2)
ON CONFLICT (user_id, blocked_id) DO NOTHING
",
self.user_id.0,
self.blocked_id.0,
)
.execute(exec)
.await?;

Ok(())
}

pub async fn is_blocked<'a, E>(
user_id: DBUserId,
blocked_id: DBUserId,
exec: E,
) -> Result<bool, sqlx::Error>
where
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
{
let blocked = sqlx::query_scalar!(
"
SELECT 1 FROM blocked_users
WHERE user_id = $1 AND blocked_id = $2
",
user_id.0,
blocked_id.0,
)
.fetch_optional(exec)
.await?;

Ok(blocked.is_some())
}

pub async fn get_blocked_for_user<'a, E>(
user_id: DBUserId,
exec: E,
) -> Result<Vec<DBUserId>, sqlx::Error>
where
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
{
let blocked = sqlx::query_scalar!(
"
SELECT blocked_id FROM blocked_users
WHERE user_id = $1
",
user_id.0,
)
.fetch_all(exec)
.await?
.into_iter()
.map(DBUserId)
.collect();

Ok(blocked)
}

pub async fn remove<'a, E>(
user_id: DBUserId,
blocked_id: DBUserId,
exec: E,
) -> Result<(), sqlx::Error>
where
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
{
sqlx::query!(
"
DELETE FROM blocked_users
WHERE user_id = $1 AND blocked_id = $2
",
user_id.0,
blocked_id.0,
)
.execute(exec)
.await?;

Ok(())
}
}
1 change: 1 addition & 0 deletions apps/labrinth/src/database/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use thiserror::Error;

pub mod affiliate_code_item;
pub mod analytics_event_item;
pub mod blocked_user_item;
pub mod categories;
pub mod charge_item;
pub mod collection_item;
Expand Down
41 changes: 41 additions & 0 deletions apps/labrinth/src/routes/internal/blocked_users.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::database::PgPool;
use crate::database::models::DBUserId;
use crate::database::models::blocked_user_item::DBBlockedUser;
use crate::routes::ApiError;
use crate::util::guards::admin_key_guard;
use actix_web::{get, web};
use ariadne::ids::base62_impl::parse_base62;
use serde::Serialize;

pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(block_status);
}

#[derive(Serialize, utoipa::ToSchema)]
pub struct BlockStatus {
pub blocked: bool,
}

/// Check whether one user has blocked another.
#[utoipa::path(tag = "blocked_users", responses((status = OK, body = BlockStatus)))]
#[get("/block/{user_id}/{target_id}", guard = "admin_key_guard")]
pub async fn block_status(
info: web::Path<(String, String)>,
pool: web::Data<PgPool>,
) -> Result<web::Json<BlockStatus>, ApiError> {
let (user_id, target_id) = info.into_inner();

let user_id =
DBUserId(parse_base62(&user_id).map_err(|_| {
ApiError::InvalidInput("invalid user_id".to_string())
})? as i64);
let target_id =
DBUserId(parse_base62(&target_id).map_err(|_| {
ApiError::InvalidInput("invalid target_id".to_string())
})? as i64);

let blocked =
DBBlockedUser::is_blocked(user_id, target_id, &**pool).await?;

Ok(web::Json(BlockStatus { blocked }))
}
3 changes: 3 additions & 0 deletions apps/labrinth/src/routes/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod admin;
pub mod affiliate;
pub mod attribution;
pub mod billing;
pub mod blocked_users;
pub mod campaign;
pub mod delphi;
pub mod external_notifications;
Expand Down Expand Up @@ -29,6 +30,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
web::scope("/_internal")
.wrap(default_cors())
.configure(admin::config)
.configure(blocked_users::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
Expand Down Expand Up @@ -65,6 +67,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
),
paths(
admin::count_download,
blocked_users::block_status,
admin::force_reindex,
admin::force_reindex_project,
session::list,
Expand Down
121 changes: 121 additions & 0 deletions apps/labrinth/src/routes/v3/blocked_users.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use crate::auth::get_user_from_headers;
use crate::database::PgPool;
use crate::database::models::DBUser;
use crate::database::models::blocked_user_item::DBBlockedUser;
use crate::database::models::friend_item::DBFriend;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::{HttpRequest, delete, get, post, web};
use ariadne::ids::UserId;
use eyre::eyre;
use xredis::RedisPool;

pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(block_user);
cfg.service(unblock_user);
cfg.service(get_blocked_users);
}

/// Block a user.
#[utoipa::path(tag = "blocked_users", responses((status = NO_CONTENT)))]
#[post("/block/{id}")]
pub async fn block_user(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_WRITE,
)
.await?
.1;

let user_id = info.into_inner().0;
let Some(blocked) = DBUser::get(&user_id, &**pool, &redis).await? else {
return Err(ApiError::NotFound);
};

if blocked.id == user.id.into() {
return Err(ApiError::Request(eyre!("you cannot block yourself")));
}

let mut transaction = pool.begin().await?;

DBFriend::remove(user.id.into(), blocked.id, &mut transaction).await?;

DBBlockedUser {
user_id: user.id.into(),
blocked_id: blocked.id,
}
.insert(&mut transaction)
.await?;

transaction.commit().await?;

Ok(())
}

/// Unblock a user.
#[utoipa::path(tag = "blocked_users", responses((status = NO_CONTENT)))]
#[delete("/block/{id}")]
pub async fn unblock_user(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_WRITE,
)
.await?
.1;

let user_id = info.into_inner().0;
let Some(blocked) = DBUser::get(&user_id, &**pool, &redis).await? else {
return Err(ApiError::NotFound);
};

DBBlockedUser::remove(user.id.into(), blocked.id, &**pool).await?;

Ok(())
}

/// List the users blocked by the current user.
#[utoipa::path(tag = "blocked_users", responses((status = OK, body = Vec<UserId>)))]
#[get("/blocks")]
pub async fn get_blocked_users(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<Vec<UserId>>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_READ,
)
.await?
.1;

let blocked = DBBlockedUser::get_blocked_for_user(user.id.into(), &**pool)
.await?
.into_iter()
.map(UserId::from)
.collect();

Ok(web::Json(blocked))
}
Loading
Loading