You've already forked AstralRinth
forked from didirus/AstralRinth
Fix clippy errors + lint, use turbo CI
This commit is contained in:
@@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{
|
||||
ids::{
|
||||
NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId,
|
||||
UserId, VersionId,
|
||||
NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId,
|
||||
ThreadMessageId, UserId, VersionId,
|
||||
},
|
||||
notifications::{Notification, NotificationAction, NotificationBody},
|
||||
projects::ProjectStatus,
|
||||
@@ -78,11 +78,21 @@ pub enum LegacyNotificationBody {
|
||||
impl LegacyNotification {
|
||||
pub fn from(notification: Notification) -> Self {
|
||||
let type_ = match ¬ification.body {
|
||||
NotificationBody::ProjectUpdate { .. } => Some("project_update".to_string()),
|
||||
NotificationBody::TeamInvite { .. } => Some("team_invite".to_string()),
|
||||
NotificationBody::OrganizationInvite { .. } => Some("organization_invite".to_string()),
|
||||
NotificationBody::StatusChange { .. } => Some("status_change".to_string()),
|
||||
NotificationBody::ModeratorMessage { .. } => Some("moderator_message".to_string()),
|
||||
NotificationBody::ProjectUpdate { .. } => {
|
||||
Some("project_update".to_string())
|
||||
}
|
||||
NotificationBody::TeamInvite { .. } => {
|
||||
Some("team_invite".to_string())
|
||||
}
|
||||
NotificationBody::OrganizationInvite { .. } => {
|
||||
Some("organization_invite".to_string())
|
||||
}
|
||||
NotificationBody::StatusChange { .. } => {
|
||||
Some("status_change".to_string())
|
||||
}
|
||||
NotificationBody::ModeratorMessage { .. } => {
|
||||
Some("moderator_message".to_string())
|
||||
}
|
||||
NotificationBody::LegacyMarkdown {
|
||||
notification_type, ..
|
||||
} => notification_type.clone(),
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::database::models::{version_item, DatabaseError};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::projects::{
|
||||
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus, Project,
|
||||
ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
|
||||
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus,
|
||||
Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::threads::ThreadId;
|
||||
use crate::routes::v2_reroute::{self, capitalize_first};
|
||||
@@ -87,12 +87,13 @@ impl LegacyProject {
|
||||
.cloned()
|
||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||
|
||||
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
let project_type =
|
||||
if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
|
||||
(project_type, og_project_type)
|
||||
}
|
||||
@@ -102,7 +103,10 @@ impl LegacyProject {
|
||||
// - This can be any version, because the fields are ones that used to be on the project itself.
|
||||
// - Its conceivable that certain V3 projects that have many different ones may not have the same fields on all of them.
|
||||
// It's safe to use a db version_item for this as the only info is side types, game versions, and loader fields (for loaders), which used to be public on project anyway.
|
||||
pub fn from(data: Project, versions_item: Option<version_item::QueryVersion>) -> Self {
|
||||
pub fn from(
|
||||
data: Project,
|
||||
versions_item: Option<version_item::QueryVersion>,
|
||||
) -> Self {
|
||||
let mut client_side = LegacySideType::Unknown;
|
||||
let mut server_side = LegacySideType::Unknown;
|
||||
|
||||
@@ -110,7 +114,8 @@ impl LegacyProject {
|
||||
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||
// If there are no project types, default to 'project'
|
||||
let project_types = data.project_types;
|
||||
let (mut project_type, og_project_type) = Self::get_project_type(&project_types);
|
||||
let (mut project_type, og_project_type) =
|
||||
Self::get_project_type(&project_types);
|
||||
|
||||
let mut loaders = data.loaders;
|
||||
|
||||
@@ -128,16 +133,22 @@ impl LegacyProject {
|
||||
let fields = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
|
||||
.map(|f| {
|
||||
(f.field_name.clone(), f.value.clone().serialize_internal())
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
(client_side, server_side) =
|
||||
v2_reroute::convert_side_types_v2(&fields, Some(&*og_project_type));
|
||||
(client_side, server_side) = v2_reroute::convert_side_types_v2(
|
||||
&fields,
|
||||
Some(&*og_project_type),
|
||||
);
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the loaders are whatever the corresponding loader fields are
|
||||
if loaders.contains(&"mrpack".to_string()) {
|
||||
project_type = "modpack".to_string();
|
||||
if let Some(mrpack_loaders) = data.fields.iter().find(|f| f.0 == "mrpack_loaders") {
|
||||
if let Some(mrpack_loaders) =
|
||||
data.fields.iter().find(|f| f.0 == "mrpack_loaders")
|
||||
{
|
||||
let values = mrpack_loaders
|
||||
.1
|
||||
.iter()
|
||||
@@ -227,7 +238,8 @@ impl LegacyProject {
|
||||
.iter()
|
||||
.filter_map(|p| p.versions.first().map(|i| (*i).into()))
|
||||
.collect();
|
||||
let example_versions = version_item::Version::get_many(&version_ids, exec, redis).await?;
|
||||
let example_versions =
|
||||
version_item::Version::get_many(&version_ids, exec, redis).await?;
|
||||
let mut legacy_projects = Vec::new();
|
||||
for project in data {
|
||||
let version_item = example_versions
|
||||
@@ -308,7 +320,9 @@ pub struct LegacyVersion {
|
||||
impl From<Version> for LegacyVersion {
|
||||
fn from(data: Version) -> Self {
|
||||
let mut game_versions = Vec::new();
|
||||
if let Some(value) = data.fields.get("game_versions").and_then(|v| v.as_array()) {
|
||||
if let Some(value) =
|
||||
data.fields.get("game_versions").and_then(|v| v.as_array())
|
||||
{
|
||||
for gv in value {
|
||||
if let Some(game_version) = gv.as_str() {
|
||||
game_versions.push(game_version.to_string());
|
||||
@@ -318,14 +332,17 @@ impl From<Version> for LegacyVersion {
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the v2 loaders are whatever the corresponding loader fields are
|
||||
let mut loaders = data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
||||
let mut loaders =
|
||||
data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
||||
if loaders.contains(&"mrpack".to_string()) {
|
||||
if let Some((_, mrpack_loaders)) = data
|
||||
.fields
|
||||
.into_iter()
|
||||
.find(|(key, _)| key == "mrpack_loaders")
|
||||
{
|
||||
if let Ok(mrpack_loaders) = serde_json::from_value(mrpack_loaders) {
|
||||
if let Ok(mrpack_loaders) =
|
||||
serde_json::from_value(mrpack_loaders)
|
||||
{
|
||||
loaders = mrpack_loaders;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,14 +92,16 @@ impl LegacyResultSearchProject {
|
||||
.cloned()
|
||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||
|
||||
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
let project_type =
|
||||
if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
|
||||
let project_loader_fields = result_search_project.project_loader_fields.clone();
|
||||
let project_loader_fields =
|
||||
result_search_project.project_loader_fields.clone();
|
||||
let get_one_bool_loader_field = |key: &str| {
|
||||
project_loader_fields
|
||||
.get(key)
|
||||
@@ -110,17 +112,20 @@ impl LegacyResultSearchProject {
|
||||
};
|
||||
|
||||
let singleplayer = get_one_bool_loader_field("singleplayer");
|
||||
let client_only = get_one_bool_loader_field("client_only").unwrap_or(false);
|
||||
let server_only = get_one_bool_loader_field("server_only").unwrap_or(false);
|
||||
let client_only =
|
||||
get_one_bool_loader_field("client_only").unwrap_or(false);
|
||||
let server_only =
|
||||
get_one_bool_loader_field("server_only").unwrap_or(false);
|
||||
let client_and_server = get_one_bool_loader_field("client_and_server");
|
||||
|
||||
let (client_side, server_side) = v2_reroute::convert_side_types_v2_bools(
|
||||
singleplayer,
|
||||
client_only,
|
||||
server_only,
|
||||
client_and_server,
|
||||
Some(&*og_project_type),
|
||||
);
|
||||
let (client_side, server_side) =
|
||||
v2_reroute::convert_side_types_v2_bools(
|
||||
singleplayer,
|
||||
client_only,
|
||||
server_only,
|
||||
client_and_server,
|
||||
Some(&*og_project_type),
|
||||
);
|
||||
let client_side = client_side.to_string();
|
||||
let server_side = server_side.to_string();
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::models::ids::{ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId};
|
||||
use crate::models::ids::{
|
||||
ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId,
|
||||
};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::users::{User, UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -57,8 +59,12 @@ pub enum LegacyThreadType {
|
||||
impl From<crate::models::v3::threads::ThreadType> for LegacyThreadType {
|
||||
fn from(t: crate::models::v3::threads::ThreadType) -> Self {
|
||||
match t {
|
||||
crate::models::v3::threads::ThreadType::Report => LegacyThreadType::Report,
|
||||
crate::models::v3::threads::ThreadType::Project => LegacyThreadType::Project,
|
||||
crate::models::v3::threads::ThreadType::Report => {
|
||||
LegacyThreadType::Report
|
||||
}
|
||||
crate::models::v3::threads::ThreadType::Project => {
|
||||
LegacyThreadType::Project
|
||||
}
|
||||
crate::models::v3::threads::ThreadType::DirectMessage => {
|
||||
LegacyThreadType::DirectMessage
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@ pub struct UserSubscription {
|
||||
impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
for UserSubscription
|
||||
{
|
||||
fn from(x: crate::database::models::user_subscription_item::UserSubscriptionItem) -> Self {
|
||||
fn from(
|
||||
x: crate::database::models::user_subscription_item::UserSubscriptionItem,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: x.id.into(),
|
||||
user_id: x.user_id.into(),
|
||||
|
||||
@@ -13,7 +13,9 @@ pub use super::teams::TeamId;
|
||||
pub use super::threads::ThreadId;
|
||||
pub use super::threads::ThreadMessageId;
|
||||
pub use super::users::UserId;
|
||||
pub use crate::models::billing::{ChargeId, ProductId, ProductPriceId, UserSubscriptionId};
|
||||
pub use crate::models::billing::{
|
||||
ChargeId, ProductId, ProductPriceId, UserSubscriptionId,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
@@ -41,7 +43,11 @@ pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
|
||||
random_base62_rng_range(rng, n, n)
|
||||
}
|
||||
|
||||
pub fn random_base62_rng_range<R: rand::RngCore>(rng: &mut R, n_min: usize, n_max: usize) -> u64 {
|
||||
pub fn random_base62_rng_range<R: rand::RngCore>(
|
||||
rng: &mut R,
|
||||
n_min: usize,
|
||||
n_max: usize,
|
||||
) -> u64 {
|
||||
use rand::Rng;
|
||||
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
@@ -155,7 +161,10 @@ pub mod base62_impl {
|
||||
impl<'de> Visitor<'de> for Base62Visitor {
|
||||
type Value = Base62Id;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
formatter.write_str("a base62 string id")
|
||||
}
|
||||
|
||||
@@ -211,7 +220,9 @@ pub mod base62_impl {
|
||||
}
|
||||
|
||||
// We don't want this panicking or wrapping on integer overflow
|
||||
if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) {
|
||||
if let Some(n) =
|
||||
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
|
||||
{
|
||||
num = n;
|
||||
} else {
|
||||
return Err(DecodingError::Overflow);
|
||||
|
||||
@@ -90,7 +90,9 @@ impl ImageContext {
|
||||
match self {
|
||||
ImageContext::Project { project_id } => project_id.map(|x| x.0),
|
||||
ImageContext::Version { version_id } => version_id.map(|x| x.0),
|
||||
ImageContext::ThreadMessage { thread_message_id } => thread_message_id.map(|x| x.0),
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
thread_message_id.map(|x| x.0)
|
||||
}
|
||||
ImageContext::Report { report_id } => report_id.map(|x| x.0),
|
||||
ImageContext::Unknown => None,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use super::ids::OrganizationId;
|
||||
use super::users::UserId;
|
||||
use crate::database::models::notification_item::Notification as DBNotification;
|
||||
use crate::database::models::notification_item::NotificationAction as DBNotificationAction;
|
||||
use crate::models::ids::{ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId};
|
||||
use crate::models::ids::{
|
||||
ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId,
|
||||
};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -93,7 +93,11 @@ impl From<DBOAuthClient> for OAuthClient {
|
||||
name: value.name,
|
||||
icon_url: value.icon_url,
|
||||
max_scopes: value.max_scopes,
|
||||
redirect_uris: value.redirect_uris.into_iter().map(|r| r.into()).collect(),
|
||||
redirect_uris: value
|
||||
.redirect_uris
|
||||
.into_iter()
|
||||
.map(|r| r.into())
|
||||
.collect(),
|
||||
created_by: value.created_by.into(),
|
||||
created: value.created,
|
||||
url: value.url,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{models::v2::projects::LegacySideType, util::env::parse_strings_from_var};
|
||||
use crate::{
|
||||
models::v2::projects::LegacySideType, util::env::parse_strings_from_var,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
@@ -29,7 +31,9 @@ pub struct PackFile {
|
||||
pub file_size: u32,
|
||||
}
|
||||
|
||||
fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationError> {
|
||||
fn validate_download_url(
|
||||
values: &[String],
|
||||
) -> Result<(), validator::ValidationError> {
|
||||
for value in values {
|
||||
let url = url::Url::parse(value)
|
||||
.ok()
|
||||
@@ -39,7 +43,8 @@ fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationE
|
||||
return Err(validator::ValidationError::new("invalid URL"));
|
||||
}
|
||||
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default();
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS")
|
||||
.unwrap_or_default();
|
||||
if !domains.contains(
|
||||
&url.domain()
|
||||
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?
|
||||
|
||||
@@ -131,7 +131,9 @@ impl Scopes {
|
||||
self.intersects(Self::restricted())
|
||||
}
|
||||
|
||||
pub fn parse_from_oauth_scopes(scopes: &str) -> Result<Scopes, bitflags::parser::ParseError> {
|
||||
pub fn parse_from_oauth_scopes(
|
||||
scopes: &str,
|
||||
) -> Result<Scopes, bitflags::parser::ParseError> {
|
||||
let scopes = scopes.replace(['+', ' '], "|").replace("%20", "|");
|
||||
bitflags::parser::from_str(&scopes)
|
||||
}
|
||||
@@ -187,7 +189,9 @@ mod test {
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_well_formed() {
|
||||
let raw = "USER_READ_EMAIL SESSION_READ ORGANIZATION_CREATE";
|
||||
let expected = Scopes::USER_READ_EMAIL | Scopes::SESSION_READ | Scopes::ORGANIZATION_CREATE;
|
||||
let expected = Scopes::USER_READ_EMAIL
|
||||
| Scopes::SESSION_READ
|
||||
| Scopes::ORGANIZATION_CREATE;
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
|
||||
|
||||
@@ -224,7 +228,8 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_url_encoded() {
|
||||
let raw = urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string();
|
||||
let raw =
|
||||
urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string();
|
||||
let expected = Scopes::PAT_WRITE | Scopes::COLLECTION_DELETE;
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(&raw).unwrap();
|
||||
|
||||
@@ -128,7 +128,9 @@ pub fn from_duplicate_version_fields(
|
||||
let mut fields: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
|
||||
for vf in version_fields {
|
||||
// We use a string directly, so we can remove duplicates
|
||||
let serialized = if let Some(inner_array) = vf.value.serialize_internal().as_array() {
|
||||
let serialized = if let Some(inner_array) =
|
||||
vf.value.serialize_internal().as_array()
|
||||
{
|
||||
inner_array.clone()
|
||||
} else {
|
||||
vec![vf.value.serialize_internal()]
|
||||
@@ -151,7 +153,8 @@ pub fn from_duplicate_version_fields(
|
||||
|
||||
impl From<QueryProject> for Project {
|
||||
fn from(data: QueryProject) -> Self {
|
||||
let fields = from_duplicate_version_fields(data.aggregate_version_fields);
|
||||
let fields =
|
||||
from_duplicate_version_fields(data.aggregate_version_fields);
|
||||
let m = data.inner;
|
||||
Self {
|
||||
id: m.id.into(),
|
||||
@@ -655,7 +658,9 @@ pub struct Version {
|
||||
pub fields: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn skip_nulls<'de, D>(deserializer: D) -> Result<HashMap<String, serde_json::Value>, D::Error>
|
||||
pub fn skip_nulls<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<HashMap<String, serde_json::Value>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
@@ -708,7 +713,9 @@ impl From<QueryVersion> for Version {
|
||||
version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
|
||||
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
|
||||
file_name: d.file_name,
|
||||
dependency_type: DependencyType::from_string(d.dependency_type.as_str()),
|
||||
dependency_type: DependencyType::from_string(
|
||||
d.dependency_type.as_str(),
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
loaders: data.loaders.into_iter().map(Loader).collect(),
|
||||
|
||||
@@ -118,7 +118,8 @@ impl OrganizationPermissions {
|
||||
}
|
||||
if role.is_mod() {
|
||||
return Some(
|
||||
OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT,
|
||||
OrganizationPermissions::EDIT_DETAILS
|
||||
| OrganizationPermissions::ADD_PROJECT,
|
||||
);
|
||||
}
|
||||
None
|
||||
|
||||
@@ -93,7 +93,11 @@ impl ThreadType {
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn from(data: crate::database::models::Thread, users: Vec<User>, user: &User) -> Self {
|
||||
pub fn from(
|
||||
data: crate::database::models::Thread,
|
||||
users: Vec<User>,
|
||||
user: &User,
|
||||
) -> Self {
|
||||
let thread_type = data.type_;
|
||||
|
||||
Thread {
|
||||
@@ -107,7 +111,8 @@ impl Thread {
|
||||
.filter(|x| {
|
||||
if let MessageBody::Text { private, .. } = x.body {
|
||||
!private || user.role.is_mod()
|
||||
} else if let MessageBody::Deleted { private, .. } = x.body {
|
||||
} else if let MessageBody::Deleted { private, .. } = x.body
|
||||
{
|
||||
!private || user.role.is_mod()
|
||||
} else {
|
||||
true
|
||||
@@ -121,7 +126,10 @@ impl Thread {
|
||||
}
|
||||
|
||||
impl ThreadMessage {
|
||||
pub fn from(data: crate::database::models::ThreadMessage, user: &User) -> Self {
|
||||
pub fn from(
|
||||
data: crate::database::models::ThreadMessage,
|
||||
user: &User,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
author_id: if data.hide_identity && !user.role.is_mod() {
|
||||
|
||||
Reference in New Issue
Block a user