Rework push rules
This commit is contained in:
parent
e31cc955cc
commit
fab9709be2
26 changed files with 205 additions and 5793 deletions
|
@ -26,7 +26,6 @@ ruma-html = { version = "0.1.0", path = "crates/ruma-html" }
|
|||
ruma-identifiers-validation = { version = "0.9.3", path = "crates/ruma-identifiers-validation" }
|
||||
ruma-identity-service-api = { version = "0.8.0", path = "crates/ruma-identity-service-api" }
|
||||
ruma-macros = { version = "=0.12.0", path = "crates/ruma-macros" }
|
||||
ruma-push-gateway-api = { version = "0.8.0", path = "crates/ruma-push-gateway-api" }
|
||||
ruma-signatures = { version = "0.14.0", path = "crates/ruma-signatures" }
|
||||
ruma-server-util = { version = "0.2.0", path = "crates/ruma-server-util" }
|
||||
ruma-state-res = { version = "0.10.0", path = "crates/ruma-state-res" }
|
||||
|
|
|
@ -25,11 +25,20 @@ pub struct Device {
|
|||
/// Unix timestamp that the session was last active.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_seen_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
|
||||
/// Whether to enable notifications for this device
|
||||
pub enable_push: bool,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Creates a new `Device` with the given device ID.
|
||||
pub fn new(device_id: OwnedDeviceId) -> Self {
|
||||
Self { device_id, display_name: None, last_seen_ip: None, last_seen_ts: None }
|
||||
Self {
|
||||
device_id,
|
||||
display_name: None,
|
||||
last_seen_ip: None,
|
||||
last_seen_ts: None,
|
||||
enable_push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,10 @@ pub mod v3 {
|
|||
/// If this is `None`, the display name won't be changed.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// Whether to enable notifications for this device
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_push: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response type for the `update_device` endpoint.
|
||||
|
@ -44,7 +48,7 @@ pub mod v3 {
|
|||
impl Request {
|
||||
/// Creates a new `Request` with the given device ID.
|
||||
pub fn new(device_id: OwnedDeviceId) -> Self {
|
||||
Self { device_id, display_name: None }
|
||||
Self { device_id, display_name: None, enable_push: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,197 +1,14 @@
|
|||
//! Endpoints for push notifications.
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub use ruma_common::push::RuleKind;
|
||||
use ruma_common::{
|
||||
push::{
|
||||
Action, AnyPushRule, AnyPushRuleRef, ConditionalPushRule, ConditionalPushRuleInit,
|
||||
HttpPusherData, PatternedPushRule, PatternedPushRuleInit, PushCondition, SimplePushRule,
|
||||
SimplePushRuleInit,
|
||||
},
|
||||
serde::{JsonObject, StringEnum},
|
||||
};
|
||||
use ruma_common::{push::HttpPusherData, serde::JsonObject};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::PrivOwnedStr;
|
||||
|
||||
// TODO: remove almost all of these
|
||||
pub mod delete_pushrule;
|
||||
pub mod get_notifications;
|
||||
pub mod get_pushers;
|
||||
pub mod get_pushrule;
|
||||
pub mod get_pushrule_actions;
|
||||
pub mod get_pushrule_enabled;
|
||||
pub mod get_pushrules_all;
|
||||
pub mod get_pushrules_global_scope;
|
||||
mod pusher_serde;
|
||||
pub mod set_pusher;
|
||||
pub mod set_pushrule;
|
||||
pub mod set_pushrule_actions;
|
||||
pub mod set_pushrule_enabled;
|
||||
pub mod get_inbox;
|
||||
|
||||
/// Like `SimplePushRule`, but may represent any kind of push rule thanks to `pattern` and
|
||||
/// `conditions` being optional.
|
||||
///
|
||||
/// To create an instance of this type, use one of its `From` implementations.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct PushRule {
|
||||
/// The actions to perform when this rule is matched.
|
||||
pub actions: Vec<Action>,
|
||||
|
||||
/// Whether this is a default rule, or has been set explicitly.
|
||||
pub default: bool,
|
||||
|
||||
/// Whether the push rule is enabled or not.
|
||||
pub enabled: bool,
|
||||
|
||||
/// The ID of this rule.
|
||||
pub rule_id: String,
|
||||
|
||||
/// The conditions that must hold true for an event in order for a rule to be applied to an
|
||||
/// event.
|
||||
///
|
||||
/// A rule with no conditions always matches. Only applicable to underride and override rules.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conditions: Option<Vec<PushCondition>>,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Only applicable to content rules.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> From<SimplePushRule<T>> for PushRule
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(push_rule: SimplePushRule<T>) -> Self {
|
||||
let SimplePushRule { actions, default, enabled, rule_id, .. } = push_rule;
|
||||
let rule_id = rule_id.into();
|
||||
Self { actions, default, enabled, rule_id, conditions: None, pattern: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternedPushRule> for PushRule {
|
||||
fn from(push_rule: PatternedPushRule) -> Self {
|
||||
let PatternedPushRule { actions, default, enabled, rule_id, pattern, .. } = push_rule;
|
||||
Self { actions, default, enabled, rule_id, conditions: None, pattern: Some(pattern) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConditionalPushRule> for PushRule {
|
||||
fn from(push_rule: ConditionalPushRule) -> Self {
|
||||
let ConditionalPushRule { actions, default, enabled, rule_id, conditions, .. } = push_rule;
|
||||
Self { actions, default, enabled, rule_id, conditions: Some(conditions), pattern: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<SimplePushRuleInit<T>> for PushRule
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(init: SimplePushRuleInit<T>) -> Self {
|
||||
let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
|
||||
let rule_id = rule_id.into();
|
||||
Self { actions, default, enabled, rule_id, pattern: None, conditions: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConditionalPushRuleInit> for PushRule {
|
||||
fn from(init: ConditionalPushRuleInit) -> Self {
|
||||
let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
|
||||
Self { actions, default, enabled, rule_id, pattern: None, conditions: Some(conditions) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternedPushRuleInit> for PushRule {
|
||||
fn from(init: PatternedPushRuleInit) -> Self {
|
||||
let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
|
||||
Self { actions, default, enabled, rule_id, pattern: Some(pattern), conditions: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyPushRule> for PushRule {
|
||||
fn from(push_rule: AnyPushRule) -> Self {
|
||||
// The catch-all is unreachable if the "unstable-exhaustive-types" feature is enabled.
|
||||
#[allow(unreachable_patterns)]
|
||||
match push_rule {
|
||||
AnyPushRule::Override(r) => r.into(),
|
||||
AnyPushRule::Content(r) => r.into(),
|
||||
AnyPushRule::Room(r) => r.into(),
|
||||
AnyPushRule::Sender(r) => r.into(),
|
||||
AnyPushRule::Underride(r) => r.into(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AnyPushRuleRef<'a>> for PushRule {
|
||||
fn from(push_rule: AnyPushRuleRef<'a>) -> Self {
|
||||
push_rule.to_owned().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryFrom<PushRule> for SimplePushRule<T>
|
||||
where
|
||||
T: TryFrom<String>,
|
||||
{
|
||||
type Error = <T as TryFrom<String>>::Error;
|
||||
|
||||
fn try_from(push_rule: PushRule) -> Result<Self, Self::Error> {
|
||||
let PushRule { actions, default, enabled, rule_id, .. } = push_rule;
|
||||
let rule_id = T::try_from(rule_id)?;
|
||||
Ok(SimplePushRuleInit { actions, default, enabled, rule_id }.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that happens when `PushRule` cannot
|
||||
/// be converted into `PatternedPushRule`
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct MissingPatternError;
|
||||
|
||||
impl fmt::Display for MissingPatternError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Push rule does not have a pattern.")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MissingPatternError {}
|
||||
|
||||
impl TryFrom<PushRule> for PatternedPushRule {
|
||||
type Error = MissingPatternError;
|
||||
|
||||
fn try_from(push_rule: PushRule) -> Result<Self, Self::Error> {
|
||||
if let PushRule { actions, default, enabled, rule_id, pattern: Some(pattern), .. } =
|
||||
push_rule
|
||||
{
|
||||
Ok(PatternedPushRuleInit { actions, default, enabled, rule_id, pattern }.into())
|
||||
} else {
|
||||
Err(MissingPatternError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushRule> for ConditionalPushRule {
|
||||
fn from(push_rule: PushRule) -> Self {
|
||||
let PushRule { actions, default, enabled, rule_id, conditions, .. } = push_rule;
|
||||
|
||||
ConditionalPushRuleInit {
|
||||
actions,
|
||||
default,
|
||||
enabled,
|
||||
rule_id,
|
||||
conditions: conditions.unwrap_or_default(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
pub mod get_pushers;
|
||||
pub mod set_ack;
|
||||
pub mod set_pusher;
|
||||
|
||||
/// Which kind a pusher is, and the information for that kind.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum PusherKind {
|
||||
/// A pusher that sends HTTP pokes.
|
||||
|
@ -208,7 +25,7 @@ pub enum PusherKind {
|
|||
///
|
||||
/// To create an instance of this type, first create a `PusherInit` and convert it via
|
||||
/// `Pusher::from` / `.into()`.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Pusher {
|
||||
/// Identifiers for this pusher.
|
||||
|
@ -290,7 +107,7 @@ impl PusherIds {
|
|||
}
|
||||
|
||||
/// Information for an email pusher.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct EmailPusherData;
|
||||
|
||||
|
@ -302,22 +119,9 @@ impl EmailPusherData {
|
|||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct CustomPusherData {
|
||||
kind: String,
|
||||
data: JsonObject,
|
||||
}
|
||||
|
||||
/// The scope of a push rule.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, PartialEq, Eq, StringEnum)]
|
||||
#[ruma_enum(rename_all = "lowercase")]
|
||||
#[non_exhaustive]
|
||||
pub enum RuleScope {
|
||||
/// The global rules.
|
||||
Global,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! `POST /_matrix/client/*/inbox/query`
|
||||
//! `POST /_matrix/client/*/inbox`
|
||||
//!
|
||||
//! Paginate through the list of events that the user has been, or would have been notified about.
|
||||
|
||||
|
@ -11,12 +11,14 @@ pub mod v3 {
|
|||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
serde::Raw,
|
||||
serde::{Raw, StringEnum},
|
||||
OwnedRoomId,
|
||||
};
|
||||
use ruma_events::AnyTimelineEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::PrivOwnedStr;
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
|
@ -60,7 +62,7 @@ pub mod v3 {
|
|||
pub next_batch: Option<String>,
|
||||
|
||||
/// The list of threads mention events are in
|
||||
pub threads: Vec<Notification>,
|
||||
pub threads: Vec<Raw<AnyTimelineEvent>>,
|
||||
|
||||
/// The list of thread and notification events
|
||||
pub chunk: Vec<Notification>,
|
||||
|
@ -81,10 +83,10 @@ pub mod v3 {
|
|||
}
|
||||
|
||||
/// An inbox filter.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[derive(Clone, StringEnum)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[ruma_enum(rename_all = "snake_case")]
|
||||
pub enum InboxFilter {
|
||||
#[default]
|
||||
/// The default filter: MentionsUser | MentionsBulk | ThreadsParticipating | ThreadsInteresting
|
||||
Default,
|
||||
|
||||
|
@ -102,6 +104,9 @@ pub mod v3 {
|
|||
|
||||
/// Include read threads.
|
||||
IncludeRead,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// Represents a notification.
|
||||
|
|
66
crates/ruma-client-api/src/push/set_ack.rs
Normal file
66
crates/ruma-client-api/src/push/set_ack.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
//! `POST /_matrix/client/*/ack`
|
||||
//!
|
||||
//! Acknoledge/mark some events as read/unread
|
||||
|
||||
pub mod v3 {
|
||||
//! `/v3/` ([spec])
|
||||
//!
|
||||
//! [spec]: TODO: write and link spec
|
||||
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
OwnedRoomId, OwnedEventId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_matrix/client/v1/ack",
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `get_notifications` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Default)]
|
||||
pub struct Request {
|
||||
/// The events being acknowledged
|
||||
pub acks: Vec<Ack>,
|
||||
}
|
||||
|
||||
/// Response type for the `get_inbox` endpoint.
|
||||
#[response(error = crate::Error)]
|
||||
pub struct Response {}
|
||||
|
||||
impl Request {
|
||||
/// Creates an empty `Request`.
|
||||
pub fn new(acks: Vec<Ack>) -> Self {
|
||||
Request { acks }
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given notifications.
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a notification.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Ack {
|
||||
/// The room id this thread is in
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The root event id of the thread to mark as read. Set as empty for the "main" thread.
|
||||
pub thread_id: Option<OwnedEventId>,
|
||||
|
||||
/// The last read event id. Set as empty to use the last event.
|
||||
pub event_id: Option<OwnedEventId>,
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
//! Get all new events from all rooms since the last sync or a given point in time.
|
||||
|
||||
use js_int::UInt;
|
||||
use ruma_common::OwnedUserId;
|
||||
use ruma_common::{OwnedUserId, OwnedEventId};
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
|
||||
pub mod v3;
|
||||
|
@ -15,13 +15,27 @@ pub mod v4;
|
|||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
/// The number of unread notifications with the highlight flag set.
|
||||
/// The last acked/read event or message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub highlight_count: Option<UInt>,
|
||||
pub last_ack: Option<OwnedEventId>,
|
||||
|
||||
/// The total number of unread notifications.
|
||||
/// The number of user mentions since the last ack.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notification_count: Option<UInt>,
|
||||
pub mention_user: Option<UInt>,
|
||||
|
||||
/// The number of bulk mentions since the last ack
|
||||
/// Bulk mentions are @room and @thread. If there is a user mention,
|
||||
/// `mention_user` is incremented instead
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mention_bulk: Option<UInt>,
|
||||
|
||||
/// The total number of unread notifications since the last ack.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notify: Option<UInt>,
|
||||
|
||||
/// The total number of unread messages since the last ack.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub messages: Option<UInt>,
|
||||
}
|
||||
|
||||
impl UnreadNotificationsCount {
|
||||
|
@ -32,7 +46,11 @@ impl UnreadNotificationsCount {
|
|||
|
||||
/// Returns true if there are no notification count updates.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.highlight_count.is_none() && self.notification_count.is_none()
|
||||
self.mention_user.is_none()
|
||||
&& self.mention_bulk.is_none()
|
||||
&& self.notify.is_none()
|
||||
&& self.messages.is_none()
|
||||
&& self.last_ack.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,252 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use as_variant::as_variant;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
|
||||
|
||||
use crate::serde::from_raw_json_value;
|
||||
|
||||
/// This represents the different actions that should be taken when a rule is matched, and
|
||||
/// controls how notifications are delivered to the client.
|
||||
///
|
||||
/// See [the spec](https://spec.matrix.org/latest/client-server-api/#actions) for details.
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum Action {
|
||||
/// Causes matching events to generate a notification.
|
||||
Notify,
|
||||
|
||||
/// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
|
||||
SetTweak(Tweak),
|
||||
|
||||
/// An unknown action.
|
||||
#[doc(hidden)]
|
||||
_Custom(CustomAction),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
/// Whether this action is an `Action::SetTweak(Tweak::Highlight(true))`.
|
||||
pub fn is_highlight(&self) -> bool {
|
||||
matches!(self, Action::SetTweak(Tweak::Highlight(true)))
|
||||
}
|
||||
|
||||
/// Whether this action should trigger a notification.
|
||||
pub fn should_notify(&self) -> bool {
|
||||
matches!(self, Action::Notify)
|
||||
}
|
||||
|
||||
/// The sound that should be played with this action, if any.
|
||||
pub fn sound(&self) -> Option<&str> {
|
||||
as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `set_tweak` action.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")]
|
||||
pub enum Tweak {
|
||||
/// A string representing the sound to be played when this notification arrives.
|
||||
///
|
||||
/// A value of "default" means to play a default sound. A device may choose to alert the user
|
||||
/// by some other means if appropriate, eg. vibration.
|
||||
Sound(String),
|
||||
|
||||
/// A boolean representing whether or not this message should be highlighted in the UI.
|
||||
///
|
||||
/// This will normally take the form of presenting the message in a different color and/or
|
||||
/// style. The UI might also be adjusted to draw particular attention to the room in which the
|
||||
/// event occurred. If a `highlight` tweak is given with no value, its value is defined to be
|
||||
/// `true`. If no highlight tweak is given at all then the value of `highlight` is defined to
|
||||
/// be `false`.
|
||||
Highlight(#[serde(default = "crate::serde::default_true")] bool),
|
||||
|
||||
/// A custom tweak
|
||||
Custom {
|
||||
/// The name of the custom tweak (`set_tweak` field)
|
||||
name: String,
|
||||
|
||||
/// The value of the custom tweak
|
||||
value: Box<RawJsonValue>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Action {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
let custom: CustomAction = from_raw_json_value(&json)?;
|
||||
|
||||
match &custom {
|
||||
CustomAction::String(s) => match s.as_str() {
|
||||
"notify" => Ok(Action::Notify),
|
||||
_ => Ok(Action::_Custom(custom)),
|
||||
},
|
||||
CustomAction::Object(o) => {
|
||||
if o.get("set_tweak").is_some() {
|
||||
Ok(Action::SetTweak(from_raw_json_value(&json)?))
|
||||
} else {
|
||||
Ok(Action::_Custom(custom))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Action {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"),
|
||||
Action::SetTweak(kind) => kind.serialize(serializer),
|
||||
Action::_Custom(custom) => custom.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unknown action.
|
||||
#[doc(hidden)]
|
||||
#[allow(unknown_lints, unnameable_types)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CustomAction {
|
||||
/// A string.
|
||||
String(String),
|
||||
|
||||
/// An object.
|
||||
Object(BTreeMap<String, JsonValue>),
|
||||
}
|
||||
|
||||
mod tweak_serde {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
/// Values for the `set_tweak` action.
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Tweak {
|
||||
Sound(SoundTweak),
|
||||
Highlight(HighlightTweak),
|
||||
Custom {
|
||||
#[serde(rename = "set_tweak")]
|
||||
name: String,
|
||||
value: Box<RawJsonValue>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "set_tweak", rename = "sound")]
|
||||
pub(crate) struct SoundTweak {
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "set_tweak", rename = "highlight")]
|
||||
pub(crate) struct HighlightTweak {
|
||||
#[serde(
|
||||
default = "crate::serde::default_true",
|
||||
skip_serializing_if = "crate::serde::is_true"
|
||||
)]
|
||||
value: bool,
|
||||
}
|
||||
|
||||
impl From<super::Tweak> for Tweak {
|
||||
fn from(tweak: super::Tweak) -> Self {
|
||||
use super::Tweak::*;
|
||||
|
||||
match tweak {
|
||||
Sound(value) => Self::Sound(SoundTweak { value }),
|
||||
Highlight(value) => Self::Highlight(HighlightTweak { value }),
|
||||
Custom { name, value } => Self::Custom { name, value },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tweak> for super::Tweak {
|
||||
fn from(tweak: Tweak) -> Self {
|
||||
use Tweak::*;
|
||||
|
||||
match tweak {
|
||||
Sound(SoundTweak { value }) => Self::Sound(value),
|
||||
Highlight(HighlightTweak { value }) => Self::Highlight(value),
|
||||
Custom { name, value } => Self::Custom { name, value },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_matches;
|
||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||
|
||||
use super::{Action, Tweak};
|
||||
|
||||
#[test]
|
||||
fn serialize_string() {
|
||||
assert_eq!(to_json_value(&Action::Notify).unwrap(), json!("notify"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_tweak_sound() {
|
||||
assert_eq!(
|
||||
to_json_value(&Action::SetTweak(Tweak::Sound("default".into()))).unwrap(),
|
||||
json!({ "set_tweak": "sound", "value": "default" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_tweak_highlight() {
|
||||
assert_eq!(
|
||||
to_json_value(&Action::SetTweak(Tweak::Highlight(true))).unwrap(),
|
||||
json!({ "set_tweak": "highlight" })
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
to_json_value(&Action::SetTweak(Tweak::Highlight(false))).unwrap(),
|
||||
json!({ "set_tweak": "highlight", "value": false })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_string() {
|
||||
assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_tweak_sound() {
|
||||
let json_data = json!({
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
});
|
||||
assert_matches!(
|
||||
from_json_value::<Action>(json_data),
|
||||
Ok(Action::SetTweak(Tweak::Sound(value)))
|
||||
);
|
||||
assert_eq!(value, "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_tweak_highlight() {
|
||||
let json_data = json!({
|
||||
"set_tweak": "highlight",
|
||||
"value": true
|
||||
});
|
||||
assert_matches!(
|
||||
from_json_value::<Action>(json_data),
|
||||
Ok(Action::SetTweak(Tweak::Highlight(true)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_tweak_highlight_with_default_value() {
|
||||
assert_matches!(
|
||||
from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
|
||||
Ok(Action::SetTweak(Tweak::Highlight(true)))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,932 +0,0 @@
|
|||
use std::{collections::BTreeMap, ops::RangeBounds, str::FromStr};
|
||||
|
||||
use js_int::{Int, UInt};
|
||||
use regex::bytes::Regex;
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
use ruma_macros::StringEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::Value as JsonValue;
|
||||
use wildmatch::WildMatch;
|
||||
|
||||
use crate::{power_levels::NotificationPowerLevels, OwnedRoomId, OwnedUserId, UserId};
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
use crate::{PrivOwnedStr, RoomVersionId};
|
||||
|
||||
mod flattened_json;
|
||||
mod push_condition_serde;
|
||||
mod room_member_count_is;
|
||||
|
||||
pub use self::{
|
||||
flattened_json::{FlattenedJson, FlattenedJsonValue, ScalarJsonValue},
|
||||
room_member_count_is::{ComparisonOperator, RoomMemberCountIs},
|
||||
};
|
||||
|
||||
/// Features supported by room versions.
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, PartialEq, Eq, StringEnum)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum RoomVersionFeature {
|
||||
/// m.extensible_events
|
||||
///
|
||||
/// The room supports [extensible events].
|
||||
///
|
||||
/// [extensible events]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
|
||||
#[cfg(feature = "unstable-msc3932")]
|
||||
#[ruma_enum(rename = "org.matrix.msc3932.extensible_events")]
|
||||
ExtensibleEvents,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
impl RoomVersionFeature {
|
||||
/// Get the default features for the given room version.
|
||||
pub fn list_for_room_version(version: &RoomVersionId) -> Vec<Self> {
|
||||
match version {
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
| RoomVersionId::V7
|
||||
| RoomVersionId::V8
|
||||
| RoomVersionId::V9
|
||||
| RoomVersionId::V10
|
||||
| RoomVersionId::V11
|
||||
| RoomVersionId::_Custom(_) => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A condition that must apply for an associated push rule's action to be taken.
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum PushCondition {
|
||||
/// A glob pattern match on a field of the event.
|
||||
EventMatch {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Patterns with no special glob characters should be treated as having asterisks
|
||||
/// prepended and appended when testing the condition.
|
||||
pattern: String,
|
||||
},
|
||||
|
||||
/// Matches unencrypted messages where `content.body` contains the owner's display name in that
|
||||
/// room.
|
||||
ContainsDisplayName,
|
||||
|
||||
/// Matches the current number of members in the room.
|
||||
RoomMemberCount {
|
||||
/// The condition on the current number of members in the room.
|
||||
is: RoomMemberCountIs,
|
||||
},
|
||||
|
||||
/// Takes into account the current power levels in the room, ensuring the sender of the event
|
||||
/// has high enough power to trigger the notification.
|
||||
SenderNotificationPermission {
|
||||
/// The field in the power level event the user needs a minimum power level for.
|
||||
///
|
||||
/// Fields must be specified under the `notifications` property in the power level event's
|
||||
/// `content`.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Apply the rule only to rooms that support a given feature.
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
RoomVersionSupports {
|
||||
/// The feature the room must support for the push rule to apply.
|
||||
feature: RoomVersionFeature,
|
||||
},
|
||||
|
||||
/// Exact value match on a property of the event.
|
||||
EventPropertyIs {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: ScalarJsonValue,
|
||||
},
|
||||
|
||||
/// Exact value match on a value in an array property of the event.
|
||||
EventPropertyContains {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: ScalarJsonValue,
|
||||
},
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(_CustomPushCondition),
|
||||
}
|
||||
|
||||
pub(super) fn check_event_match(
|
||||
event: &FlattenedJson,
|
||||
key: &str,
|
||||
pattern: &str,
|
||||
context: &PushConditionRoomCtx,
|
||||
) -> bool {
|
||||
let value = match key {
|
||||
"room_id" => context.room_id.as_str(),
|
||||
_ => match event.get_str(key) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
},
|
||||
};
|
||||
|
||||
value.matches_pattern(pattern, key == "content.body")
|
||||
}
|
||||
|
||||
impl PushCondition {
|
||||
/// Check if this condition applies to the event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The flattened JSON representation of a room message event.
|
||||
/// * `context` - The context of the room at the time of the event.
|
||||
pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
|
||||
if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::EventMatch { key, pattern } => check_event_match(event, key, pattern, context),
|
||||
Self::ContainsDisplayName => {
|
||||
let value = match event.get_str("content.body") {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
value.matches_pattern(&context.user_display_name, true)
|
||||
}
|
||||
Self::RoomMemberCount { is } => is.contains(&context.member_count),
|
||||
Self::SenderNotificationPermission { key } => {
|
||||
let sender_id = match event.get_str("sender") {
|
||||
Some(v) => match <&UserId>::try_from(v) {
|
||||
Ok(u) => u,
|
||||
Err(_) => return false,
|
||||
},
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let sender_level = context
|
||||
.users_power_levels
|
||||
.get(sender_id)
|
||||
.unwrap_or(&context.default_power_level);
|
||||
|
||||
match context.notification_power_levels.get(key) {
|
||||
Some(l) => sender_level >= l,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
Self::RoomVersionSupports { feature } => match feature {
|
||||
RoomVersionFeature::ExtensibleEvents => {
|
||||
context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents)
|
||||
}
|
||||
RoomVersionFeature::_Custom(_) => false,
|
||||
},
|
||||
Self::EventPropertyIs { key, value } => event.get(key).is_some_and(|v| v == value),
|
||||
Self::EventPropertyContains { key, value } => event
|
||||
.get(key)
|
||||
.and_then(FlattenedJsonValue::as_array)
|
||||
.is_some_and(|a| a.contains(value)),
|
||||
Self::_Custom(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unknown push condition.
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
pub struct _CustomPushCondition {
|
||||
/// The kind of the condition.
|
||||
kind: String,
|
||||
|
||||
/// The additional fields that the condition contains.
|
||||
#[serde(flatten)]
|
||||
data: BTreeMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
/// The context of the room associated to an event to be able to test all push conditions.
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
pub struct PushConditionRoomCtx {
|
||||
/// The ID of the room.
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The number of members in the room.
|
||||
pub member_count: UInt,
|
||||
|
||||
/// The users matrix ID.
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
/// The display name of the current user in the room.
|
||||
pub user_display_name: String,
|
||||
|
||||
/// The power levels of the users of the room.
|
||||
pub users_power_levels: BTreeMap<OwnedUserId, Int>,
|
||||
|
||||
/// The default power level of the users of the room.
|
||||
pub default_power_level: Int,
|
||||
|
||||
/// The notification power levels of the room.
|
||||
pub notification_power_levels: NotificationPowerLevels,
|
||||
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
/// The list of features this room's version or the room itself supports.
|
||||
pub supported_features: Vec<RoomVersionFeature>,
|
||||
}
|
||||
|
||||
/// Additional functions for character matching.
|
||||
trait CharExt {
|
||||
/// Whether or not this char can be part of a word.
|
||||
fn is_word_char(&self) -> bool;
|
||||
}
|
||||
|
||||
impl CharExt for char {
|
||||
fn is_word_char(&self) -> bool {
|
||||
self.is_ascii_alphanumeric() || *self == '_'
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional functions for string matching.
|
||||
trait StrExt {
|
||||
/// Get the length of the char at `index`. The byte index must correspond to
|
||||
/// the start of a char boundary.
|
||||
fn char_len(&self, index: usize) -> usize;
|
||||
|
||||
/// Get the char at `index`. The byte index must correspond to the start of
|
||||
/// a char boundary.
|
||||
fn char_at(&self, index: usize) -> char;
|
||||
|
||||
/// Get the index of the char that is before the char at `index`. The byte index
|
||||
/// must correspond to a char boundary.
|
||||
///
|
||||
/// Returns `None` if there's no previous char. Otherwise, returns the char.
|
||||
fn find_prev_char(&self, index: usize) -> Option<char>;
|
||||
|
||||
/// Matches this string against `pattern`.
|
||||
///
|
||||
/// The pattern can be a glob with wildcards `*` and `?`.
|
||||
///
|
||||
/// The match is case insensitive.
|
||||
///
|
||||
/// If `match_words` is `true`, checks that the pattern is separated from other words.
|
||||
fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool;
|
||||
|
||||
/// Matches this string against `pattern`, with word boundaries.
|
||||
///
|
||||
/// The pattern can be a glob with wildcards `*` and `?`.
|
||||
///
|
||||
/// A word boundary is defined as the start or end of the value, or any character not in the
|
||||
/// sets `[A-Z]`, `[a-z]`, `[0-9]` or `_`.
|
||||
///
|
||||
/// The match is case sensitive.
|
||||
fn matches_word(&self, pattern: &str) -> bool;
|
||||
|
||||
/// Translate the wildcards in `self` to a regex syntax.
|
||||
///
|
||||
/// `self` must only contain wildcards.
|
||||
fn wildcards_to_regex(&self) -> String;
|
||||
}
|
||||
|
||||
impl StrExt for str {
|
||||
fn char_len(&self, index: usize) -> usize {
|
||||
let mut len = 1;
|
||||
while !self.is_char_boundary(index + len) {
|
||||
len += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
fn char_at(&self, index: usize) -> char {
|
||||
let end = index + self.char_len(index);
|
||||
let char_str = &self[index..end];
|
||||
char::from_str(char_str)
|
||||
.unwrap_or_else(|_| panic!("Could not convert str '{char_str}' to char"))
|
||||
}
|
||||
|
||||
fn find_prev_char(&self, index: usize) -> Option<char> {
|
||||
if index == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pos = index - 1;
|
||||
while !self.is_char_boundary(pos) {
|
||||
pos -= 1;
|
||||
}
|
||||
Some(self.char_at(pos))
|
||||
}
|
||||
|
||||
fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool {
|
||||
let value = &self.to_lowercase();
|
||||
let pattern = &pattern.to_lowercase();
|
||||
|
||||
if match_words {
|
||||
value.matches_word(pattern)
|
||||
} else {
|
||||
WildMatch::new(pattern).matches(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_word(&self, pattern: &str) -> bool {
|
||||
if self == pattern {
|
||||
return true;
|
||||
}
|
||||
if pattern.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let has_wildcards = pattern.contains(|c| matches!(c, '?' | '*'));
|
||||
|
||||
if has_wildcards {
|
||||
let mut chunks: Vec<String> = vec![];
|
||||
let mut prev_wildcard = false;
|
||||
let mut chunk_start = 0;
|
||||
|
||||
for (i, c) in pattern.char_indices() {
|
||||
if matches!(c, '?' | '*') && !prev_wildcard {
|
||||
if i != 0 {
|
||||
chunks.push(regex::escape(&pattern[chunk_start..i]));
|
||||
chunk_start = i;
|
||||
}
|
||||
|
||||
prev_wildcard = true;
|
||||
} else if prev_wildcard {
|
||||
let chunk = &pattern[chunk_start..i];
|
||||
chunks.push(chunk.wildcards_to_regex());
|
||||
|
||||
chunk_start = i;
|
||||
prev_wildcard = false;
|
||||
}
|
||||
}
|
||||
|
||||
let len = pattern.len();
|
||||
if !prev_wildcard {
|
||||
chunks.push(regex::escape(&pattern[chunk_start..len]));
|
||||
} else if prev_wildcard {
|
||||
let chunk = &pattern[chunk_start..len];
|
||||
chunks.push(chunk.wildcards_to_regex());
|
||||
}
|
||||
|
||||
// The word characters in ASCII compatible mode (with the `-u` flag) match the
|
||||
// definition in the spec: any character not in the set `[A-Za-z0-9_]`.
|
||||
let regex = format!(r"(?-u:^|\W|\b){}(?-u:\b|\W|$)", chunks.concat());
|
||||
let re = Regex::new(®ex).expect("regex construction should succeed");
|
||||
re.is_match(self.as_bytes())
|
||||
} else {
|
||||
match self.find(pattern) {
|
||||
Some(start) => {
|
||||
let end = start + pattern.len();
|
||||
|
||||
// Look if the match has word boundaries.
|
||||
let word_boundary_start = !self.char_at(start).is_word_char()
|
||||
|| !self.find_prev_char(start).is_some_and(|c| c.is_word_char());
|
||||
|
||||
if word_boundary_start {
|
||||
let word_boundary_end = end == self.len()
|
||||
|| !self.find_prev_char(end).unwrap().is_word_char()
|
||||
|| !self.char_at(end).is_word_char();
|
||||
|
||||
if word_boundary_end {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Find next word.
|
||||
let non_word_str = &self[start..];
|
||||
let non_word = match non_word_str.find(|c: char| !c.is_word_char()) {
|
||||
Some(pos) => pos,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let word_str = &non_word_str[non_word..];
|
||||
let word = match word_str.find(|c: char| c.is_word_char()) {
|
||||
Some(pos) => pos,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
word_str[word..].matches_word(pattern)
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wildcards_to_regex(&self) -> String {
|
||||
// Simplify pattern to avoid performance issues:
|
||||
// - The glob `?**?**?` is equivalent to the glob `???*`
|
||||
// - The glob `???*` is equivalent to the regex `.{3,}`
|
||||
let question_marks = self.matches('?').count();
|
||||
|
||||
if self.contains('*') {
|
||||
format!(".{{{question_marks},}}")
|
||||
} else {
|
||||
format!(".{{{question_marks}}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches2::assert_matches;
|
||||
use js_int::{int, uint};
|
||||
use serde_json::{
|
||||
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
||||
};
|
||||
|
||||
use super::{FlattenedJson, PushCondition, PushConditionRoomCtx, RoomMemberCountIs, StrExt};
|
||||
use crate::{
|
||||
owned_room_id, owned_user_id, power_levels::NotificationPowerLevels, serde::Raw,
|
||||
OwnedUserId,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn serialize_event_match_condition() {
|
||||
let json_data = json!({
|
||||
"key": "content.msgtype",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.notice"
|
||||
});
|
||||
assert_eq!(
|
||||
to_json_value(PushCondition::EventMatch {
|
||||
key: "content.msgtype".into(),
|
||||
pattern: "m.notice".into(),
|
||||
})
|
||||
.unwrap(),
|
||||
json_data
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_contains_display_name_condition() {
|
||||
assert_eq!(
|
||||
to_json_value(PushCondition::ContainsDisplayName).unwrap(),
|
||||
json!({ "kind": "contains_display_name" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_room_member_count_condition() {
|
||||
let json_data = json!({
|
||||
"is": "2",
|
||||
"kind": "room_member_count"
|
||||
});
|
||||
assert_eq!(
|
||||
to_json_value(PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) })
|
||||
.unwrap(),
|
||||
json_data
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_sender_notification_permission_condition() {
|
||||
let json_data = json!({
|
||||
"key": "room",
|
||||
"kind": "sender_notification_permission"
|
||||
});
|
||||
assert_eq!(
|
||||
json_data,
|
||||
to_json_value(PushCondition::SenderNotificationPermission { key: "room".into() })
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_event_match_condition() {
|
||||
let json_data = json!({
|
||||
"key": "content.msgtype",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.notice"
|
||||
});
|
||||
assert_matches!(
|
||||
from_json_value::<PushCondition>(json_data).unwrap(),
|
||||
PushCondition::EventMatch { key, pattern }
|
||||
);
|
||||
assert_eq!(key, "content.msgtype");
|
||||
assert_eq!(pattern, "m.notice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_contains_display_name_condition() {
|
||||
assert_matches!(
|
||||
from_json_value::<PushCondition>(json!({ "kind": "contains_display_name" })).unwrap(),
|
||||
PushCondition::ContainsDisplayName
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_room_member_count_condition() {
|
||||
let json_data = json!({
|
||||
"is": "2",
|
||||
"kind": "room_member_count"
|
||||
});
|
||||
assert_matches!(
|
||||
from_json_value::<PushCondition>(json_data).unwrap(),
|
||||
PushCondition::RoomMemberCount { is }
|
||||
);
|
||||
assert_eq!(is, RoomMemberCountIs::from(uint!(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_sender_notification_permission_condition() {
|
||||
let json_data = json!({
|
||||
"key": "room",
|
||||
"kind": "sender_notification_permission"
|
||||
});
|
||||
assert_matches!(
|
||||
from_json_value::<PushCondition>(json_data).unwrap(),
|
||||
PushCondition::SenderNotificationPermission { key }
|
||||
);
|
||||
assert_eq!(key, "room");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn words_match() {
|
||||
assert!("foo bar".matches_word("foo"));
|
||||
assert!(!"Foo bar".matches_word("foo"));
|
||||
assert!(!"foobar".matches_word("foo"));
|
||||
assert!("foobar foo".matches_word("foo"));
|
||||
assert!(!"foobar foobar".matches_word("foo"));
|
||||
assert!(!"foobar bar".matches_word("bar bar"));
|
||||
assert!("foobar bar bar".matches_word("bar bar"));
|
||||
assert!(!"foobar bar barfoo".matches_word("bar bar"));
|
||||
assert!("conduit ⚡️".matches_word("conduit ⚡️"));
|
||||
assert!("conduit ⚡️".matches_word("conduit"));
|
||||
assert!("conduit ⚡️".matches_word("⚡️"));
|
||||
assert!("conduit⚡️".matches_word("conduit"));
|
||||
assert!("conduit⚡️".matches_word("⚡️"));
|
||||
assert!("⚡️conduit".matches_word("conduit"));
|
||||
assert!("⚡️conduit".matches_word("⚡️"));
|
||||
assert!("Ruma Dev👩💻".matches_word("Dev"));
|
||||
assert!("Ruma Dev👩💻".matches_word("👩💻"));
|
||||
assert!("Ruma Dev👩💻".matches_word("Dev👩💻"));
|
||||
|
||||
// Regex syntax is escaped
|
||||
assert!(!"matrix".matches_word(r"\w*"));
|
||||
assert!(r"\w".matches_word(r"\w*"));
|
||||
assert!(!"matrix".matches_word("[a-z]*"));
|
||||
assert!("[a-z] and [0-9]".matches_word("[a-z]*"));
|
||||
assert!(!"m".matches_word("[[:alpha:]]?"));
|
||||
assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?"));
|
||||
|
||||
// From the spec: <https://spec.matrix.org/v1.9/client-server-api/#conditions-1>
|
||||
assert!("An example event.".matches_word("ex*ple"));
|
||||
assert!("exple".matches_word("ex*ple"));
|
||||
assert!("An exciting triple-whammy".matches_word("ex*ple"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patterns_match() {
|
||||
// Word matching without glob
|
||||
assert!("foo bar".matches_pattern("foo", true));
|
||||
assert!("Foo bar".matches_pattern("foo", true));
|
||||
assert!(!"foobar".matches_pattern("foo", true));
|
||||
assert!("".matches_pattern("", true));
|
||||
assert!(!"foo".matches_pattern("", true));
|
||||
assert!("foo bar".matches_pattern("foo bar", true));
|
||||
assert!(" foo bar ".matches_pattern("foo bar", true));
|
||||
assert!("baz foo bar baz".matches_pattern("foo bar", true));
|
||||
assert!("foo baré".matches_pattern("foo bar", true));
|
||||
assert!(!"bar foo".matches_pattern("foo bar", true));
|
||||
assert!("foo bar".matches_pattern("foo ", true));
|
||||
assert!("foo ".matches_pattern("foo ", true));
|
||||
assert!("foo ".matches_pattern("foo ", true));
|
||||
assert!(" foo ".matches_pattern("foo ", true));
|
||||
|
||||
// Word matching with glob
|
||||
assert!("foo bar".matches_pattern("foo*", true));
|
||||
assert!("foo bar".matches_pattern("foo b?r", true));
|
||||
assert!(" foo bar ".matches_pattern("foo b?r", true));
|
||||
assert!("baz foo bar baz".matches_pattern("foo b?r", true));
|
||||
assert!("foo baré".matches_pattern("foo b?r", true));
|
||||
assert!(!"bar foo".matches_pattern("foo b?r", true));
|
||||
assert!("foo bar".matches_pattern("f*o ", true));
|
||||
assert!("foo ".matches_pattern("f*o ", true));
|
||||
assert!("foo ".matches_pattern("f*o ", true));
|
||||
assert!(" foo ".matches_pattern("f*o ", true));
|
||||
|
||||
// Glob matching
|
||||
assert!(!"foo bar".matches_pattern("foo", false));
|
||||
assert!("foo".matches_pattern("foo", false));
|
||||
assert!("foo".matches_pattern("foo*", false));
|
||||
assert!("foobar".matches_pattern("foo*", false));
|
||||
assert!("foo bar".matches_pattern("foo*", false));
|
||||
assert!(!"foo".matches_pattern("foo?", false));
|
||||
assert!("fooo".matches_pattern("foo?", false));
|
||||
assert!("FOO".matches_pattern("foo", false));
|
||||
assert!("".matches_pattern("", false));
|
||||
assert!("".matches_pattern("*", false));
|
||||
assert!(!"foo".matches_pattern("", false));
|
||||
|
||||
// From the spec: <https://spec.matrix.org/v1.9/client-server-api/#conditions-1>
|
||||
assert!("Lunch plans".matches_pattern("lunc?*", false));
|
||||
assert!("LUNCH".matches_pattern("lunc?*", false));
|
||||
assert!(!" lunch".matches_pattern("lunc?*", false));
|
||||
assert!(!"lunc".matches_pattern("lunc?*", false));
|
||||
}
|
||||
|
||||
fn sender() -> OwnedUserId {
|
||||
owned_user_id!("@worthy_whale:server.name")
|
||||
}
|
||||
|
||||
fn push_context() -> PushConditionRoomCtx {
|
||||
let mut users_power_levels = BTreeMap::new();
|
||||
users_power_levels.insert(sender(), int!(25));
|
||||
|
||||
PushConditionRoomCtx {
|
||||
room_id: owned_room_id!("!room:server.name"),
|
||||
member_count: uint!(3),
|
||||
user_id: owned_user_id!("@gorilla:server.name"),
|
||||
user_display_name: "Groovy Gorilla".into(),
|
||||
users_power_levels,
|
||||
default_power_level: int!(50),
|
||||
notification_power_levels: NotificationPowerLevels { room: int!(50) },
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
supported_features: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_flattened_event() -> FlattenedJson {
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"sender": "@worthy_whale:server.name",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "@room Give a warm welcome to Groovy Gorilla"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
FlattenedJson::from_raw(&raw)
|
||||
}
|
||||
|
||||
fn second_flattened_event() -> FlattenedJson {
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"sender": "@party_bot:server.name",
|
||||
"content": {
|
||||
"msgtype": "m.notice",
|
||||
"body": "Everybody come to party!"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
FlattenedJson::from_raw(&raw)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_match_applies() {
|
||||
let context = push_context();
|
||||
let first_event = first_flattened_event();
|
||||
let second_event = second_flattened_event();
|
||||
|
||||
let correct_room = PushCondition::EventMatch {
|
||||
key: "room_id".into(),
|
||||
pattern: "!room:server.name".into(),
|
||||
};
|
||||
let incorrect_room = PushCondition::EventMatch {
|
||||
key: "room_id".into(),
|
||||
pattern: "!incorrect:server.name".into(),
|
||||
};
|
||||
|
||||
assert!(correct_room.applies(&first_event, &context));
|
||||
assert!(!incorrect_room.applies(&first_event, &context));
|
||||
|
||||
let keyword =
|
||||
PushCondition::EventMatch { key: "content.body".into(), pattern: "come".into() };
|
||||
|
||||
assert!(!keyword.applies(&first_event, &context));
|
||||
assert!(keyword.applies(&second_event, &context));
|
||||
|
||||
let msgtype =
|
||||
PushCondition::EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into() };
|
||||
|
||||
assert!(!msgtype.applies(&first_event, &context));
|
||||
assert!(msgtype.applies(&second_event, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_member_count_is_applies() {
|
||||
let context = push_context();
|
||||
let event = first_flattened_event();
|
||||
|
||||
let member_count_eq =
|
||||
PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(3)) };
|
||||
let member_count_gt =
|
||||
PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)..) };
|
||||
let member_count_lt =
|
||||
PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(..uint!(3)) };
|
||||
|
||||
assert!(member_count_eq.applies(&event, &context));
|
||||
assert!(member_count_gt.applies(&event, &context));
|
||||
assert!(!member_count_lt.applies(&event, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_display_name_applies() {
|
||||
let context = push_context();
|
||||
let first_event = first_flattened_event();
|
||||
let second_event = second_flattened_event();
|
||||
|
||||
let contains_display_name = PushCondition::ContainsDisplayName;
|
||||
|
||||
assert!(contains_display_name.applies(&first_event, &context));
|
||||
assert!(!contains_display_name.applies(&second_event, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sender_notification_permission_applies() {
|
||||
let context = push_context();
|
||||
let first_event = first_flattened_event();
|
||||
let second_event = second_flattened_event();
|
||||
|
||||
let sender_notification_permission =
|
||||
PushCondition::SenderNotificationPermission { key: "room".into() };
|
||||
|
||||
assert!(!sender_notification_permission.applies(&first_event, &context));
|
||||
assert!(sender_notification_permission.applies(&second_event, &context));
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc3932")]
|
||||
#[test]
|
||||
fn room_version_supports_applies() {
|
||||
let context_not_matching = push_context();
|
||||
|
||||
let context_matching = PushConditionRoomCtx {
|
||||
room_id: owned_room_id!("!room:server.name"),
|
||||
member_count: uint!(3),
|
||||
user_id: owned_user_id!("@gorilla:server.name"),
|
||||
user_display_name: "Groovy Gorilla".into(),
|
||||
users_power_levels: context_not_matching.users_power_levels.clone(),
|
||||
default_power_level: int!(50),
|
||||
notification_power_levels: NotificationPowerLevels { room: int!(50) },
|
||||
supported_features: vec![super::RoomVersionFeature::ExtensibleEvents],
|
||||
};
|
||||
|
||||
let simple_event_raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"sender": "@worthy_whale:server.name",
|
||||
"content": {
|
||||
"msgtype": "org.matrix.msc3932.extensible_events",
|
||||
"body": "@room Give a warm welcome to Groovy Gorilla"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let simple_event = FlattenedJson::from_raw(&simple_event_raw);
|
||||
|
||||
let room_version_condition = PushCondition::RoomVersionSupports {
|
||||
feature: super::RoomVersionFeature::ExtensibleEvents,
|
||||
};
|
||||
|
||||
assert!(room_version_condition.applies(&simple_event, &context_matching));
|
||||
assert!(!room_version_condition.applies(&simple_event, &context_not_matching));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_property_is_applies() {
|
||||
use crate::push::condition::ScalarJsonValue;
|
||||
|
||||
let context = push_context();
|
||||
let event_raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"sender": "@worthy_whale:server.name",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Boom!",
|
||||
"org.fake.boolean": false,
|
||||
"org.fake.number": 13,
|
||||
"org.fake.null": null
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let event = FlattenedJson::from_raw(&event_raw);
|
||||
|
||||
let string_match = PushCondition::EventPropertyIs {
|
||||
key: "content.body".to_owned(),
|
||||
value: "Boom!".into(),
|
||||
};
|
||||
assert!(string_match.applies(&event, &context));
|
||||
|
||||
let string_no_match =
|
||||
PushCondition::EventPropertyIs { key: "content.body".to_owned(), value: "Boom".into() };
|
||||
assert!(!string_no_match.applies(&event, &context));
|
||||
|
||||
let wrong_type =
|
||||
PushCondition::EventPropertyIs { key: "content.body".to_owned(), value: false.into() };
|
||||
assert!(!wrong_type.applies(&event, &context));
|
||||
|
||||
let bool_match = PushCondition::EventPropertyIs {
|
||||
key: r"content.org\.fake\.boolean".to_owned(),
|
||||
value: false.into(),
|
||||
};
|
||||
assert!(bool_match.applies(&event, &context));
|
||||
|
||||
let bool_no_match = PushCondition::EventPropertyIs {
|
||||
key: r"content.org\.fake\.boolean".to_owned(),
|
||||
value: true.into(),
|
||||
};
|
||||
assert!(!bool_no_match.applies(&event, &context));
|
||||
|
||||
let int_match = PushCondition::EventPropertyIs {
|
||||
key: r"content.org\.fake\.number".to_owned(),
|
||||
value: int!(13).into(),
|
||||
};
|
||||
assert!(int_match.applies(&event, &context));
|
||||
|
||||
let int_no_match = PushCondition::EventPropertyIs {
|
||||
key: r"content.org\.fake\.number".to_owned(),
|
||||
value: int!(130).into(),
|
||||
};
|
||||
assert!(!int_no_match.applies(&event, &context));
|
||||
|
||||
let null_match = PushCondition::EventPropertyIs {
|
||||
key: r"content.org\.fake\.null".to_owned(),
|
||||
value: ScalarJsonValue::Null,
|
||||
};
|
||||
assert!(null_match.applies(&event, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_property_contains_applies() {
|
||||
use crate::push::condition::ScalarJsonValue;
|
||||
|
||||
let context = push_context();
|
||||
let event_raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"sender": "@worthy_whale:server.name",
|
||||
"content": {
|
||||
"org.fake.array": ["Boom!", false, 13, null]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let event = FlattenedJson::from_raw(&event_raw);
|
||||
|
||||
let wrong_key =
|
||||
PushCondition::EventPropertyContains { key: "send".to_owned(), value: false.into() };
|
||||
assert!(!wrong_key.applies(&event, &context));
|
||||
|
||||
let string_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: "Boom!".into(),
|
||||
};
|
||||
assert!(string_match.applies(&event, &context));
|
||||
|
||||
let string_no_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: "Boom".into(),
|
||||
};
|
||||
assert!(!string_no_match.applies(&event, &context));
|
||||
|
||||
let bool_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: false.into(),
|
||||
};
|
||||
assert!(bool_match.applies(&event, &context));
|
||||
|
||||
let bool_no_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: true.into(),
|
||||
};
|
||||
assert!(!bool_no_match.applies(&event, &context));
|
||||
|
||||
let int_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: int!(13).into(),
|
||||
};
|
||||
assert!(int_match.applies(&event, &context));
|
||||
|
||||
let int_no_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: int!(130).into(),
|
||||
};
|
||||
assert!(!int_no_match.applies(&event, &context));
|
||||
|
||||
let null_match = PushCondition::EventPropertyContains {
|
||||
key: r"content.org\.fake\.array".to_owned(),
|
||||
value: ScalarJsonValue::Null,
|
||||
};
|
||||
assert!(null_match.applies(&event, &context));
|
||||
}
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use as_variant::as_variant;
|
||||
use js_int::Int;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{to_value as to_json_value, value::Value as JsonValue};
|
||||
use thiserror::Error;
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use crate::serde::Raw;
|
||||
|
||||
/// The flattened representation of a JSON object.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FlattenedJson {
|
||||
/// The internal map containing the flattened JSON as a pair path, value.
|
||||
map: BTreeMap<String, FlattenedJsonValue>,
|
||||
}
|
||||
|
||||
impl FlattenedJson {
|
||||
/// Create a `FlattenedJson` from `Raw`.
|
||||
pub fn from_raw<T>(raw: &Raw<T>) -> Self {
|
||||
let mut s = Self { map: BTreeMap::new() };
|
||||
s.flatten_value(to_json_value(raw).unwrap(), "".into());
|
||||
s
|
||||
}
|
||||
|
||||
/// Flatten and insert the `value` at `path`.
|
||||
#[instrument(skip(self, value))]
|
||||
fn flatten_value(&mut self, value: JsonValue, path: String) {
|
||||
match value {
|
||||
JsonValue::Object(fields) => {
|
||||
if fields.is_empty() {
|
||||
if self.map.insert(path.clone(), FlattenedJsonValue::EmptyObject).is_some() {
|
||||
warn!("Duplicate path in flattened JSON: {path}");
|
||||
}
|
||||
} else {
|
||||
for (key, value) in fields {
|
||||
let key = escape_key(&key);
|
||||
let path = if path.is_empty() { key } else { format!("{path}.{key}") };
|
||||
self.flatten_value(value, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
value => {
|
||||
if let Some(v) = FlattenedJsonValue::from_json_value(value) {
|
||||
if self.map.insert(path.clone(), v).is_some() {
|
||||
warn!("Duplicate path in flattened JSON: {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value associated with the given `path`.
|
||||
pub fn get(&self, path: &str) -> Option<&FlattenedJsonValue> {
|
||||
self.map.get(path)
|
||||
}
|
||||
|
||||
/// Get the value associated with the given `path`, if it is a string.
|
||||
pub fn get_str(&self, path: &str) -> Option<&str> {
|
||||
self.map.get(path).and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Whether this flattened JSON contains an `m.mentions` property under the `content` property.
|
||||
pub fn contains_mentions(&self) -> bool {
|
||||
self.map
|
||||
.keys()
|
||||
.any(|s| s == r"content.m\.mentions" || s.starts_with(r"content.m\.mentions."))
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape a key for path matching.
|
||||
///
|
||||
/// This escapes the dots (`.`) and backslashes (`\`) in the key with a backslash.
|
||||
fn escape_key(key: &str) -> String {
|
||||
key.replace('\\', r"\\").replace('.', r"\.")
|
||||
}
|
||||
|
||||
/// The set of possible errors when converting to a JSON subset.
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
enum IntoJsonSubsetError {
|
||||
/// The numeric value failed conversion to js_int::Int.
|
||||
#[error("number found is not a valid `js_int::Int`")]
|
||||
IntConvert,
|
||||
|
||||
/// The JSON type is not accepted in this subset.
|
||||
#[error("JSON type is not accepted in this subset")]
|
||||
NotInSubset,
|
||||
}
|
||||
|
||||
/// Scalar (non-compound) JSON values.
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
pub enum ScalarJsonValue {
|
||||
/// Represents a `null` value.
|
||||
#[default]
|
||||
Null,
|
||||
|
||||
/// Represents a boolean.
|
||||
Bool(bool),
|
||||
|
||||
/// Represents an integer.
|
||||
Integer(Int),
|
||||
|
||||
/// Represents a string.
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl ScalarJsonValue {
|
||||
fn try_from_json_value(val: JsonValue) -> Result<Self, IntoJsonSubsetError> {
|
||||
Ok(match val {
|
||||
JsonValue::Bool(b) => Self::Bool(b),
|
||||
JsonValue::Number(num) => Self::Integer(
|
||||
Int::try_from(num.as_i64().ok_or(IntoJsonSubsetError::IntConvert)?)
|
||||
.map_err(|_| IntoJsonSubsetError::IntConvert)?,
|
||||
),
|
||||
JsonValue::String(string) => Self::String(string),
|
||||
JsonValue::Null => Self::Null,
|
||||
_ => Err(IntoJsonSubsetError::NotInSubset)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// If the `ScalarJsonValue` is a `Bool`, return the inner value.
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
as_variant!(self, Self::Bool).copied()
|
||||
}
|
||||
|
||||
/// If the `ScalarJsonValue` is an `Integer`, return the inner value.
|
||||
pub fn as_integer(&self) -> Option<Int> {
|
||||
as_variant!(self, Self::Integer).copied()
|
||||
}
|
||||
|
||||
/// If the `ScalarJsonValue` is a `String`, return a reference to the inner value.
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
as_variant!(self, Self::String)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ScalarJsonValue {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Null => serializer.serialize_unit(),
|
||||
Self::Bool(b) => serializer.serialize_bool(*b),
|
||||
Self::Integer(n) => n.serialize(serializer),
|
||||
Self::String(s) => serializer.serialize_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ScalarJsonValue {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let val = JsonValue::deserialize(deserializer)?;
|
||||
ScalarJsonValue::try_from_json_value(val).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for ScalarJsonValue {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Int> for ScalarJsonValue {
|
||||
fn from(value: Int) -> Self {
|
||||
Self::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ScalarJsonValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ScalarJsonValue {
|
||||
fn from(value: &str) -> Self {
|
||||
value.to_owned().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FlattenedJsonValue> for ScalarJsonValue {
|
||||
fn eq(&self, other: &FlattenedJsonValue) -> bool {
|
||||
match self {
|
||||
Self::Null => *other == FlattenedJsonValue::Null,
|
||||
Self::Bool(b) => other.as_bool() == Some(*b),
|
||||
Self::Integer(i) => other.as_integer() == Some(*i),
|
||||
Self::String(s) => other.as_str() == Some(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible JSON values after an object is flattened.
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
pub enum FlattenedJsonValue {
|
||||
/// Represents a `null` value.
|
||||
#[default]
|
||||
Null,
|
||||
|
||||
/// Represents a boolean.
|
||||
Bool(bool),
|
||||
|
||||
/// Represents an integer.
|
||||
Integer(Int),
|
||||
|
||||
/// Represents a string.
|
||||
String(String),
|
||||
|
||||
/// Represents an array.
|
||||
Array(Vec<ScalarJsonValue>),
|
||||
|
||||
/// Represents an empty object.
|
||||
EmptyObject,
|
||||
}
|
||||
|
||||
impl FlattenedJsonValue {
|
||||
fn from_json_value(val: JsonValue) -> Option<Self> {
|
||||
Some(match val {
|
||||
JsonValue::Bool(b) => Self::Bool(b),
|
||||
JsonValue::Number(num) => Self::Integer(Int::try_from(num.as_i64()?).ok()?),
|
||||
JsonValue::String(string) => Self::String(string),
|
||||
JsonValue::Null => Self::Null,
|
||||
JsonValue::Array(vec) => Self::Array(
|
||||
// Drop values we don't need instead of throwing an error.
|
||||
vec.into_iter()
|
||||
.filter_map(|v| ScalarJsonValue::try_from_json_value(v).ok())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
_ => None?,
|
||||
})
|
||||
}
|
||||
|
||||
/// If the `FlattenedJsonValue` is a `Bool`, return the inner value.
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
as_variant!(self, Self::Bool).copied()
|
||||
}
|
||||
|
||||
/// If the `FlattenedJsonValue` is an `Integer`, return the inner value.
|
||||
pub fn as_integer(&self) -> Option<Int> {
|
||||
as_variant!(self, Self::Integer).copied()
|
||||
}
|
||||
|
||||
/// If the `FlattenedJsonValue` is a `String`, return a reference to the inner value.
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
as_variant!(self, Self::String)
|
||||
}
|
||||
|
||||
/// If the `FlattenedJsonValue` is an `Array`, return a reference to the inner value.
|
||||
pub fn as_array(&self) -> Option<&[ScalarJsonValue]> {
|
||||
as_variant!(self, Self::Array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for FlattenedJsonValue {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Int> for FlattenedJsonValue {
|
||||
fn from(value: Int) -> Self {
|
||||
Self::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for FlattenedJsonValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for FlattenedJsonValue {
|
||||
fn from(value: &str) -> Self {
|
||||
value.to_owned().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ScalarJsonValue>> for FlattenedJsonValue {
|
||||
fn from(value: Vec<ScalarJsonValue>) -> Self {
|
||||
Self::Array(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ScalarJsonValue> for FlattenedJsonValue {
|
||||
fn eq(&self, other: &ScalarJsonValue) -> bool {
|
||||
match self {
|
||||
Self::Null => *other == ScalarJsonValue::Null,
|
||||
Self::Bool(b) => other.as_bool() == Some(*b),
|
||||
Self::Integer(i) => other.as_integer() == Some(*i),
|
||||
Self::String(s) => other.as_str() == Some(s),
|
||||
Self::Array(_) | Self::EmptyObject => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use js_int::int;
|
||||
use maplit::btreemap;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{FlattenedJson, FlattenedJsonValue};
|
||||
use crate::serde::Raw;
|
||||
|
||||
#[test]
|
||||
fn flattened_json_values() {
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"string": "Hello World",
|
||||
"number": 10,
|
||||
"array": [1, 2],
|
||||
"boolean": true,
|
||||
"null": null,
|
||||
"empty_object": {}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let flattened = FlattenedJson::from_raw(&raw);
|
||||
assert_eq!(
|
||||
flattened.map,
|
||||
btreemap! {
|
||||
"string".into() => "Hello World".into(),
|
||||
"number".into() => int!(10).into(),
|
||||
"array".into() => vec![int!(1).into(), int!(2).into()].into(),
|
||||
"boolean".into() => true.into(),
|
||||
"null".into() => FlattenedJsonValue::Null,
|
||||
"empty_object".into() => FlattenedJsonValue::EmptyObject,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattened_json_nested() {
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"desc": "Level 0",
|
||||
"desc.bis": "Level 0 bis",
|
||||
"up": {
|
||||
"desc": 1,
|
||||
"desc.bis": null,
|
||||
"up": {
|
||||
"desc": ["Level 2a", "Level 2b"],
|
||||
"desc\\bis": true
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let flattened = FlattenedJson::from_raw(&raw);
|
||||
assert_eq!(
|
||||
flattened.map,
|
||||
btreemap! {
|
||||
"desc".into() => "Level 0".into(),
|
||||
r"desc\.bis".into() => "Level 0 bis".into(),
|
||||
"up.desc".into() => int!(1).into(),
|
||||
r"up.desc\.bis".into() => FlattenedJsonValue::Null,
|
||||
"up.up.desc".into() => vec!["Level 2a".into(), "Level 2b".into()].into(),
|
||||
r"up.up.desc\\bis".into() => true.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_mentions() {
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"m.mentions": {},
|
||||
"content": {
|
||||
"body": "Text"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let flattened = FlattenedJson::from_raw(&raw);
|
||||
assert!(!flattened.contains_mentions());
|
||||
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"content": {
|
||||
"body": "Text",
|
||||
"m.mentions": {}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let flattened = FlattenedJson::from_raw(&raw);
|
||||
assert!(flattened.contains_mentions());
|
||||
|
||||
let raw = serde_json::from_str::<Raw<JsonValue>>(
|
||||
r#"{
|
||||
"content": {
|
||||
"body": "Text",
|
||||
"m.mentions": {
|
||||
"room": true
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let flattened = FlattenedJson::from_raw(&raw);
|
||||
assert!(flattened.contains_mentions());
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
use serde::{de, Deserialize, Serialize, Serializer};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
use super::RoomVersionFeature;
|
||||
use super::{PushCondition, RoomMemberCountIs, ScalarJsonValue};
|
||||
use crate::serde::from_raw_json_value;
|
||||
|
||||
impl Serialize for PushCondition {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
PushCondition::_Custom(custom) => custom.serialize(serializer),
|
||||
_ => PushConditionSerDeHelper::from(self.clone()).serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PushCondition {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
let ExtractKind { kind } = from_raw_json_value(&json)?;
|
||||
|
||||
match kind.as_ref() {
|
||||
"event_match"
|
||||
| "contains_display_name"
|
||||
| "room_member_count"
|
||||
| "sender_notification_permission"
|
||||
| "event_property_is"
|
||||
| "event_property_contains" => {
|
||||
let helper: PushConditionSerDeHelper = from_raw_json_value(&json)?;
|
||||
Ok(helper.into())
|
||||
}
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
"org.matrix.msc3931.room_version_supports" => {
|
||||
let helper: PushConditionSerDeHelper = from_raw_json_value(&json)?;
|
||||
Ok(helper.into())
|
||||
}
|
||||
_ => from_raw_json_value(&json).map(Self::_Custom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtractKind {
|
||||
kind: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
enum PushConditionSerDeHelper {
|
||||
/// A glob pattern match on a field of the event.
|
||||
EventMatch {
|
||||
/// The dot-separated field of the event to match.
|
||||
key: String,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Patterns with no special glob characters should be treated as having asterisks
|
||||
/// prepended and appended when testing the condition.
|
||||
pattern: String,
|
||||
},
|
||||
|
||||
/// Matches unencrypted messages where `content.body` contains the owner's display name in that
|
||||
/// room.
|
||||
ContainsDisplayName,
|
||||
|
||||
/// Matches the current number of members in the room.
|
||||
RoomMemberCount {
|
||||
/// The condition on the current number of members in the room.
|
||||
is: RoomMemberCountIs,
|
||||
},
|
||||
|
||||
/// Takes into account the current power levels in the room, ensuring the sender of the event
|
||||
/// has high enough power to trigger the notification.
|
||||
SenderNotificationPermission {
|
||||
/// The field in the power level event the user needs a minimum power level for.
|
||||
///
|
||||
/// Fields must be specified under the `notifications` property in the power level event's
|
||||
/// `content`.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Apply the rule only to rooms that support a given feature.
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
#[serde(rename = "org.matrix.msc3931.room_version_supports")]
|
||||
RoomVersionSupports {
|
||||
/// The feature the room must support for the push rule to apply.
|
||||
feature: RoomVersionFeature,
|
||||
},
|
||||
|
||||
EventPropertyIs {
|
||||
key: String,
|
||||
value: ScalarJsonValue,
|
||||
},
|
||||
|
||||
EventPropertyContains {
|
||||
key: String,
|
||||
value: ScalarJsonValue,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<PushConditionSerDeHelper> for PushCondition {
|
||||
fn from(value: PushConditionSerDeHelper) -> Self {
|
||||
match value {
|
||||
PushConditionSerDeHelper::EventMatch { key, pattern } => {
|
||||
Self::EventMatch { key, pattern }
|
||||
}
|
||||
PushConditionSerDeHelper::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushConditionSerDeHelper::RoomMemberCount { is } => Self::RoomMemberCount { is },
|
||||
PushConditionSerDeHelper::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
PushConditionSerDeHelper::RoomVersionSupports { feature } => {
|
||||
Self::RoomVersionSupports { feature }
|
||||
}
|
||||
PushConditionSerDeHelper::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value }
|
||||
}
|
||||
PushConditionSerDeHelper::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushCondition> for PushConditionSerDeHelper {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { is } => Self::RoomMemberCount { is },
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
#[cfg(feature = "unstable-msc3931")]
|
||||
PushCondition::RoomVersionSupports { feature } => Self::RoomVersionSupports { feature },
|
||||
PushCondition::EventPropertyIs { key, value } => Self::EventPropertyIs { key, value },
|
||||
PushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value }
|
||||
}
|
||||
PushCondition::_Custom(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
use std::{
|
||||
fmt,
|
||||
ops::{Bound, RangeBounds, RangeFrom, RangeTo, RangeToInclusive},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use js_int::UInt;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// One of `==`, `<`, `>`, `>=` or `<=`.
|
||||
///
|
||||
/// Used by `RoomMemberCountIs`. Defaults to `==`.
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
pub enum ComparisonOperator {
|
||||
/// Equals
|
||||
#[default]
|
||||
Eq,
|
||||
|
||||
/// Less than
|
||||
Lt,
|
||||
|
||||
/// Greater than
|
||||
Gt,
|
||||
|
||||
/// Greater or equal
|
||||
Ge,
|
||||
|
||||
/// Less or equal
|
||||
Le,
|
||||
}
|
||||
|
||||
/// A decimal integer optionally prefixed by one of `==`, `<`, `>`, `>=` or `<=`.
|
||||
///
|
||||
/// A prefix of `<` matches rooms where the member count is strictly less than the given
|
||||
/// number and so forth. If no prefix is present, this parameter defaults to `==`.
|
||||
///
|
||||
/// Can be constructed from a number or a range:
|
||||
/// ```
|
||||
/// use js_int::uint;
|
||||
/// use ruma_common::push::RoomMemberCountIs;
|
||||
///
|
||||
/// // equivalent to `is: "3"` or `is: "==3"`
|
||||
/// let exact = RoomMemberCountIs::from(uint!(3));
|
||||
///
|
||||
/// // equivalent to `is: ">=3"`
|
||||
/// let greater_or_equal = RoomMemberCountIs::from(uint!(3)..);
|
||||
///
|
||||
/// // equivalent to `is: "<3"`
|
||||
/// let less = RoomMemberCountIs::from(..uint!(3));
|
||||
///
|
||||
/// // equivalent to `is: "<=3"`
|
||||
/// let less_or_equal = RoomMemberCountIs::from(..=uint!(3));
|
||||
///
|
||||
/// // An exclusive range can be constructed with `RoomMemberCountIs::gt`:
|
||||
/// // (equivalent to `is: ">3"`)
|
||||
/// let greater = RoomMemberCountIs::gt(uint!(3));
|
||||
/// ```
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
pub struct RoomMemberCountIs {
|
||||
/// One of `==`, `<`, `>`, `>=`, `<=`, or no prefix.
|
||||
pub prefix: ComparisonOperator,
|
||||
|
||||
/// The number of people in the room.
|
||||
pub count: UInt,
|
||||
}
|
||||
|
||||
impl RoomMemberCountIs {
|
||||
/// Creates an instance of `RoomMemberCount` equivalent to `<X`,
|
||||
/// where X is the specified member count.
|
||||
pub fn gt(count: UInt) -> Self {
|
||||
RoomMemberCountIs { prefix: ComparisonOperator::Gt, count }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UInt> for RoomMemberCountIs {
|
||||
fn from(x: UInt) -> Self {
|
||||
RoomMemberCountIs { prefix: ComparisonOperator::Eq, count: x }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeFrom<UInt>> for RoomMemberCountIs {
|
||||
fn from(x: RangeFrom<UInt>) -> Self {
|
||||
RoomMemberCountIs { prefix: ComparisonOperator::Ge, count: x.start }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeTo<UInt>> for RoomMemberCountIs {
|
||||
fn from(x: RangeTo<UInt>) -> Self {
|
||||
RoomMemberCountIs { prefix: ComparisonOperator::Lt, count: x.end }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeToInclusive<UInt>> for RoomMemberCountIs {
|
||||
fn from(x: RangeToInclusive<UInt>) -> Self {
|
||||
RoomMemberCountIs { prefix: ComparisonOperator::Le, count: x.end }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RoomMemberCountIs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use ComparisonOperator as Op;
|
||||
|
||||
let prefix = match self.prefix {
|
||||
Op::Eq => "",
|
||||
Op::Lt => "<",
|
||||
Op::Gt => ">",
|
||||
Op::Ge => ">=",
|
||||
Op::Le => "<=",
|
||||
};
|
||||
|
||||
write!(f, "{prefix}{}", self.count)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RoomMemberCountIs {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let s = self.to_string();
|
||||
s.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RoomMemberCountIs {
|
||||
type Err = js_int::ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use ComparisonOperator as Op;
|
||||
|
||||
let (prefix, count_str) = match s {
|
||||
s if s.starts_with("<=") => (Op::Le, &s[2..]),
|
||||
s if s.starts_with('<') => (Op::Lt, &s[1..]),
|
||||
s if s.starts_with(">=") => (Op::Ge, &s[2..]),
|
||||
s if s.starts_with('>') => (Op::Gt, &s[1..]),
|
||||
s if s.starts_with("==") => (Op::Eq, &s[2..]),
|
||||
s => (Op::Eq, s),
|
||||
};
|
||||
|
||||
Ok(RoomMemberCountIs { prefix, count: UInt::from_str(count_str)? })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RoomMemberCountIs {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = crate::serde::deserialize_cow_str(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl RangeBounds<UInt> for RoomMemberCountIs {
|
||||
fn start_bound(&self) -> Bound<&UInt> {
|
||||
use ComparisonOperator as Op;
|
||||
|
||||
match self.prefix {
|
||||
Op::Eq => Bound::Included(&self.count),
|
||||
Op::Lt | Op::Le => Bound::Unbounded,
|
||||
Op::Gt => Bound::Excluded(&self.count),
|
||||
Op::Ge => Bound::Included(&self.count),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&UInt> {
|
||||
use ComparisonOperator as Op;
|
||||
|
||||
match self.prefix {
|
||||
Op::Eq => Bound::Included(&self.count),
|
||||
Op::Gt | Op::Ge => Bound::Unbounded,
|
||||
Op::Lt => Bound::Excluded(&self.count),
|
||||
Op::Le => Bound::Included(&self.count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
use js_int::uint;
|
||||
|
||||
use super::RoomMemberCountIs;
|
||||
|
||||
#[test]
|
||||
fn eq_range_contains_its_own_count() {
|
||||
let count = uint!(2);
|
||||
let range = RoomMemberCountIs::from(count);
|
||||
|
||||
assert!(range.contains(&count));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ge_range_contains_large_number() {
|
||||
let range = RoomMemberCountIs::from(uint!(2)..);
|
||||
let large_number = uint!(9001);
|
||||
|
||||
assert!(range.contains(&large_number));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt_range_does_not_contain_initial_point() {
|
||||
let range = RoomMemberCountIs::gt(uint!(2));
|
||||
let initial_point = uint!(2);
|
||||
|
||||
assert!(!range.contains(&initial_point));
|
||||
}
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
use indexmap::set::{IntoIter as IndexSetIntoIter, Iter as IndexSetIter};
|
||||
|
||||
use super::{
|
||||
condition, Action, ConditionalPushRule, FlattenedJson, PatternedPushRule, PushConditionRoomCtx,
|
||||
Ruleset, SimplePushRule,
|
||||
};
|
||||
use crate::{OwnedRoomId, OwnedUserId};
|
||||
|
||||
/// The kinds of push rules that are available.
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum AnyPushRule {
|
||||
/// Rules that override all other kinds.
|
||||
Override(ConditionalPushRule),
|
||||
|
||||
/// Content-specific rules.
|
||||
Content(PatternedPushRule),
|
||||
|
||||
/// Room-specific rules.
|
||||
Room(SimplePushRule<OwnedRoomId>),
|
||||
|
||||
/// Sender-specific rules.
|
||||
Sender(SimplePushRule<OwnedUserId>),
|
||||
|
||||
/// Lowest priority rules.
|
||||
Underride(ConditionalPushRule),
|
||||
}
|
||||
|
||||
impl AnyPushRule {
|
||||
/// Convert `AnyPushRule` to `AnyPushRuleRef`.
|
||||
pub fn as_ref(&self) -> AnyPushRuleRef<'_> {
|
||||
match self {
|
||||
Self::Override(o) => AnyPushRuleRef::Override(o),
|
||||
Self::Content(c) => AnyPushRuleRef::Content(c),
|
||||
Self::Room(r) => AnyPushRuleRef::Room(r),
|
||||
Self::Sender(s) => AnyPushRuleRef::Sender(s),
|
||||
Self::Underride(u) => AnyPushRuleRef::Underride(u),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `enabled` flag of the push rule.
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.as_ref().enabled()
|
||||
}
|
||||
|
||||
/// Get the `actions` of the push rule.
|
||||
pub fn actions(&self) -> &[Action] {
|
||||
self.as_ref().actions()
|
||||
}
|
||||
|
||||
/// Whether an event that matches the push rule should be highlighted.
|
||||
pub fn triggers_highlight(&self) -> bool {
|
||||
self.as_ref().triggers_highlight()
|
||||
}
|
||||
|
||||
/// Whether an event that matches the push rule should trigger a notification.
|
||||
pub fn triggers_notification(&self) -> bool {
|
||||
self.as_ref().triggers_notification()
|
||||
}
|
||||
|
||||
/// The sound that should be played when an event matches the push rule, if any.
|
||||
pub fn triggers_sound(&self) -> Option<&str> {
|
||||
self.as_ref().triggers_sound()
|
||||
}
|
||||
|
||||
/// Get the `rule_id` of the push rule.
|
||||
pub fn rule_id(&self) -> &str {
|
||||
self.as_ref().rule_id()
|
||||
}
|
||||
|
||||
/// Whether the push rule is a server-default rule.
|
||||
pub fn is_server_default(&self) -> bool {
|
||||
self.as_ref().is_server_default()
|
||||
}
|
||||
|
||||
/// Check if the push rule applies to the event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The flattened JSON representation of a room message event.
|
||||
/// * `context` - The context of the room at the time of the event.
|
||||
pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
|
||||
self.as_ref().applies(event, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator type for `Ruleset`
|
||||
#[derive(Debug)]
|
||||
pub struct RulesetIntoIter {
|
||||
content: IndexSetIntoIter<PatternedPushRule>,
|
||||
override_: IndexSetIntoIter<ConditionalPushRule>,
|
||||
room: IndexSetIntoIter<SimplePushRule<OwnedRoomId>>,
|
||||
sender: IndexSetIntoIter<SimplePushRule<OwnedUserId>>,
|
||||
underride: IndexSetIntoIter<ConditionalPushRule>,
|
||||
}
|
||||
|
||||
impl Iterator for RulesetIntoIter {
|
||||
type Item = AnyPushRule;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.override_
|
||||
.next()
|
||||
.map(AnyPushRule::Override)
|
||||
.or_else(|| self.content.next().map(AnyPushRule::Content))
|
||||
.or_else(|| self.room.next().map(AnyPushRule::Room))
|
||||
.or_else(|| self.sender.next().map(AnyPushRule::Sender))
|
||||
.or_else(|| self.underride.next().map(AnyPushRule::Underride))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Ruleset {
|
||||
type Item = AnyPushRule;
|
||||
type IntoIter = RulesetIntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
RulesetIntoIter {
|
||||
content: self.content.into_iter(),
|
||||
override_: self.override_.into_iter(),
|
||||
room: self.room.into_iter(),
|
||||
sender: self.sender.into_iter(),
|
||||
underride: self.underride.into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference to any kind of push rule.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum AnyPushRuleRef<'a> {
|
||||
/// Rules that override all other kinds.
|
||||
Override(&'a ConditionalPushRule),
|
||||
|
||||
/// Content-specific rules.
|
||||
Content(&'a PatternedPushRule),
|
||||
|
||||
/// Room-specific rules.
|
||||
Room(&'a SimplePushRule<OwnedRoomId>),
|
||||
|
||||
/// Sender-specific rules.
|
||||
Sender(&'a SimplePushRule<OwnedUserId>),
|
||||
|
||||
/// Lowest priority rules.
|
||||
Underride(&'a ConditionalPushRule),
|
||||
}
|
||||
|
||||
impl<'a> AnyPushRuleRef<'a> {
|
||||
/// Convert `AnyPushRuleRef` to `AnyPushRule` by cloning the inner value.
|
||||
pub fn to_owned(self) -> AnyPushRule {
|
||||
match self {
|
||||
Self::Override(o) => AnyPushRule::Override(o.clone()),
|
||||
Self::Content(c) => AnyPushRule::Content(c.clone()),
|
||||
Self::Room(r) => AnyPushRule::Room(r.clone()),
|
||||
Self::Sender(s) => AnyPushRule::Sender(s.clone()),
|
||||
Self::Underride(u) => AnyPushRule::Underride(u.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `enabled` flag of the push rule.
|
||||
pub fn enabled(self) -> bool {
|
||||
match self {
|
||||
Self::Override(rule) => rule.enabled,
|
||||
Self::Underride(rule) => rule.enabled,
|
||||
Self::Content(rule) => rule.enabled,
|
||||
Self::Room(rule) => rule.enabled,
|
||||
Self::Sender(rule) => rule.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `actions` of the push rule.
|
||||
pub fn actions(self) -> &'a [Action] {
|
||||
match self {
|
||||
Self::Override(rule) => &rule.actions,
|
||||
Self::Underride(rule) => &rule.actions,
|
||||
Self::Content(rule) => &rule.actions,
|
||||
Self::Room(rule) => &rule.actions,
|
||||
Self::Sender(rule) => &rule.actions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an event that matches the push rule should be highlighted.
|
||||
pub fn triggers_highlight(self) -> bool {
|
||||
self.actions().iter().any(|a| a.is_highlight())
|
||||
}
|
||||
|
||||
/// Whether an event that matches the push rule should trigger a notification.
|
||||
pub fn triggers_notification(self) -> bool {
|
||||
self.actions().iter().any(|a| a.should_notify())
|
||||
}
|
||||
|
||||
/// The sound that should be played when an event matches the push rule, if any.
|
||||
pub fn triggers_sound(self) -> Option<&'a str> {
|
||||
self.actions().iter().find_map(|a| a.sound())
|
||||
}
|
||||
|
||||
/// Get the `rule_id` of the push rule.
|
||||
pub fn rule_id(self) -> &'a str {
|
||||
match self {
|
||||
Self::Override(rule) => &rule.rule_id,
|
||||
Self::Underride(rule) => &rule.rule_id,
|
||||
Self::Content(rule) => &rule.rule_id,
|
||||
Self::Room(rule) => rule.rule_id.as_ref(),
|
||||
Self::Sender(rule) => rule.rule_id.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the push rule is a server-default rule.
|
||||
pub fn is_server_default(self) -> bool {
|
||||
match self {
|
||||
Self::Override(rule) => rule.default,
|
||||
Self::Underride(rule) => rule.default,
|
||||
Self::Content(rule) => rule.default,
|
||||
Self::Room(rule) => rule.default,
|
||||
Self::Sender(rule) => rule.default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the push rule applies to the event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The flattened JSON representation of a room message event.
|
||||
/// * `context` - The context of the room at the time of the event.
|
||||
pub fn applies(self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
|
||||
if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Override(rule) => rule.applies(event, context),
|
||||
Self::Underride(rule) => rule.applies(event, context),
|
||||
Self::Content(rule) => rule.applies_to("content.body", event, context),
|
||||
Self::Room(rule) => {
|
||||
rule.enabled
|
||||
&& condition::check_event_match(
|
||||
event,
|
||||
"room_id",
|
||||
rule.rule_id.as_ref(),
|
||||
context,
|
||||
)
|
||||
}
|
||||
Self::Sender(rule) => {
|
||||
rule.enabled
|
||||
&& condition::check_event_match(event, "sender", rule.rule_id.as_ref(), context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator type for `Ruleset`
|
||||
#[derive(Debug)]
|
||||
pub struct RulesetIter<'a> {
|
||||
content: IndexSetIter<'a, PatternedPushRule>,
|
||||
override_: IndexSetIter<'a, ConditionalPushRule>,
|
||||
room: IndexSetIter<'a, SimplePushRule<OwnedRoomId>>,
|
||||
sender: IndexSetIter<'a, SimplePushRule<OwnedUserId>>,
|
||||
underride: IndexSetIter<'a, ConditionalPushRule>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RulesetIter<'a> {
|
||||
type Item = AnyPushRuleRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.override_
|
||||
.next()
|
||||
.map(AnyPushRuleRef::Override)
|
||||
.or_else(|| self.content.next().map(AnyPushRuleRef::Content))
|
||||
.or_else(|| self.room.next().map(AnyPushRuleRef::Room))
|
||||
.or_else(|| self.sender.next().map(AnyPushRuleRef::Sender))
|
||||
.or_else(|| self.underride.next().map(AnyPushRuleRef::Underride))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Ruleset {
|
||||
type Item = AnyPushRuleRef<'a>;
|
||||
type IntoIter = RulesetIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
RulesetIter {
|
||||
content: self.content.iter(),
|
||||
override_: self.override_.iter(),
|
||||
room: self.room.iter(),
|
||||
sender: self.sender.iter(),
|
||||
underride: self.underride.iter(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,761 +0,0 @@
|
|||
//! Constructors for [predefined push rules].
|
||||
//!
|
||||
//! [predefined push rules]: https://spec.matrix.org/latest/client-server-api/#predefined-rules
|
||||
|
||||
use ruma_macros::StringEnum;
|
||||
|
||||
use super::{
|
||||
Action::*, ConditionalPushRule, PatternedPushRule, PushCondition::*, RoomMemberCountIs,
|
||||
Ruleset, Tweak,
|
||||
};
|
||||
use crate::{PrivOwnedStr, UserId};
|
||||
|
||||
impl Ruleset {
|
||||
/// The list of all [predefined push rules].
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `user_id`: the user for which to generate the default rules. Some rules depend on the
|
||||
/// user's ID (for instance those to send notifications when they are mentioned).
|
||||
///
|
||||
/// [predefined push rules]: https://spec.matrix.org/latest/client-server-api/#predefined-rules
|
||||
pub fn server_default(user_id: &UserId) -> Self {
|
||||
Self {
|
||||
content: [
|
||||
#[allow(deprecated)]
|
||||
PatternedPushRule::contains_user_name(user_id),
|
||||
]
|
||||
.into(),
|
||||
override_: [
|
||||
ConditionalPushRule::master(),
|
||||
ConditionalPushRule::suppress_notices(),
|
||||
ConditionalPushRule::invite_for_me(user_id),
|
||||
ConditionalPushRule::member_event(),
|
||||
ConditionalPushRule::is_user_mention(user_id),
|
||||
#[allow(deprecated)]
|
||||
ConditionalPushRule::contains_display_name(),
|
||||
ConditionalPushRule::is_room_mention(),
|
||||
#[allow(deprecated)]
|
||||
ConditionalPushRule::roomnotif(),
|
||||
ConditionalPushRule::tombstone(),
|
||||
ConditionalPushRule::reaction(),
|
||||
ConditionalPushRule::server_acl(),
|
||||
ConditionalPushRule::suppress_edits(),
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
ConditionalPushRule::poll_response(),
|
||||
]
|
||||
.into(),
|
||||
underride: [
|
||||
ConditionalPushRule::call(),
|
||||
ConditionalPushRule::encrypted_room_one_to_one(),
|
||||
ConditionalPushRule::room_one_to_one(),
|
||||
ConditionalPushRule::message(),
|
||||
ConditionalPushRule::encrypted(),
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
ConditionalPushRule::poll_start_one_to_one(),
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
ConditionalPushRule::poll_start(),
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
ConditionalPushRule::poll_end_one_to_one(),
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
ConditionalPushRule::poll_end(),
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Update this ruleset with the given server-default push rules.
|
||||
///
|
||||
/// This will replace the server-default rules in this ruleset (with `default` set to `true`)
|
||||
/// with the given ones while keeping the `enabled` and `actions` fields in the same state.
|
||||
///
|
||||
/// The default rules in this ruleset that are not in the new server-default rules are removed.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `server_default`: the new server-default push rules. This ruleset must not contain
|
||||
/// non-default rules.
|
||||
pub fn update_with_server_default(&mut self, mut new_server_default: Ruleset) {
|
||||
// Copy the default rules states from the old rules to the new rules and remove the
|
||||
// server-default rules from the old rules.
|
||||
macro_rules! copy_rules_state {
|
||||
($new_ruleset:ident, $old_ruleset:ident, @fields $($field_name:ident),+) => {
|
||||
$(
|
||||
$new_ruleset.$field_name = $new_ruleset
|
||||
.$field_name
|
||||
.into_iter()
|
||||
.map(|mut new_rule| {
|
||||
if let Some(old_rule) =
|
||||
$old_ruleset.$field_name.take(new_rule.rule_id.as_str())
|
||||
{
|
||||
new_rule.enabled = old_rule.enabled;
|
||||
new_rule.actions = old_rule.actions;
|
||||
}
|
||||
|
||||
new_rule
|
||||
})
|
||||
.collect();
|
||||
)+
|
||||
};
|
||||
}
|
||||
copy_rules_state!(new_server_default, self, @fields override_, content, room, sender, underride);
|
||||
|
||||
// Remove the remaining server-default rules from the old rules.
|
||||
macro_rules! remove_remaining_default_rules {
|
||||
($ruleset:ident, @fields $($field_name:ident),+) => {
|
||||
$(
|
||||
$ruleset.$field_name.retain(|rule| !rule.default);
|
||||
)+
|
||||
};
|
||||
}
|
||||
remove_remaining_default_rules!(self, @fields override_, content, room, sender, underride);
|
||||
|
||||
// `.m.rule.master` comes before all other push rules, while the other server-default push
|
||||
// rules come after.
|
||||
if let Some(master_rule) =
|
||||
new_server_default.override_.take(PredefinedOverrideRuleId::Master.as_str())
|
||||
{
|
||||
let (pos, _) = self.override_.insert_full(master_rule);
|
||||
self.override_.move_index(pos, 0);
|
||||
}
|
||||
|
||||
// Merge the new server-default rules into the old rules.
|
||||
macro_rules! merge_rules {
|
||||
($old_ruleset:ident, $new_ruleset:ident, @fields $($field_name:ident),+) => {
|
||||
$(
|
||||
$old_ruleset.$field_name.extend($new_ruleset.$field_name);
|
||||
)+
|
||||
};
|
||||
}
|
||||
merge_rules!(self, new_server_default, @fields override_, content, room, sender, underride);
|
||||
}
|
||||
}
|
||||
|
||||
/// Default override push rules
|
||||
impl ConditionalPushRule {
|
||||
/// Matches all events, this can be enabled to turn off all push notifications other than those
|
||||
/// generated by override rules set by the user.
|
||||
pub fn master() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: false,
|
||||
rule_id: PredefinedOverrideRuleId::Master.to_string(),
|
||||
conditions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches messages with a `msgtype` of `notice`.
|
||||
pub fn suppress_notices() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::SuppressNotices.to_string(),
|
||||
conditions: vec![EventMatch {
|
||||
key: "content.msgtype".into(),
|
||||
pattern: "m.notice".into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any invites to a new room for this user.
|
||||
pub fn invite_for_me(user_id: &UserId) -> Self {
|
||||
Self {
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".into())),
|
||||
SetTweak(Tweak::Highlight(false)),
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::InviteForMe.to_string(),
|
||||
conditions: vec![
|
||||
EventMatch { key: "type".into(), pattern: "m.room.member".into() },
|
||||
EventMatch { key: "content.membership".into(), pattern: "invite".into() },
|
||||
EventMatch { key: "state_key".into(), pattern: user_id.to_string() },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any `m.room.member_event`.
|
||||
pub fn member_event() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::MemberEvent.to_string(),
|
||||
conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.member".into() }],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any message which contains the user’s Matrix ID in the list of `user_ids` under the
|
||||
/// `m.mentions` property.
|
||||
pub fn is_user_mention(user_id: &UserId) -> Self {
|
||||
Self {
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".to_owned())),
|
||||
SetTweak(Tweak::Highlight(true)),
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::IsUserMention.to_string(),
|
||||
conditions: vec![EventPropertyContains {
|
||||
key: r"content.m\.mentions.user_ids".to_owned(),
|
||||
value: user_id.as_str().into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any message whose content is unencrypted and contains the user's current display
|
||||
/// name in the room in which it was sent.
|
||||
///
|
||||
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
|
||||
/// `m.mentions` property.
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_user_mention() instead."]
|
||||
pub fn contains_display_name() -> Self {
|
||||
#[allow(deprecated)]
|
||||
Self {
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".into())),
|
||||
SetTweak(Tweak::Highlight(true)),
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(),
|
||||
conditions: vec![ContainsDisplayName],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any state event whose type is `m.room.tombstone`. This
|
||||
/// is intended to notify users of a room when it is upgraded,
|
||||
/// similar to what an `@room` notification would accomplish.
|
||||
pub fn tombstone() -> Self {
|
||||
Self {
|
||||
actions: vec![Notify, SetTweak(Tweak::Highlight(true))],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::Tombstone.to_string(),
|
||||
conditions: vec![
|
||||
EventMatch { key: "type".into(), pattern: "m.room.tombstone".into() },
|
||||
EventMatch { key: "state_key".into(), pattern: "".into() },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any message from a sender with the proper power level with the `room` property of
|
||||
/// the `m.mentions` property set to `true`.
|
||||
pub fn is_room_mention() -> Self {
|
||||
Self {
|
||||
actions: vec![Notify, SetTweak(Tweak::Highlight(true))],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::IsRoomMention.to_string(),
|
||||
conditions: vec![
|
||||
EventPropertyIs { key: r"content.m\.mentions.room".to_owned(), value: true.into() },
|
||||
SenderNotificationPermission { key: "room".to_owned() },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any message whose content is unencrypted and contains the text `@room`, signifying
|
||||
/// the whole room should be notified of the event.
|
||||
///
|
||||
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
|
||||
/// `m.mentions` property.
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_room_mention() instead."]
|
||||
pub fn roomnotif() -> Self {
|
||||
#[allow(deprecated)]
|
||||
Self {
|
||||
actions: vec![Notify, SetTweak(Tweak::Highlight(true))],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(),
|
||||
conditions: vec![
|
||||
EventMatch { key: "content.body".into(), pattern: "@room".into() },
|
||||
SenderNotificationPermission { key: "room".into() },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches [reactions] to a message.
|
||||
///
|
||||
/// [reactions]: https://spec.matrix.org/latest/client-server-api/#event-annotations-and-reactions
|
||||
pub fn reaction() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::Reaction.to_string(),
|
||||
conditions: vec![EventMatch { key: "type".into(), pattern: "m.reaction".into() }],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches [room server ACLs].
|
||||
///
|
||||
/// [room server ACLs]: https://spec.matrix.org/latest/client-server-api/#server-access-control-lists-acls-for-rooms
|
||||
pub fn server_acl() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::RoomServerAcl.to_string(),
|
||||
conditions: vec![
|
||||
EventMatch { key: "type".into(), pattern: "m.room.server_acl".into() },
|
||||
EventMatch { key: "state_key".into(), pattern: "".into() },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches [event replacements].
|
||||
///
|
||||
/// [event replacements]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
||||
pub fn suppress_edits() -> Self {
|
||||
Self {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: PredefinedOverrideRuleId::SuppressEdits.to_string(),
|
||||
conditions: vec![EventPropertyIs {
|
||||
key: r"content.m\.relates_to.rel_type".to_owned(),
|
||||
value: "m.replace".into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a poll response event sent in any room.
|
||||
///
|
||||
/// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
|
||||
///
|
||||
/// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
pub fn poll_response() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedOverrideRuleId::PollResponse.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventPropertyIs {
|
||||
key: "type".to_owned(),
|
||||
value: "org.matrix.msc3381.poll.response".into(),
|
||||
}],
|
||||
actions: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default content push rules
|
||||
impl PatternedPushRule {
|
||||
/// Matches any message whose content is unencrypted and contains the local part of the user's
|
||||
/// Matrix ID, separated by word boundaries.
|
||||
///
|
||||
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
|
||||
/// `m.mentions` property.
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_user_mention() instead."]
|
||||
pub fn contains_user_name(user_id: &UserId) -> Self {
|
||||
#[allow(deprecated)]
|
||||
Self {
|
||||
rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
|
||||
enabled: true,
|
||||
default: true,
|
||||
pattern: user_id.localpart().into(),
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".into())),
|
||||
SetTweak(Tweak::Highlight(true)),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default underrides push rules
|
||||
impl ConditionalPushRule {
|
||||
/// Matches any incoming VOIP call.
|
||||
pub fn call() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::Call.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventMatch { key: "type".into(), pattern: "m.call.invite".into() }],
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("ring".into())),
|
||||
SetTweak(Tweak::Highlight(false)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any encrypted event sent in a room with exactly two members.
|
||||
///
|
||||
/// Unlike other push rules, this rule cannot be matched against the content of the event by
|
||||
/// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it
|
||||
/// either matches all events that are encrypted (in 1:1 rooms) or none.
|
||||
pub fn encrypted_room_one_to_one() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::EncryptedRoomOneToOne.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![
|
||||
RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) },
|
||||
EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() },
|
||||
],
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".into())),
|
||||
SetTweak(Tweak::Highlight(false)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any message sent in a room with exactly two members.
|
||||
pub fn room_one_to_one() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::RoomOneToOne.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![
|
||||
RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) },
|
||||
EventMatch { key: "type".into(), pattern: "m.room.message".into() },
|
||||
],
|
||||
actions: vec![
|
||||
Notify,
|
||||
SetTweak(Tweak::Sound("default".into())),
|
||||
SetTweak(Tweak::Highlight(false)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches all chat messages.
|
||||
pub fn message() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::Message.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.message".into() }],
|
||||
actions: vec![Notify, SetTweak(Tweak::Highlight(false))],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches all encrypted events.
|
||||
///
|
||||
/// Unlike other push rules, this rule cannot be matched against the content of the event by
|
||||
/// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it
|
||||
/// either matches all events that are encrypted (in group rooms) or none.
|
||||
pub fn encrypted() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::Encrypted.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }],
|
||||
actions: vec![Notify, SetTweak(Tweak::Highlight(false))],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a poll start event sent in a room with exactly two members.
|
||||
///
|
||||
/// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
|
||||
///
|
||||
/// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
pub fn poll_start_one_to_one() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::PollStartOneToOne.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![
|
||||
RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) },
|
||||
EventPropertyIs {
|
||||
key: "type".to_owned(),
|
||||
value: "org.matrix.msc3381.poll.start".into(),
|
||||
},
|
||||
],
|
||||
actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a poll start event sent in any room.
|
||||
///
|
||||
/// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
|
||||
///
|
||||
/// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
pub fn poll_start() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::PollStart.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventPropertyIs {
|
||||
key: "type".to_owned(),
|
||||
value: "org.matrix.msc3381.poll.start".into(),
|
||||
}],
|
||||
actions: vec![Notify],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a poll end event sent in a room with exactly two members.
|
||||
///
|
||||
/// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
|
||||
///
|
||||
/// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
pub fn poll_end_one_to_one() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::PollEndOneToOne.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![
|
||||
RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) },
|
||||
EventPropertyIs {
|
||||
key: "type".to_owned(),
|
||||
value: "org.matrix.msc3381.poll.end".into(),
|
||||
},
|
||||
],
|
||||
actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a poll end event sent in any room.
|
||||
///
|
||||
/// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
|
||||
///
|
||||
/// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
pub fn poll_end() -> Self {
|
||||
Self {
|
||||
rule_id: PredefinedUnderrideRuleId::PollEnd.to_string(),
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: vec![EventPropertyIs {
|
||||
key: "type".to_owned(),
|
||||
value: "org.matrix.msc3381.poll.end".into(),
|
||||
}],
|
||||
actions: vec![Notify],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule IDs of the predefined server push rules.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum PredefinedRuleId {
|
||||
/// User-configured rules that override all other kinds.
|
||||
Override(PredefinedOverrideRuleId),
|
||||
|
||||
/// Lowest priority user-defined rules.
|
||||
Underride(PredefinedUnderrideRuleId),
|
||||
|
||||
/// Content-specific rules.
|
||||
Content(PredefinedContentRuleId),
|
||||
}
|
||||
|
||||
/// The rule IDs of the predefined override server push rules.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
|
||||
#[ruma_enum(rename_all = ".m.rule.snake_case")]
|
||||
#[non_exhaustive]
|
||||
pub enum PredefinedOverrideRuleId {
|
||||
/// `.m.rule.master`
|
||||
Master,
|
||||
|
||||
/// `.m.rule.suppress_notices`
|
||||
SuppressNotices,
|
||||
|
||||
/// `.m.rule.invite_for_me`
|
||||
InviteForMe,
|
||||
|
||||
/// `.m.rule.member_event`
|
||||
MemberEvent,
|
||||
|
||||
/// `.m.rule.is_user_mention`
|
||||
IsUserMention,
|
||||
|
||||
/// `.m.rule.contains_display_name`
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
|
||||
ContainsDisplayName,
|
||||
|
||||
/// `.m.rule.is_room_mention`
|
||||
IsRoomMention,
|
||||
|
||||
/// `.m.rule.roomnotif`
|
||||
#[ruma_enum(rename = ".m.rule.roomnotif")]
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsRoomMention instead."]
|
||||
RoomNotif,
|
||||
|
||||
/// `.m.rule.tombstone`
|
||||
Tombstone,
|
||||
|
||||
/// `.m.rule.reaction`
|
||||
Reaction,
|
||||
|
||||
/// `.m.rule.room.server_acl`
|
||||
#[ruma_enum(rename = ".m.rule.room.server_acl")]
|
||||
RoomServerAcl,
|
||||
|
||||
/// `.m.rule.suppress_edits`
|
||||
SuppressEdits,
|
||||
|
||||
/// `.m.rule.poll_response`
|
||||
///
|
||||
/// This uses the unstable prefix defined in [MSC3930].
|
||||
///
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
#[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_response")]
|
||||
PollResponse,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The rule IDs of the predefined underride server push rules.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
|
||||
#[ruma_enum(rename_all = ".m.rule.snake_case")]
|
||||
#[non_exhaustive]
|
||||
pub enum PredefinedUnderrideRuleId {
|
||||
/// `.m.rule.call`
|
||||
Call,
|
||||
|
||||
/// `.m.rule.encrypted_room_one_to_one`
|
||||
EncryptedRoomOneToOne,
|
||||
|
||||
/// `.m.rule.room_one_to_one`
|
||||
RoomOneToOne,
|
||||
|
||||
/// `.m.rule.message`
|
||||
Message,
|
||||
|
||||
/// `.m.rule.encrypted`
|
||||
Encrypted,
|
||||
|
||||
/// `.m.rule.poll_start_one_to_one`
|
||||
///
|
||||
/// This uses the unstable prefix defined in [MSC3930].
|
||||
///
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
#[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_start_one_to_one")]
|
||||
PollStartOneToOne,
|
||||
|
||||
/// `.m.rule.poll_start`
|
||||
///
|
||||
/// This uses the unstable prefix defined in [MSC3930].
|
||||
///
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
#[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_start")]
|
||||
PollStart,
|
||||
|
||||
/// `.m.rule.poll_end_one_to_one`
|
||||
///
|
||||
/// This uses the unstable prefix defined in [MSC3930].
|
||||
///
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
#[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_end_one_to_one")]
|
||||
PollEndOneToOne,
|
||||
|
||||
/// `.m.rule.poll_end`
|
||||
///
|
||||
/// This uses the unstable prefix defined in [MSC3930].
|
||||
///
|
||||
/// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
|
||||
#[cfg(feature = "unstable-msc3930")]
|
||||
#[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_end")]
|
||||
PollEnd,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The rule IDs of the predefined content server push rules.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
|
||||
#[ruma_enum(rename_all = ".m.rule.snake_case")]
|
||||
#[non_exhaustive]
|
||||
pub enum PredefinedContentRuleId {
|
||||
/// `.m.rule.contains_user_name`
|
||||
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
|
||||
ContainsUserName,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_matches;
|
||||
use assign::assign;
|
||||
|
||||
use super::PredefinedOverrideRuleId;
|
||||
use crate::{
|
||||
push::{Action, ConditionalPushRule, ConditionalPushRuleInit, Ruleset},
|
||||
user_id,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn update_with_server_default() {
|
||||
let user_rule_id = "user_always_true";
|
||||
let default_rule_id = ".default_always_true";
|
||||
|
||||
let override_ = [
|
||||
// Default `.m.rule.master` push rule with non-default state.
|
||||
assign!(ConditionalPushRule::master(), { enabled: true, actions: vec![Action::Notify]}),
|
||||
// User-defined push rule.
|
||||
ConditionalPushRuleInit {
|
||||
actions: vec![],
|
||||
default: false,
|
||||
enabled: false,
|
||||
rule_id: user_rule_id.to_owned(),
|
||||
conditions: vec![],
|
||||
}
|
||||
.into(),
|
||||
// Old server-default push rule.
|
||||
ConditionalPushRuleInit {
|
||||
actions: vec![],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: default_rule_id.to_owned(),
|
||||
conditions: vec![],
|
||||
}
|
||||
.into(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut ruleset = Ruleset { override_, ..Default::default() };
|
||||
|
||||
let new_server_default = Ruleset::server_default(user_id!("@user:localhost"));
|
||||
|
||||
ruleset.update_with_server_default(new_server_default);
|
||||
|
||||
// Master rule is in first position.
|
||||
let master_rule = &ruleset.override_[0];
|
||||
assert_eq!(master_rule.rule_id, PredefinedOverrideRuleId::Master.as_str());
|
||||
|
||||
// `enabled` and `actions` have been copied from the old rules.
|
||||
assert!(master_rule.enabled);
|
||||
assert_eq!(master_rule.actions.len(), 1);
|
||||
assert_matches!(&master_rule.actions[0], Action::Notify);
|
||||
|
||||
// Non-server-default rule is still present and hasn't changed.
|
||||
let user_rule = ruleset.override_.get(user_rule_id).unwrap();
|
||||
assert!(!user_rule.enabled);
|
||||
assert_eq!(user_rule.actions.len(), 0);
|
||||
|
||||
// Old server-default rule is gone.
|
||||
assert_matches!(ruleset.override_.get(default_rule_id), None);
|
||||
|
||||
// New server-default rule is present and hasn't changed.
|
||||
let member_event_rule =
|
||||
ruleset.override_.get(PredefinedOverrideRuleId::MemberEvent.as_str()).unwrap();
|
||||
assert!(member_event_rule.enabled);
|
||||
assert_eq!(member_event_rule.actions.len(), 0);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ event_enum! {
|
|||
/// Any room account data event.
|
||||
enum RoomAccountData {
|
||||
"m.tag" => super::tag,
|
||||
"m.push_rules.override" => super::push_rules,
|
||||
}
|
||||
|
||||
/// Any ephemeral room event.
|
||||
|
|
|
@ -147,9 +147,9 @@ pub mod message;
|
|||
pub mod mixins;
|
||||
#[cfg(feature = "unstable-pdu")]
|
||||
pub mod pdu;
|
||||
pub mod push_rules;
|
||||
pub mod policy;
|
||||
pub mod presence;
|
||||
pub mod push_rules;
|
||||
pub mod reaction;
|
||||
pub mod receipt;
|
||||
pub mod relation;
|
||||
|
@ -278,6 +278,12 @@ pub struct Mentions {
|
|||
/// Defaults to `false`.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub room: bool,
|
||||
|
||||
/// Whether the thread is mentioned. Notifies anyone watching.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub thread: bool,
|
||||
}
|
||||
|
||||
impl Mentions {
|
||||
|
@ -296,9 +302,15 @@ impl Mentions {
|
|||
Self { room: true, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Create a `Mentions` for a room mention.
|
||||
pub fn with_thread_mention() -> Self {
|
||||
Self { thread: true, ..Default::default() }
|
||||
}
|
||||
|
||||
fn add(&mut self, mentions: Self) {
|
||||
self.user_ids.extend(mentions.user_ids);
|
||||
self.room |= mentions.room;
|
||||
self.thread |= mentions.thread;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
//! mixins that are added to events
|
||||
//! this will probably be removed
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use js_int::UInt;
|
||||
|
|
1
crates/ruma-events/src/push.rs
Normal file
1
crates/ruma-events/src/push.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,235 +1,23 @@
|
|||
//! Types for the [`m.push_rules`] event.
|
||||
//!
|
||||
//! [`m.push_rules`]: https://spec.matrix.org/latest/client-server-api/#mpush_rules
|
||||
//! types for push rules
|
||||
|
||||
use ruma_common::push::Ruleset;
|
||||
use ruma_common::push::PushRules;
|
||||
use ruma_macros::EventContent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// The content of an `m.push_rules` event.
|
||||
///
|
||||
/// Describes all push rules for a user.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[ruma_event(type = "m.push_rules", kind = GlobalAccountData)]
|
||||
pub struct PushRulesEventContent {
|
||||
/// The global ruleset.
|
||||
pub global: Ruleset,
|
||||
#[serde(flatten)]
|
||||
pub rules: PushRules,
|
||||
}
|
||||
|
||||
impl PushRulesEventContent {
|
||||
/// Creates a new `PushRulesEventContent` with the given global ruleset.
|
||||
///
|
||||
/// You can also construct a `PushRulesEventContent` from a global ruleset using `From` /
|
||||
/// `Into`.
|
||||
pub fn new(global: Ruleset) -> Self {
|
||||
Self { global }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ruleset> for PushRulesEventContent {
|
||||
fn from(global: Ruleset) -> Self {
|
||||
Self::new(global)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{from_value as from_json_value, json};
|
||||
|
||||
use super::PushRulesEvent;
|
||||
|
||||
#[test]
|
||||
fn sanity_check() {
|
||||
// This is a full example of a push rules event from the specification.
|
||||
let json_data = json!({
|
||||
"content": {
|
||||
"global": {
|
||||
"content": [
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"pattern": "alice",
|
||||
"rule_id": ".m.rule.contains_user_name"
|
||||
}
|
||||
],
|
||||
"override": [
|
||||
{
|
||||
"actions": [],
|
||||
"conditions": [],
|
||||
"default": true,
|
||||
"enabled": false,
|
||||
"rule_id": ".m.rule.master"
|
||||
},
|
||||
{
|
||||
"actions": [],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "content.msgtype",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.notice"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.suppress_notices"
|
||||
}
|
||||
],
|
||||
"room": [],
|
||||
"sender": [],
|
||||
"underride": [
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "ring"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "type",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.call.invite"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.call"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "contains_display_name"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.contains_display_name"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"is": "2",
|
||||
"kind": "room_member_count"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.room_one_to_one"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "type",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.room.member"
|
||||
},
|
||||
{
|
||||
"key": "content.membership",
|
||||
"kind": "event_match",
|
||||
"pattern": "invite"
|
||||
},
|
||||
{
|
||||
"key": "state_key",
|
||||
"kind": "event_match",
|
||||
"pattern": "@alice:example.com"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.invite_for_me"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "type",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.room.member"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.member_event"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "type",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.room.message"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.message"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "m.push_rules"
|
||||
});
|
||||
|
||||
from_json_value::<PushRulesEvent>(json_data).unwrap();
|
||||
}
|
||||
/// The content of an `m.push_rules` event.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[ruma_event(type = "m.push_rules.override", kind = RoomAccountData)]
|
||||
pub struct PushRulesOverrideEventContent {
|
||||
#[serde(flatten)]
|
||||
pub rules: PushRules,
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
# [unreleased]
|
||||
|
||||
# 0.8.0
|
||||
|
||||
No changes for this version
|
||||
|
||||
# 0.7.1
|
||||
|
||||
Improvements:
|
||||
|
||||
* Update links to the latest version of the Matrix spec
|
||||
|
||||
# 0.7.0
|
||||
|
||||
No changes for this version
|
||||
|
||||
# 0.6.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Remove `PartialEq` implementation for `NotificationCounts`
|
||||
|
||||
# 0.5.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Upgrade dependencies
|
||||
|
||||
# 0.4.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Upgrade dependencies
|
||||
|
||||
# 0.3.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Upgrade dependencies
|
||||
|
||||
# 0.2.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Upgrade ruma-events to 0.23.0
|
||||
|
||||
# 0.1.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Remove `Copy` implementation for `NotificationCounts` to avoid simple changes
|
||||
being breaking
|
||||
* Change `Box<RawJsonValue>` to `&RawJsonValue` in request types
|
||||
* Upgrade public dependencies
|
||||
|
||||
# 0.0.1
|
||||
|
||||
* Add endpoint `send_event_notification::v1`
|
|
@ -1,27 +1,9 @@
|
|||
[package]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.8.0"
|
||||
description = "Types for the endpoints in the Matrix push gateway API."
|
||||
homepage = "https://ruma.io/"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/ruma/ruma"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
unstable-exhaustive-types = []
|
||||
unstable-unspecified = []
|
||||
client = []
|
||||
server = []
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
js_int = { workspace = true, features = ["serde"] }
|
||||
ruma-common = { workspace = true, features = ["api"] }
|
||||
ruma-events = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
![license: MIT](https://img.shields.io/crates/l/ruma-push-gateway-api.svg)
|
||||
|
||||
**ruma-push-gateway-api** contains serializable types for the requests and responses for each endpoint in the [Matrix](https://matrix.org/) push gateway API specification.
|
||||
|
||||
It is being removed in favor of having the server itself push the notifications.
|
||||
|
|
|
@ -1,25 +1 @@
|
|||
#![doc(html_favicon_url = "https://ruma.io/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://ruma.io/images/logo.png")]
|
||||
//! (De)serializable types for the [Matrix Push Gateway API][push-api].
|
||||
//! These types can be shared by push gateway and server code.
|
||||
//!
|
||||
//! [push-api]: https://spec.matrix.org/latest/push-gateway-api/
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub mod send_event_notification;
|
||||
|
||||
// Wrapper around `Box<str>` that cannot be used in a meaningful way outside of
|
||||
// this crate. Used for string enums because their `_Custom` variant can't be
|
||||
// truly private (only `#[doc(hidden)]`).
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PrivOwnedStr(Box<str>);
|
||||
|
||||
impl fmt::Debug for PrivOwnedStr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,458 +0,0 @@
|
|||
//! `POST /_matrix/push/*/notify`
|
||||
//!
|
||||
//! Notify a push gateway about an event or update the number of unread notifications a user has.
|
||||
|
||||
pub mod v1 {
|
||||
//! `/v1/` ([spec])
|
||||
//!
|
||||
//! [spec]: https://spec.matrix.org/latest/push-gateway-api/#post_matrixpushv1notify
|
||||
|
||||
use js_int::{uint, UInt};
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
push::{PushFormat, Tweak},
|
||||
serde::StringEnum,
|
||||
OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, SecondsSinceUnixEpoch,
|
||||
};
|
||||
use ruma_events::TimelineEventType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
use serde_json::value::Value as JsonValue;
|
||||
|
||||
use crate::PrivOwnedStr;
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST,
|
||||
rate_limited: false,
|
||||
authentication: None,
|
||||
history: {
|
||||
1.0 => "/_matrix/push/v1/notify",
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `send_event_notification` endpoint.
|
||||
#[request]
|
||||
pub struct Request {
|
||||
/// Information about the push notification
|
||||
pub notification: Notification,
|
||||
}
|
||||
|
||||
/// Response type for the `send_event_notification` endpoint.
|
||||
#[response]
|
||||
#[derive(Default)]
|
||||
pub struct Response {
|
||||
/// A list of all pushkeys given in the notification request that are not valid.
|
||||
///
|
||||
/// These could have been rejected by an upstream gateway because they have expired or have
|
||||
/// never been valid. Homeservers must cease sending notification requests for these
|
||||
/// pushkeys and remove the associated pushers. It may not necessarily be the notification
|
||||
/// in the request that failed: it could be that a previous notification to the same
|
||||
/// pushkey failed. May be empty.
|
||||
pub rejected: Vec<String>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given notification.
|
||||
pub fn new(notification: Notification) -> Self {
|
||||
Self { notification }
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given list of rejected pushkeys.
|
||||
pub fn new(rejected: Vec<String>) -> Self {
|
||||
Self { rejected }
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for passing information about a push notification
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Notification {
|
||||
/// The Matrix event ID of the event being notified about.
|
||||
///
|
||||
/// Required if the notification is about a particular Matrix event. May be omitted for
|
||||
/// notifications that only contain updated badge counts. This ID can and should be used to
|
||||
/// detect duplicate notification requests.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event_id: Option<OwnedEventId>,
|
||||
|
||||
/// The ID of the room in which this event occurred.
|
||||
///
|
||||
/// Required if the notification relates to a specific Matrix event.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub room_id: Option<OwnedRoomId>,
|
||||
|
||||
/// The type of the event as in the event's `type` field.
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub event_type: Option<TimelineEventType>,
|
||||
|
||||
/// The sender of the event as in the corresponding event field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sender: Option<OwnedUserId>,
|
||||
|
||||
/// The current display name of the sender in the room in which the event occurred.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sender_display_name: Option<String>,
|
||||
|
||||
/// The name of the room in which the event occurred.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub room_name: Option<String>,
|
||||
|
||||
/// An alias to display for the room in which the event occurred.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub room_alias: Option<OwnedRoomAliasId>,
|
||||
|
||||
/// Whether the user receiving the notification is the subject of a member event (i.e. the
|
||||
/// `state_key` of the member event is equal to the user's Matrix ID).
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub user_is_target: bool,
|
||||
|
||||
/// The priority of the notification.
|
||||
///
|
||||
/// If omitted, `high` is assumed. This may be used by push gateways to deliver less
|
||||
/// time-sensitive notifications in a way that will preserve battery power on mobile
|
||||
/// devices.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub prio: NotificationPriority,
|
||||
|
||||
/// The `content` field from the event, if present.
|
||||
///
|
||||
/// The pusher may omit this if the event had no content or for any other reason.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Box<RawJsonValue>>,
|
||||
|
||||
/// Current number of unacknowledged communications for the recipient user.
|
||||
///
|
||||
/// Counts whose value is zero should be omitted.
|
||||
#[serde(default, skip_serializing_if = "NotificationCounts::is_default")]
|
||||
pub counts: NotificationCounts,
|
||||
|
||||
/// An array of devices that the notification should be sent to.
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification for the given devices.
|
||||
pub fn new(devices: Vec<Device>) -> Self {
|
||||
Notification { devices, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for passing information about notification priority.
|
||||
///
|
||||
/// This may be used by push gateways to deliver less time-sensitive
|
||||
/// notifications in a way that will preserve battery power on mobile devices.
|
||||
///
|
||||
/// This type can hold an arbitrary string. To build this with a custom value, convert it from a
|
||||
/// string with `::from()` / `.into()`. To check for values that are not available as a
|
||||
/// documented variant here, use its string representation, obtained through `.as_str()`.
|
||||
#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
|
||||
#[ruma_enum(rename_all = "snake_case")]
|
||||
#[non_exhaustive]
|
||||
pub enum NotificationPriority {
|
||||
/// A high priority notification
|
||||
#[default]
|
||||
High,
|
||||
|
||||
/// A low priority notification
|
||||
Low,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// Type for passing information about notification counts.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct NotificationCounts {
|
||||
/// The number of unread messages a user has across all of the rooms they
|
||||
/// are a member of.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub unread: UInt,
|
||||
|
||||
/// The number of unacknowledged missed calls a user has across all rooms of
|
||||
/// which they are a member.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub missed_calls: UInt,
|
||||
}
|
||||
|
||||
impl NotificationCounts {
|
||||
/// Create new notification counts from the given unread and missed call
|
||||
/// counts.
|
||||
pub fn new(unread: UInt, missed_calls: UInt) -> Self {
|
||||
NotificationCounts { unread, missed_calls }
|
||||
}
|
||||
|
||||
fn is_default(&self) -> bool {
|
||||
self.unread == uint!(0) && self.missed_calls == uint!(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for passing information about devices.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Device {
|
||||
/// The `app_id` given when the pusher was created.
|
||||
///
|
||||
/// Max length: 64 chars.
|
||||
pub app_id: String,
|
||||
|
||||
/// The `pushkey` given when the pusher was created.
|
||||
///
|
||||
/// Max length: 512 bytes.
|
||||
pub pushkey: String,
|
||||
|
||||
/// The unix timestamp (in seconds) when the pushkey was last updated.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pushkey_ts: Option<SecondsSinceUnixEpoch>,
|
||||
|
||||
/// A dictionary of additional pusher-specific data.
|
||||
#[serde(default, skip_serializing_if = "PusherData::is_empty")]
|
||||
pub data: PusherData,
|
||||
|
||||
/// A dictionary of customisations made to the way this notification is to be presented.
|
||||
///
|
||||
/// These are added by push rules.
|
||||
#[serde(with = "tweak_serde", skip_serializing_if = "Vec::is_empty")]
|
||||
pub tweaks: Vec<Tweak>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Create a new device with the given app id and pushkey
|
||||
pub fn new(app_id: String, pushkey: String) -> Self {
|
||||
Device {
|
||||
app_id,
|
||||
pushkey,
|
||||
pushkey_ts: None,
|
||||
data: PusherData::new(),
|
||||
tweaks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information for the pusher implementation itself.
|
||||
///
|
||||
/// This is the data dictionary passed in at pusher creation minus the `url` key.
|
||||
///
|
||||
/// It can be constructed from [`ruma_common::push::HttpPusherData`] with `::from()` /
|
||||
/// `.into()`.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct PusherData {
|
||||
/// The format to use when sending notifications to the Push Gateway.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<PushFormat>,
|
||||
|
||||
/// iOS (+ macOS?) specific default payload that will be sent to apple push notification
|
||||
/// service.
|
||||
///
|
||||
/// For more information, see [Sygnal docs][sygnal].
|
||||
///
|
||||
/// [sygnal]: https://github.com/matrix-org/sygnal/blob/main/docs/applications.md#ios-applications-beware
|
||||
// Not specified, issue: https://github.com/matrix-org/matrix-spec/issues/921
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
#[serde(default, skip_serializing_if = "JsonValue::is_null")]
|
||||
pub default_payload: JsonValue,
|
||||
}
|
||||
|
||||
impl PusherData {
|
||||
/// Creates an empty `PusherData`.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Returns `true` if all fields are `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
#[cfg(not(feature = "unstable-unspecified"))]
|
||||
{
|
||||
self.format.is_none()
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
{
|
||||
self.format.is_none() && self.default_payload.is_null()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma_common::push::HttpPusherData> for PusherData {
|
||||
fn from(data: ruma_common::push::HttpPusherData) -> Self {
|
||||
let ruma_common::push::HttpPusherData {
|
||||
format,
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
default_payload,
|
||||
..
|
||||
} = data;
|
||||
|
||||
Self {
|
||||
format,
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
default_payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tweak_serde {
|
||||
use std::fmt;
|
||||
|
||||
use ruma_common::push::Tweak;
|
||||
use serde::{
|
||||
de::{MapAccess, Visitor},
|
||||
ser::SerializeMap,
|
||||
Deserializer, Serializer,
|
||||
};
|
||||
|
||||
pub(super) fn serialize<S>(tweak: &[Tweak], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(tweak.len()))?;
|
||||
for item in tweak {
|
||||
#[allow(unreachable_patterns)]
|
||||
match item {
|
||||
Tweak::Highlight(b) => map.serialize_entry("highlight", b)?,
|
||||
Tweak::Sound(value) => map.serialize_entry("sound", value)?,
|
||||
Tweak::Custom { value, name } => map.serialize_entry(name, value)?,
|
||||
_ => unreachable!("variant added to Tweak not covered by Custom"),
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
|
||||
struct TweaksVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for TweaksVisitor {
|
||||
type Value = Vec<Tweak>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("List of tweaks")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut tweaks = vec![];
|
||||
while let Some(key) = access.next_key::<String>()? {
|
||||
match &*key {
|
||||
"sound" => tweaks.push(Tweak::Sound(access.next_value()?)),
|
||||
// If a highlight tweak is given with no value, its value is defined to be
|
||||
// true.
|
||||
"highlight" => {
|
||||
let highlight = if let Ok(highlight) = access.next_value() {
|
||||
highlight
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
tweaks.push(Tweak::Highlight(highlight));
|
||||
}
|
||||
_ => tweaks.push(Tweak::Custom { name: key, value: access.next_value()? }),
|
||||
};
|
||||
}
|
||||
|
||||
// If no highlight tweak is given at all then the value of highlight is defined to
|
||||
// be false.
|
||||
if !tweaks.iter().any(|tw| matches!(tw, Tweak::Highlight(_))) {
|
||||
tweaks.push(Tweak::Highlight(false));
|
||||
}
|
||||
|
||||
Ok(tweaks)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Tweak>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_map(TweaksVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use js_int::uint;
|
||||
use ruma_common::{
|
||||
owned_event_id, owned_room_alias_id, owned_room_id, owned_user_id,
|
||||
SecondsSinceUnixEpoch,
|
||||
};
|
||||
use ruma_events::TimelineEventType;
|
||||
use serde_json::{
|
||||
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
||||
};
|
||||
|
||||
use super::{Device, Notification, NotificationCounts, NotificationPriority, Tweak};
|
||||
|
||||
#[test]
|
||||
fn serialize_request() {
|
||||
let expected = json!({
|
||||
"event_id": "$3957tyerfgewrf384",
|
||||
"room_id": "!slw48wfj34rtnrf:example.com",
|
||||
"type": "m.room.message",
|
||||
"sender": "@exampleuser:matrix.org",
|
||||
"sender_display_name": "Major Tom",
|
||||
"room_alias": "#exampleroom:matrix.org",
|
||||
"prio": "low",
|
||||
"content": {},
|
||||
"counts": {
|
||||
"unread": 2,
|
||||
},
|
||||
"devices": [
|
||||
{
|
||||
"app_id": "org.matrix.matrixConsole.ios",
|
||||
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
|
||||
"pushkey_ts": 123,
|
||||
"tweaks": {
|
||||
"sound": "silence",
|
||||
"highlight": true,
|
||||
"custom": "go wild"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let eid = owned_event_id!("$3957tyerfgewrf384");
|
||||
let rid = owned_room_id!("!slw48wfj34rtnrf:example.com");
|
||||
let uid = owned_user_id!("@exampleuser:matrix.org");
|
||||
let alias = owned_room_alias_id!("#exampleroom:matrix.org");
|
||||
|
||||
let count = NotificationCounts { unread: uint!(2), ..NotificationCounts::default() };
|
||||
|
||||
let device = Device {
|
||||
pushkey_ts: Some(SecondsSinceUnixEpoch(uint!(123))),
|
||||
tweaks: vec![
|
||||
Tweak::Highlight(true),
|
||||
Tweak::Sound("silence".into()),
|
||||
Tweak::Custom {
|
||||
name: "custom".into(),
|
||||
value: from_json_value(JsonValue::String("go wild".into())).unwrap(),
|
||||
},
|
||||
],
|
||||
..Device::new(
|
||||
"org.matrix.matrixConsole.ios".into(),
|
||||
"V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/".into(),
|
||||
)
|
||||
};
|
||||
let devices = vec![device];
|
||||
|
||||
let notice = Notification {
|
||||
event_id: Some(eid),
|
||||
room_id: Some(rid),
|
||||
event_type: Some(TimelineEventType::RoomMessage),
|
||||
sender: Some(uid),
|
||||
sender_display_name: Some("Major Tom".to_owned()),
|
||||
room_alias: Some(alias),
|
||||
content: Some(serde_json::from_str("{}").unwrap()),
|
||||
counts: count,
|
||||
prio: NotificationPriority::Low,
|
||||
devices,
|
||||
..Notification::default()
|
||||
};
|
||||
|
||||
assert_eq!(expected, to_json_value(notice).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,10 +60,6 @@ identity-service-api-s = [
|
|||
]
|
||||
identity-service-api = ["identity-service-api-c", "identity-service-api-s"]
|
||||
|
||||
push-gateway-api-c = ["api", "dep:ruma-push-gateway-api", "ruma-push-gateway-api?/client"]
|
||||
push-gateway-api-s = ["api", "dep:ruma-push-gateway-api", "ruma-push-gateway-api?/server"]
|
||||
push-gateway-api = ["push-gateway-api-c", "push-gateway-api-s"]
|
||||
|
||||
# Required for randomness, current system time in browser environments
|
||||
js = ["ruma-common/js"]
|
||||
|
||||
|
@ -84,7 +80,6 @@ full = [
|
|||
"client-api",
|
||||
"federation-api",
|
||||
"identity-service-api",
|
||||
"push-gateway-api",
|
||||
"rand",
|
||||
"markdown",
|
||||
"html",
|
||||
|
@ -162,7 +157,6 @@ unstable-exhaustive-types = [
|
|||
"ruma-client-api?/unstable-exhaustive-types",
|
||||
"ruma-federation-api?/unstable-exhaustive-types",
|
||||
"ruma-identity-service-api?/unstable-exhaustive-types",
|
||||
"ruma-push-gateway-api?/unstable-exhaustive-types",
|
||||
"ruma-state-res?/unstable-exhaustive-types",
|
||||
"ruma-events?/unstable-exhaustive-types"
|
||||
]
|
||||
|
@ -218,7 +212,6 @@ unstable-pdu = ["ruma-events?/unstable-pdu"]
|
|||
unstable-unspecified = [
|
||||
"ruma-common/unstable-unspecified",
|
||||
"ruma-federation-api?/unstable-unspecified",
|
||||
"ruma-push-gateway-api?/unstable-unspecified",
|
||||
]
|
||||
|
||||
# Private feature, only used in test / benchmarking code
|
||||
|
@ -279,7 +272,6 @@ ruma-appservice-api = { workspace = true, optional = true }
|
|||
ruma-client-api = { workspace = true, optional = true }
|
||||
ruma-federation-api = { workspace = true, optional = true }
|
||||
ruma-identity-service-api = { workspace = true, optional = true }
|
||||
ruma-push-gateway-api = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { workspace = true }
|
||||
|
|
Loading…
Reference in a new issue