mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 13:16:56 +00:00
feat(push): Add MSC4174 web push
This commit is contained in:
1 parent
dbcffe8af0
commit
ed1e634579
23 files changed
+1864
-281
No files matched your search
@@ -21,6 +21,7 @@ unstable-msc4203 = []
|
||||
|
||||
[dependencies]
|
||||
assign.workspace = true
|
||||
http.workspace = true
|
||||
ruma.workspace = true
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod draupnir_antispam;
|
||||
pub mod invite_permission_config;
|
||||
pub mod meowlnir_antispam;
|
||||
pub mod policy;
|
||||
pub mod pushers;
|
||||
@@ -0,0 +1,36 @@
|
||||
pub mod unstable {
|
||||
use ruma::{
|
||||
api::{auth_scheme::AccessToken, error::Error as MatrixError, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: POST,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
unstable => "/_matrix/client/unstable/org.matrix.msc4174/pushers/ack",
|
||||
}
|
||||
}
|
||||
|
||||
#[request(error = MatrixError)]
|
||||
pub struct Request {
|
||||
pub app_id: String,
|
||||
pub ack_token: String,
|
||||
}
|
||||
|
||||
#[response(error = MatrixError)]
|
||||
#[derive(Default)]
|
||||
#[allow(clippy::empty_structs_with_brackets, reason = "required by #[response]")]
|
||||
pub struct Response {}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new(app_id: String, ack_token: String) -> Self { Self { app_id, ack_token } }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new() -> Self { Self {} }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
pub mod v3 {
|
||||
use ruma::{
|
||||
api::{auth_scheme::AccessToken, error::Error as MatrixError, request, response},
|
||||
metadata,
|
||||
serde::from_raw_json_value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use crate::pushers::Pusher;
|
||||
|
||||
metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_matrix/client/r0/pushers",
|
||||
1.1 => "/_matrix/client/v3/pushers",
|
||||
}
|
||||
}
|
||||
|
||||
#[request(error = MatrixError)]
|
||||
#[derive(Default)]
|
||||
#[allow(clippy::empty_structs_with_brackets, reason = "required by #[request]")]
|
||||
pub struct Request {}
|
||||
|
||||
#[response(error = MatrixError)]
|
||||
pub struct Response {
|
||||
pub pushers: Vec<PusherWithActivation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PusherWithActivation {
|
||||
#[serde(flatten)]
|
||||
pub pusher: Pusher,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub activated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActivationDeHelper {
|
||||
activated: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PusherWithActivation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
|
||||
let ActivationDeHelper { activated } = from_raw_json_value(&json)?;
|
||||
let pusher = from_raw_json_value(&json)?;
|
||||
|
||||
Ok(Self { pusher, activated })
|
||||
}
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new() -> Self { Self {} }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(pushers: Vec<PusherWithActivation>) -> Self { Self { pushers } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
pub mod ack;
|
||||
pub mod get_pushers;
|
||||
mod pusher_serde;
|
||||
pub mod set_pusher;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub use ruma::api::client::push::{EmailPusherData, PusherIds};
|
||||
use ruma::{
|
||||
push::{HttpPusherData, PushFormat},
|
||||
serde::JsonObject,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Pusher {
|
||||
#[serde(flatten)]
|
||||
pub ids: PusherIds,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub kind: PusherKind,
|
||||
|
||||
pub app_display_name: String,
|
||||
|
||||
pub device_display_name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_tag: Option<String>,
|
||||
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PusherKind {
|
||||
Http(HttpPusherData),
|
||||
Email(EmailPusherData),
|
||||
WebPush(WebPushPusherData),
|
||||
|
||||
Custom {
|
||||
kind: String,
|
||||
data: JsonObject,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct WebPushPusherData {
|
||||
pub url: String,
|
||||
|
||||
pub auth: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<PushFormat>,
|
||||
|
||||
#[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
|
||||
pub data: JsonObject,
|
||||
}
|
||||
|
||||
impl fmt::Debug for WebPushPusherData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WebPushPusherData")
|
||||
.field("url", &self.url)
|
||||
.field("auth", &"<redacted>")
|
||||
.field("format", &self.format)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PusherKind {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
| Self::Http(_) => "http",
|
||||
| Self::Email(_) => "email",
|
||||
| Self::WebPush(_) => "org.matrix.msc4174.webpush",
|
||||
| Self::Custom { kind, .. } => kind,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_webpush(&self) -> Option<&WebPushPusherData> {
|
||||
match self {
|
||||
| Self::WebPush(data) => Some(data),
|
||||
| _ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
use ruma::serde::from_raw_json_value;
|
||||
use serde::{Deserialize, Serialize, de, ser::SerializeStruct};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use super::{Pusher, PusherIds, PusherKind};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PusherDeHelper {
|
||||
#[serde(flatten)]
|
||||
ids: PusherIds,
|
||||
app_display_name: String,
|
||||
device_display_name: String,
|
||||
profile_tag: Option<String>,
|
||||
lang: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Pusher {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
|
||||
let PusherDeHelper {
|
||||
ids,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
} = from_raw_json_value(&json)?;
|
||||
let kind = from_raw_json_value(&json)?;
|
||||
|
||||
Ok(Self {
|
||||
ids,
|
||||
kind,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PusherKind {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut st = serializer.serialize_struct("PusherKind", 2)?;
|
||||
st.serialize_field("kind", self.as_str())?;
|
||||
|
||||
match self {
|
||||
| Self::Http(data) => st.serialize_field("data", data)?,
|
||||
| Self::Email(data) => st.serialize_field("data", data)?,
|
||||
| Self::WebPush(data) => st.serialize_field("data", data)?,
|
||||
| Self::Custom { data, .. } => st.serialize_field("data", data)?,
|
||||
}
|
||||
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PusherKindDeHelper {
|
||||
kind: String,
|
||||
data: Box<RawJsonValue>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PusherKind {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
let PusherKindDeHelper { kind, data } = from_raw_json_value(&json)?;
|
||||
|
||||
match kind.as_str() {
|
||||
| "http" => from_raw_json_value(&data).map(Self::Http),
|
||||
| "email" => from_raw_json_value(&data).map(Self::Email),
|
||||
| "org.matrix.msc4174.webpush" => from_raw_json_value(&data).map(Self::WebPush),
|
||||
| _ => Ok(Self::Custom { kind, data: from_raw_json_value(&data)? }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||
|
||||
use super::{Pusher, PusherKind};
|
||||
|
||||
#[test]
|
||||
fn roundtrip_ruma_http_pusher() {
|
||||
let stored = json!({
|
||||
"pushkey": "abcdef",
|
||||
"app_id": "my.matrix.app",
|
||||
"kind": "http",
|
||||
"data": {
|
||||
"url": "https://push.example.org/_matrix/push/v1/notify",
|
||||
"format": "event_id_only",
|
||||
"default_payload": { "session_id": "abc" },
|
||||
},
|
||||
"app_display_name": "My Matrix App",
|
||||
"device_display_name": "My Phone",
|
||||
"profile_tag": "tag",
|
||||
"lang": "en",
|
||||
});
|
||||
|
||||
let pusher: Pusher = from_json_value(stored.clone()).unwrap();
|
||||
assert!(matches!(pusher.kind, PusherKind::Http(_)));
|
||||
assert_eq!(to_json_value(&pusher).unwrap(), stored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_legacy_append_field() {
|
||||
let stored = json!({
|
||||
"pushkey": "abcdef",
|
||||
"app_id": "my.matrix.app",
|
||||
"kind": "email",
|
||||
"data": {},
|
||||
"append": true,
|
||||
"app_display_name": "My Matrix App",
|
||||
"device_display_name": "My Phone",
|
||||
"lang": "en",
|
||||
});
|
||||
|
||||
let pusher: Pusher = from_json_value(stored).unwrap();
|
||||
assert!(matches!(pusher.kind, PusherKind::Email(_)));
|
||||
assert_eq!(pusher.profile_tag, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_webpush_pusher() {
|
||||
let stored = json!({
|
||||
"pushkey": "BLn9b-VR0ca83knDNZ32dCHGyjJp",
|
||||
"app_id": "m.webpush",
|
||||
"kind": "org.matrix.msc4174.webpush",
|
||||
"data": {
|
||||
"url": "https://updates.push.services.mozilla.com/wpush/v2/abc",
|
||||
"auth": "_ordMnz7uTCmrpBTeUV4Bw",
|
||||
"format": "event_id_only",
|
||||
"default_payload": { "session_id": "abc" },
|
||||
},
|
||||
"app_display_name": "WebPush",
|
||||
"device_display_name": "Firefox",
|
||||
"lang": "en",
|
||||
});
|
||||
|
||||
let pusher: Pusher = from_json_value(stored.clone()).unwrap();
|
||||
let data = pusher.kind.as_webpush().unwrap();
|
||||
assert_eq!(data.auth, "_ordMnz7uTCmrpBTeUV4Bw");
|
||||
assert_eq!(data.data.len(), 1);
|
||||
assert_eq!(to_json_value(&pusher).unwrap(), stored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_pusher_with_activation() {
|
||||
use crate::pushers::get_pushers::v3::PusherWithActivation;
|
||||
|
||||
let wire = json!({
|
||||
"pushkey": "BLn9b-VR0ca83knDNZ32dCHGyjJp",
|
||||
"app_id": "m.webpush",
|
||||
"kind": "org.matrix.msc4174.webpush",
|
||||
"data": {
|
||||
"url": "https://updates.push.services.mozilla.com/wpush/v2/abc",
|
||||
"auth": "_ordMnz7uTCmrpBTeUV4Bw",
|
||||
},
|
||||
"app_display_name": "WebPush",
|
||||
"device_display_name": "Firefox",
|
||||
"lang": "en",
|
||||
"activated": true,
|
||||
});
|
||||
|
||||
let pusher: PusherWithActivation = from_json_value(wire.clone()).unwrap();
|
||||
assert_eq!(pusher.activated, Some(true));
|
||||
assert_eq!(pusher.pusher.kind.as_str(), "org.matrix.msc4174.webpush");
|
||||
assert_eq!(to_json_value(&pusher).unwrap(), wire);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_unknown_kind() {
|
||||
let stored = json!({
|
||||
"pushkey": "abcdef",
|
||||
"app_id": "my.matrix.app",
|
||||
"kind": "my.custom.kind",
|
||||
"data": { "whatever": 1 },
|
||||
"app_display_name": "App",
|
||||
"device_display_name": "Device",
|
||||
"lang": "en",
|
||||
});
|
||||
|
||||
let pusher: Pusher = from_json_value(stored.clone()).unwrap();
|
||||
assert_eq!(pusher.kind.as_str(), "my.custom.kind");
|
||||
assert_eq!(to_json_value(&pusher).unwrap(), stored);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
pub mod v3 {
|
||||
use ruma::{
|
||||
JsOption,
|
||||
api::{
|
||||
EmptyBody, IncomingResponse, OutgoingResponse,
|
||||
auth_scheme::AccessToken,
|
||||
error::{DeserializationError, Error as MatrixError, IntoHttpError},
|
||||
request,
|
||||
},
|
||||
metadata,
|
||||
serde::from_raw_json_value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use crate::pushers::{Pusher, PusherIds};
|
||||
|
||||
metadata! {
|
||||
method: POST,
|
||||
rate_limited: true,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_matrix/client/r0/pushers/set",
|
||||
1.1 => "/_matrix/client/v3/pushers/set",
|
||||
}
|
||||
}
|
||||
|
||||
#[request(error = MatrixError)]
|
||||
pub struct Request {
|
||||
#[ruma_api(body)]
|
||||
pub action: PusherAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Response {
|
||||
pub needs_activation: bool,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new(action: PusherAction) -> Self { Self { action } }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(needs_activation: bool) -> Self { Self { needs_activation } }
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl OutgoingResponse for Response {
|
||||
type Body = EmptyBody<false>;
|
||||
|
||||
fn try_into_http_response_inner(
|
||||
self,
|
||||
) -> Result<http::Response<Self::Body>, IntoHttpError> {
|
||||
let status = if self.needs_activation {
|
||||
http::StatusCode::CREATED
|
||||
} else {
|
||||
http::StatusCode::OK
|
||||
};
|
||||
|
||||
http::Response::builder()
|
||||
.status(status)
|
||||
.header(http::header::CONTENT_TYPE, ruma::http_headers::APPLICATION_JSON)
|
||||
.body(EmptyBody)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
impl IncomingResponse for Response {
|
||||
type EndpointError = MatrixError;
|
||||
|
||||
fn try_from_http_response_inner(
|
||||
response: http::Response<&[u8]>,
|
||||
) -> Result<Self, DeserializationError> {
|
||||
Ok(Self {
|
||||
needs_activation: response.status() == http::StatusCode::CREATED,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PusherAction {
|
||||
Post(Box<Pusher>),
|
||||
Delete(PusherIds),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PusherActionDeHelper {
|
||||
kind: JsOption<String>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PusherAction {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
let PusherActionDeHelper { kind } = from_raw_json_value(&json)?;
|
||||
|
||||
match kind {
|
||||
| JsOption::Some(_) => Ok(Self::Post(from_raw_json_value(&json)?)),
|
||||
| JsOption::Null => Ok(Self::Delete(from_raw_json_value(&json)?)),
|
||||
| JsOption::Undefined => Err(de::Error::missing_field("kind")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PusherAction {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
| Self::Post(pusher) => pusher.serialize(serializer),
|
||||
| Self::Delete(ids) => {
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut st = serializer.serialize_struct("PusherAction", 3)?;
|
||||
st.serialize_field("pushkey", &ids.pushkey)?;
|
||||
st.serialize_field("app_id", &ids.app_id)?;
|
||||
st.serialize_field("kind", &None::<&str>)?;
|
||||
st.end()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user