1
0
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:
Pierre HUBERT 2021-04-15 16:35:25 +02:00
parent bd1f7ac63b
commit 42c7425aae
9 changed files with 360 additions and 14 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
config.private.yaml
config.private.yaml
storage

182
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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";

View File

@ -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()
}
}

View File

@ -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")?,
})
}

View 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(&notif)
.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(())
}

View File

@ -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;

View File

@ -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)?;