Basic Web Push support.

This commit is contained in:
Paul
2021-02-18 16:21:34 +00:00
parent a2f14d2d37
commit 52e070c21c
15 changed files with 277 additions and 52 deletions

66
Cargo.lock generated
View File

@@ -243,6 +243,16 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2"
dependencies = [
"byteorder",
"safemem",
]
[[package]]
name = "base64"
version = "0.11.0"
@@ -686,6 +696,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0465971a8cc1fa2455c8465aaa377131e1f1cf4983280f474a13e68793aa770c"
dependencies = [
"serde",
]
[[package]]
name = "err-derive"
version = "0.2.4"
@@ -2454,10 +2473,11 @@ dependencies = [
[[package]]
name = "revolt"
version = "0.3.3-alpha.5"
version = "0.3.3-alpha.6"
dependencies = [
"async-std",
"async-tungstenite",
"base64 0.13.0",
"bitfield",
"chrono",
"ctrlc",
@@ -2489,6 +2509,7 @@ dependencies = [
"ulid",
"urlencoding",
"validator",
"web-push",
]
[[package]]
@@ -2501,7 +2522,7 @@ dependencies = [
"libc",
"once_cell",
"spin",
"untrusted",
"untrusted 0.7.1",
"web-sys",
"winapi 0.3.9",
]
@@ -2673,6 +2694,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "safemem"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
[[package]]
name = "schannel"
version = "0.1.19"
@@ -2705,7 +2732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
"untrusted 0.7.1",
]
[[package]]
@@ -3477,6 +3504,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
[[package]]
name = "untrusted"
version = "0.7.1"
@@ -3678,6 +3711,31 @@ version = "0.2.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
[[package]]
name = "web-push"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8353dd6c7cfb9a02737fd6dc9a66a80dca2a93fb690f6ad264d2a7672e6f1c0"
dependencies = [
"base64 0.7.0",
"chrono",
"erased-serde",
"futures",
"http",
"hyper",
"hyper-tls",
"lazy_static",
"log",
"native-tls",
"openssl",
"ring",
"serde",
"serde_derive",
"serde_json",
"time 0.1.44",
"untrusted 0.6.2",
]
[[package]]
name = "web-sys"
version = "0.3.47"
@@ -3695,7 +3753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
"untrusted 0.7.1",
]
[[package]]

View File

@@ -1,14 +1,16 @@
[package]
name = "revolt"
version = "0.3.3-alpha.5"
version = "0.3.3-alpha.6"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.13.0"
futures = "0.3.8"
impl_ops = "0.1.1"
web-push = "0.7.2"
many-to-many = "0.1.2"
ctrlc = { version = "3.0", features = ["termination"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }

View File

@@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
@@ -17,5 +17,5 @@ pub struct File {
metadata: Metadata,
content_type: String,
message_id: Option<String>
message_id: Option<String>,
}

View File

@@ -46,9 +46,9 @@ pub enum Channel {
impl Channel {
pub fn id(&self) -> &str {
match self {
Channel::SavedMessages { id, .. } => id,
Channel::DirectMessage { id, .. } => id,
Channel::Group { id, .. } => id,
Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } => id,
}
}

View File

@@ -1,12 +1,19 @@
use crate::util::variables::VAPID_PRIVATE_KEY;
use crate::{
database::*,
notifications::events::ClientboundNotification,
notifications::{events::ClientboundNotification, websocket::is_online},
util::result::{Error, Result},
};
use mongodb::bson::{doc, to_bson, DateTime};
use futures::StreamExt;
use mongodb::{
bson::{doc, to_bson, DateTime},
options::FindOptions,
};
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use web_push::{ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
@@ -49,7 +56,7 @@ impl Message {
// ! FIXME: temp code
let channels = get_collection("channels");
match channel {
match &channel {
Channel::DirectMessage { id, .. } => {
channels
.update_one(
@@ -96,11 +103,84 @@ impl Message {
_ => {}
}
let enc = serde_json::to_string(&self).unwrap();
ClientboundNotification::Message(self)
.publish(channel.id().to_string())
.await
.ok();
/*
Web Push Test Code
! FIXME: temp code
*/
// Find all offline users.
let mut target_ids = vec![];
match &channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
// if !is_online(recipient) {
target_ids.push(recipient.clone());
// }
}
}
_ => {}
}
// Fetch their corresponding sessions.
let mut cursor = get_collection("accounts")
.find(
doc! {
"_id": {
"$in": target_ids
},
"sessions.subscription": {
"$exists": true
}
},
FindOptions::builder()
.projection(doc! { "sessions": 1 })
.build(),
)
.await
.unwrap(); // !FIXME
let mut subscriptions = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(sessions) = doc.get_array("sessions") {
for session in sessions {
if let Some(doc) = session.as_document() {
if let Ok(sub) = doc.get_document("subscription") {
let endpoint = sub.get_str("endpoint").unwrap().to_string();
let p256dh = sub.get_str("p256dh").unwrap().to_string();
let auth = sub.get_str("auth").unwrap().to_string();
subscriptions.push(SubscriptionInfo::new(endpoint, p256dh, auth));
}
}
}
}
}
}
if subscriptions.len() > 0 {
let client = WebPushClient::new();
let key = base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap();
for subscription in subscriptions {
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
let sig_builder =
VapidSignatureBuilder::from_pem(std::io::Cursor::new(&key), &subscription).unwrap();
let signature = sig_builder.build().unwrap();
builder.set_vapid_signature(signature);
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
let m = builder.build().unwrap();
let response = client.send(m).await.unwrap();
dbg!(response);
}
}
Ok(())
}

View File

@@ -1,11 +1,11 @@
mod autumn;
mod channel;
mod guild;
mod message;
mod user;
mod autumn;
pub use autumn::*;
pub use channel::*;
pub use guild::*;
pub use message::*;
pub use user::*;
pub use autumn::*;

View File

@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Options {
ids: Vec<String>
ids: Vec<String>,
}
#[post("/<target>/messages/stale", data = "<data>")]
@@ -35,7 +35,7 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal
},
"channel": target.id()
},
None
None,
)
.await
.map_err(|_| Error::DatabaseError {
@@ -47,12 +47,11 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal
let mut found_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let msg = from_document::<Message>(doc)
.map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?;
let msg = from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?;
found_ids.push(msg.id.clone());
if msg.edited.is_some() {
updated.push(msg);

View File

@@ -1,7 +1,10 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::{bson::{doc, from_document}, options::FindOneOptions};
use mongodb::{
bson::{doc, from_document},
options::FindOneOptions,
};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
@@ -41,7 +44,7 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
},
FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build()
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
@@ -64,33 +67,40 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
"$exists": false
}
},
None
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})? {
let attachment = from_document::<File>(doc)
.map_err(|_| Error::DatabaseError { operation: "from_document", with: "attachment" })?;
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"message_id": &id
}
},
None
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "attachment" })?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"message_id": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Some(attachment)
} else {
return Err(Error::UnknownAttachment)
return Err(Error::UnknownAttachment);
}
} else {
None

View File

@@ -5,6 +5,7 @@ use rocket::Rocket;
mod channels;
mod guild;
mod onboard;
mod push;
mod root;
mod users;
@@ -15,4 +16,5 @@ pub fn mount(rocket: Rocket) -> Rocket {
.mount("/users", users::routes())
.mount("/channels", channels::routes())
.mount("/guild", guild::routes())
.mount("/push", push::routes())
}

8
src/routes/push/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
use rocket::Route;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
}

