mirror of
https://gitlab.com/comunic/comunicapiv3
synced 2024-12-25 21:18:50 +00:00
Can send push notification messages through Firebase
This commit is contained in:
parent
bd1f7ac63b
commit
42c7425aae
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
config.private.yaml
|
||||
config.private.yaml
|
||||
storage
|
||||
|
182
Cargo.lock
generated
182
Cargo.lock
generated
@ -121,7 +121,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_urlencoded 0.7.0",
|
||||
"sha-1",
|
||||
"slab",
|
||||
"time 0.2.25",
|
||||
@ -306,7 +306,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_urlencoded 0.7.0",
|
||||
"socket2",
|
||||
"time 0.2.25",
|
||||
"tinyvec",
|
||||
@ -441,6 +441,36 @@ dependencies = [
|
||||
"syn 1.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe174d1b67f7b2bafed829c09db039301eb5841f66e43be2cf60b326e7f8e2cc"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_urlencoded 0.6.1",
|
||||
"url",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "0.1.7"
|
||||
@ -474,7 +504,7 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_urlencoded 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -768,6 +798,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"encoding_rs",
|
||||
"futures",
|
||||
"gouth",
|
||||
"image",
|
||||
"kamadak-exif",
|
||||
"lazy_static",
|
||||
@ -1013,6 +1044,12 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
@ -1235,6 +1272,17 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gcemeta"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64b740806c16b381ca8d78cb3869fb47ce5b490db28c5f19bc0336a9b9aaca6e"
|
||||
dependencies = [
|
||||
"attohttpc 0.15.0",
|
||||
"lazy_static",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.3"
|
||||
@ -1298,6 +1346,20 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "gouth"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f60d0f7f508e9354fb3d9f734655e47ac525177e60dbdcb120ed482f58f385c"
|
||||
dependencies = [
|
||||
"attohttpc 0.16.3",
|
||||
"gcemeta",
|
||||
"jsonwebtoken",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.2.7"
|
||||
@ -1590,6 +1652,20 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "7.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32"
|
||||
dependencies = [
|
||||
"base64 0.12.3",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kamadak-exif"
|
||||
version = "0.5.3"
|
||||
@ -2712,7 +2788,7 @@ dependencies = [
|
||||
"pin-project-lite 0.2.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_urlencoded 0.7.0",
|
||||
"tokio 1.2.0",
|
||||
"tokio-native-tls",
|
||||
"url",
|
||||
@ -2732,6 +2808,21 @@ dependencies = [
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.10.3"
|
||||
@ -2758,6 +2849,19 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
|
||||
dependencies = [
|
||||
"base64 0.12.3",
|
||||
"log",
|
||||
"ring",
|
||||
"sct",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
@ -2786,6 +2890,16 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.0.0"
|
||||
@ -2855,6 +2969,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
"itoa",
|
||||
"serde",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.0"
|
||||
@ -2920,6 +3046,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"num-bigint 0.2.6",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.2.3"
|
||||
@ -2970,6 +3107,12 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "standback"
|
||||
version = "0.2.15"
|
||||
@ -3474,6 +3617,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.1"
|
||||
@ -3659,6 +3808,25 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739"
|
||||
dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webrtc-sdp"
|
||||
version = "0.3.8"
|
||||
@ -3681,6 +3849,12 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
|
@ -38,4 +38,5 @@ bcrypt = "0.9.0"
|
||||
mp3-metadata = "0.3.3"
|
||||
mp4 = "0.8.1"
|
||||
zip = "0.5.10"
|
||||
webpage = "1.2.0"
|
||||
webpage = "1.2.0"
|
||||
gouth = "0.2.0"
|
@ -222,4 +222,7 @@ pub mod conversations {
|
||||
/// Conversation logo size
|
||||
pub const MAX_CONV_LOGO_WIDTH: u32 = 200;
|
||||
pub const MAX_CONV_LOGO_HEIGHT: u32 = 200;
|
||||
}
|
||||
}
|
||||
|
||||
/// Url where Firebase push notifications can be sent
|
||||
pub const FIREBASE_PUSH_MESSAGE_URL: &str = "https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send";
|
@ -8,11 +8,13 @@ pub struct APIClient {
|
||||
pub domain: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
pub default_expiration_time: u64,
|
||||
pub firebase_project_name: Option<String>,
|
||||
pub firebase_service_account_file: Option<String>,
|
||||
}
|
||||
|
||||
impl APIClient {
|
||||
pub fn is_firebase_available(&self) -> bool {
|
||||
self.firebase_service_account_file.is_some()
|
||||
self.firebase_project_name.is_some() &&
|
||||
self.firebase_service_account_file.is_some()
|
||||
}
|
||||
}
|
@ -23,6 +23,13 @@ pub fn get_by_origin(name: &str) -> ResultBoxError<APIClient> {
|
||||
.query_row(db_to_client)
|
||||
}
|
||||
|
||||
/// Get information about a client, based on its ID
|
||||
pub fn get_by_id(id: u64) -> Res<APIClient> {
|
||||
QueryInfo::new(CLIENTS_TABLE)
|
||||
.cond_u64("id", id)
|
||||
.query_row(db_to_client)
|
||||
}
|
||||
|
||||
fn db_to_client(res: &database::RowResult) -> Res<APIClient> {
|
||||
Ok(APIClient {
|
||||
id: res.get_u64("id")?,
|
||||
@ -30,6 +37,7 @@ fn db_to_client(res: &database::RowResult) -> Res<APIClient> {
|
||||
domain: res.get_optional_str("domain")?,
|
||||
comment: res.get_optional_str("comment")?,
|
||||
default_expiration_time: res.get_u64("default_expiration_time")?,
|
||||
firebase_project_name: res.get_optional_str("firebase_project_name")?,
|
||||
firebase_service_account_file: res.get_optional_str("firebase_service_account_file")?,
|
||||
})
|
||||
}
|
145
src/helpers/firebase_notifications_helper.rs
Normal file
145
src/helpers/firebase_notifications_helper.rs
Normal file
@ -0,0 +1,145 @@
|
||||
//! # Firebase notifications helper
|
||||
//!
|
||||
//! Helper used to send notifications through the Firebase Cloud Messaging services
|
||||
//!
|
||||
//! @author Pierre Hubert
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::constants::FIREBASE_PUSH_MESSAGE_URL;
|
||||
use crate::data::api_client::APIClient;
|
||||
use crate::data::error::{ExecError, Res};
|
||||
use crate::data::push_notification::PushNotification;
|
||||
use crate::data::user_token::{PushNotificationToken, UserAccessToken};
|
||||
use crate::helpers::api_helper;
|
||||
|
||||
struct FirebaseClientIdentifier {
|
||||
project_name: String,
|
||||
authorization_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FirebaseNotification {
|
||||
title: String,
|
||||
body: String,
|
||||
image: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FirebaseAndroidNotification {
|
||||
tag: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FirebaseAndroid {
|
||||
notification: FirebaseAndroidNotification,
|
||||
ttl: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FirebaseMessage {
|
||||
token: String,
|
||||
notification: FirebaseNotification,
|
||||
android: FirebaseAndroid,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FirebaseNotificationRequest {
|
||||
validate_only: bool,
|
||||
message: FirebaseMessage,
|
||||
}
|
||||
|
||||
/// Get short-lived authorization token for a specific client
|
||||
fn get_credentials(client: &APIClient) -> Res<String> {
|
||||
let service_account_file = match &client.firebase_service_account_file {
|
||||
Some(file) => file,
|
||||
None => {
|
||||
return Err(ExecError::boxed_string(format!("No service account file for client {}!", client.name)));
|
||||
}
|
||||
};
|
||||
|
||||
let token = gouth::Builder::new()
|
||||
.scopes(&["https://www.googleapis.com/auth/firebase.messaging"])
|
||||
.json(service_account_file)
|
||||
.build()?
|
||||
.header_value()?
|
||||
.to_string();
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Send a single notification through Firebase service
|
||||
fn send_notification(n: &PushNotification, client_token: &str, access: &FirebaseClientIdentifier) -> Res {
|
||||
let notif = FirebaseNotificationRequest {
|
||||
validate_only: false,
|
||||
message: FirebaseMessage {
|
||||
token: client_token.to_string(),
|
||||
notification: FirebaseNotification {
|
||||
title: n.title.to_string(),
|
||||
body: n.body.to_string(),
|
||||
image: n.image.clone(),
|
||||
},
|
||||
android: FirebaseAndroid {
|
||||
notification: FirebaseAndroidNotification {
|
||||
tag: n.id.to_string(),
|
||||
},
|
||||
ttl: n.timeout.map(|t| format!("{}s", t)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Authorization", HeaderValue::from_str(&access.authorization_token)?);
|
||||
let client = Client::builder().default_headers(headers).build()?;
|
||||
|
||||
client
|
||||
.post(&FIREBASE_PUSH_MESSAGE_URL.replace("{PROJECT_ID}", &access.project_name))
|
||||
.json(¬if)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a notification
|
||||
pub fn push_notifications(n: &PushNotification, targets: Vec<UserAccessToken>) -> Res {
|
||||
let mut tokens_cache: HashMap<u64, FirebaseClientIdentifier> = HashMap::new();
|
||||
|
||||
for target in targets {
|
||||
// Get an access token if required
|
||||
if !tokens_cache.contains_key(&target.client_id) {
|
||||
let client = api_helper::get_by_id(target.client_id)?;
|
||||
tokens_cache.insert(target.client_id, FirebaseClientIdentifier {
|
||||
authorization_token: get_credentials(&client)?,
|
||||
project_name: client.firebase_project_name
|
||||
.ok_or(ExecError::boxed_new("Missing firebase project name!"))?,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let authorization_token = match tokens_cache.get(&target.client_id) {
|
||||
None => {
|
||||
return Err(ExecError::boxed_new("Should have a Firebase token now!!!"));
|
||||
}
|
||||
Some(token) => token
|
||||
};
|
||||
|
||||
let client_token = match &target.push_notifications_token {
|
||||
PushNotificationToken::FIREBASE(token) => token,
|
||||
_ => {
|
||||
return Err(ExecError::boxed_new("Invalid token!"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if let Err(e) = send_notification(n, client_token, authorization_token) {
|
||||
eprintln!("Failed to send a push notification to a device! Error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -19,4 +19,5 @@ pub mod requests_limit_helper;
|
||||
pub mod events_helper;
|
||||
pub mod calls_helper;
|
||||
pub mod push_notifications_helper;
|
||||
pub mod independent_push_notifications_service_helper;
|
||||
pub mod independent_push_notifications_service_helper;
|
||||
pub mod firebase_notifications_helper;
|
@ -9,7 +9,7 @@ use crate::data::error::Res;
|
||||
use crate::data::push_notification::PushNotification;
|
||||
use crate::data::user::UserID;
|
||||
use crate::data::user_token::{PushNotificationToken, UserAccessToken};
|
||||
use crate::helpers::{account_helper, conversations_helper, independent_push_notifications_service_helper, user_helper};
|
||||
use crate::helpers::{account_helper, conversations_helper, independent_push_notifications_service_helper, user_helper, firebase_notifications_helper};
|
||||
use crate::helpers::events_helper::Event;
|
||||
|
||||
/// Un-register for previous push notifications service
|
||||
@ -26,36 +26,47 @@ pub fn un_register_from_previous_service(client: &UserAccessToken) -> Res {
|
||||
}
|
||||
|
||||
/// Split tokens in categories : (Independent, Firebase)
|
||||
fn split_tokens(targets: Vec<UserAccessToken>) -> (Vec<String>) {
|
||||
fn split_tokens(targets: Vec<UserAccessToken>) -> (Vec<String>, Vec<UserAccessToken>) {
|
||||
let mut independent = vec![];
|
||||
let mut firebase = vec![];
|
||||
|
||||
for target in targets {
|
||||
match target.push_notifications_token {
|
||||
PushNotificationToken::INDEPENDENT(token) => {
|
||||
independent.push(token)
|
||||
}
|
||||
|
||||
PushNotificationToken::FIREBASE(_) => {
|
||||
firebase.push(target);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(independent)
|
||||
(independent, firebase)
|
||||
}
|
||||
|
||||
/// Push a notification to specific tokens
|
||||
fn push_notification(n: &PushNotification, targets: Vec<UserAccessToken>) -> Res {
|
||||
let independents = split_tokens(targets);
|
||||
let (independents, firebase) = split_tokens(targets);
|
||||
|
||||
// Push independent notifications
|
||||
if !independents.is_empty() {
|
||||
independent_push_notifications_service_helper::push_notifications(n, independents)?;
|
||||
}
|
||||
|
||||
// Push Firebase notifications
|
||||
if !firebase.is_empty() {
|
||||
firebase_notifications_helper::push_notifications(n, firebase)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel a notification for specific tokens (optional)
|
||||
fn cancel_notification(id: String, targets: Vec<UserAccessToken>) -> Res {
|
||||
let independents = split_tokens(targets);
|
||||
let (independents, _) = split_tokens(targets);
|
||||
|
||||
if !independents.is_empty() {
|
||||
independent_push_notifications_service_helper::cancel_notifications(id, independents)?;
|
||||
|
Loading…
Reference in New Issue
Block a user