View File

@@ -0,0 +1,36 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rauth::auth::Session;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Subscription {
endpoint: String,
p256dh: String,
auth: String,
}
#[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner();
let col = get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data).unwrap()
}
},
None,
)
.await
.unwrap();
Ok(())
}

View File

@@ -0,0 +1,26 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::Session;
#[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> {
let col = get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$unset": {
"sessions.$.subscription": 1
}
},
None,
)
.await
.unwrap();
Ok(())
}

View File

@@ -1,5 +1,6 @@
use crate::util::variables::{
DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, USE_EMAIL, USE_HCAPTCHA, USE_AUTUMN, AUTUMN_URL
AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, USE_AUTUMN,
USE_EMAIL, USE_HCAPTCHA, VAPID_PUBLIC_KEY,
};
use mongodb::bson::doc;
@@ -8,7 +9,7 @@ use rocket_contrib::json::JsonValue;
#[get("/")]
pub async fn root() -> JsonValue {
json!({
"revolt": "0.3.3-alpha.5",
"revolt": "0.3.3-alpha.6",
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
@@ -23,5 +24,6 @@ pub async fn root() -> JsonValue {
}
},
"ws": *EXTERNAL_WS_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}

View File

@@ -2,8 +2,8 @@ use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, Document};
use mongodb::options::FindOptions;
use mongodb::bson::{Document, doc};
use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")]
@@ -19,9 +19,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
{ "recipients": &target.id }
]
},
FindOptions::builder()
.projection(doc! { "_id": 1 })
.build()
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
@@ -35,7 +33,5 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
Ok(json!({
"channels": channels
}))
Ok(json!({ "channels": channels }))
}

View File

@@ -21,6 +21,10 @@ lazy_static! {
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
pub static ref HCAPTCHA_SITEKEY: String =
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
pub static ref VAPID_PRIVATE_KEY: String =
env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.");
pub static ref VAPID_PUBLIC_KEY: String =
env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.");
// Application Flags
pub static ref DISABLE_REGISTRATION: bool = env::var("REVOLT_DISABLE_REGISTRATION").map_or(false, |v| v == "1");
@@ -48,6 +52,8 @@ lazy_static! {
// Application Logic Settings
pub static ref MAX_GROUP_SIZE: usize =
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
pub static ref PUSH_LIMIT: usize =
env::var("REVOLT_PUSH_LIMIT").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
}
pub fn preflight_checks() {