diff --git a/ruma-common/.builds/beta.yml b/ruma-common/.builds/beta.yml new file mode 100644 index 00000000..6ee1445f --- /dev/null +++ b/ruma-common/.builds/beta.yml @@ -0,0 +1,27 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-common +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install beta --profile minimal -c rustfmt -c clippy + rustup default beta + - test: | + cd ruma-common + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --verbose + test_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test_exit )) diff --git a/ruma-common/.builds/msrv.yml b/ruma-common/.builds/msrv.yml new file mode 100644 index 00000000..877ee72c --- /dev/null +++ b/ruma-common/.builds/msrv.yml @@ -0,0 +1,16 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-common +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install 1.40.0 --profile minimal + rustup default 1.40.0 + - test: | + cd ruma-common + + # Only make sure the code builds with the MSRV. Tests can require later + # Rust versions, don't compile or run them. + cargo build --verbose diff --git a/ruma-common/.builds/nightly.yml b/ruma-common/.builds/nightly.yml new file mode 100644 index 00000000..42b167f6 --- /dev/null +++ b/ruma-common/.builds/nightly.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-common +tasks: + - rustup: | + rustup toolchain install nightly --profile minimal + rustup default nightly + + # Try installing rustfmt & clippy for nightly, but don't fail the build + # if they are not available + rustup component add rustfmt || true + rustup component add clippy || true + - test: | + cd ruma-common + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + if ( rustup component list | grep -q rustfmt ); then + cargo fmt -- --check + fi + fmt_exit=$? + + if ( rustup component list | grep -q clippy ); then + cargo clippy --all-targets --all-features -- -D warnings + fi + clippy_exit=$? + + exit $(( $fmt_exit || $clippy_exit )) diff --git a/ruma-common/.builds/stable.yml b/ruma-common/.builds/stable.yml new file mode 100644 index 00000000..60d31426 --- /dev/null +++ b/ruma-common/.builds/stable.yml @@ -0,0 +1,29 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-common +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install stable --profile minimal -c rustfmt -c clippy + rustup default stable + - test: | + cd ruma-common + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --verbose + test_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test_exit )) + # TODO: Add audit task once cargo-audit binary releases are available. + # See https://github.com/RustSec/cargo-audit/issues/66 diff --git a/ruma-common/.gitignore b/ruma-common/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/ruma-common/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/ruma-common/Cargo.toml b/ruma-common/Cargo.toml new file mode 100644 index 00000000..19f2a0ee --- /dev/null +++ b/ruma-common/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ruma-common" +version = "0.1.3" +authors = ["Jonas Platte "] +description = "Common types for other ruma crates." +homepage = "https://github.com/ruma/ruma-common" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/ruma/ruma-client-api" +edition = "2018" + +[dependencies] +matches = "0.1.8" +ruma-serde = "0.2.2" +serde = { version = "1.0.111", features = ["derive"] } +serde_json = { version = "1.0.53", features = ["raw_value"] } +strum = { version = "0.18.0", features = ["derive"] } diff --git a/ruma-common/LICENSE b/ruma-common/LICENSE new file mode 100644 index 00000000..f0d48ccb --- /dev/null +++ b/ruma-common/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Jonas Platte + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ruma-common/README.md b/ruma-common/README.md new file mode 100644 index 00000000..ad707407 --- /dev/null +++ b/ruma-common/README.md @@ -0,0 +1,7 @@ +# ruma-common + +Common types for other ruma crates. + +This crate is meant to be a dependency for other ruma crates only, consumers of +those crates should never have to use this directly (its types will be +re-exported from the other crates). diff --git a/ruma-common/src/lib.rs b/ruma-common/src/lib.rs new file mode 100644 index 00000000..7f8a83a3 --- /dev/null +++ b/ruma-common/src/lib.rs @@ -0,0 +1,6 @@ +//! Common types for other ruma crates. + +#![warn(missing_docs)] + +pub mod presence; +pub mod push; diff --git a/ruma-common/src/presence.rs b/ruma-common/src/presence.rs new file mode 100644 index 00000000..52c33556 --- /dev/null +++ b/ruma-common/src/presence.rs @@ -0,0 +1,27 @@ +//! Common types for the [presence module][presence] +//! +//! [presence]: https://matrix.org/docs/spec/client_server/r0.6.1#id62 + +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; + +/// A description of a user's connectivity and availability for chat. +#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PresenceState { + /// Disconnected from the service. + Offline, + + /// Connected to the service. + Online, + + /// Connected to the service but not available for chat. + Unavailable, +} + +impl Default for PresenceState { + fn default() -> Self { + Self::Online + } +} diff --git a/ruma-common/src/push.rs b/ruma-common/src/push.rs new file mode 100644 index 00000000..0de7f2de --- /dev/null +++ b/ruma-common/src/push.rs @@ -0,0 +1,193 @@ +//! Common types for the [push notifications module][push] +//! +//! [push]: https://matrix.org/docs/spec/client_server/r0.6.0#id89 + +use std::fmt::{self, Formatter}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::value::RawValue as RawJsonValue; + +mod tweak_serde; + +/// This represents the different actions that should be taken when a rule is matched, and +/// controls how notifications are delivered to the client. +/// +/// See https://matrix.org/docs/spec/client_server/r0.6.0#actions for details. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Action { + /// Causes matching events to generate a notification. + Notify, + + /// Prevents matching events from generating a notification. + DontNotify, + + /// Behaves like notify but homeservers may choose to coalesce multiple events + /// into a single notification. + Coalesce, + + /// Sets an entry in the 'tweaks' dictionary sent to the push gateway. + SetTweak(Tweak), +} + +/// The `set_tweak` action. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[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 = "ruma_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, + }, +} + +impl<'de> Deserialize<'de> for Action { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + + struct ActionVisitor; + impl<'de> Visitor<'de> for ActionVisitor { + type Value = Action; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "a valid action object") + } + + /// Match a simple action type + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v { + "notify" => Ok(Action::Notify), + "dont_notify" => Ok(Action::DontNotify), + "coalesce" => Ok(Action::Coalesce), + s => Err(E::unknown_variant( + &s, + &["notify", "dont_notify", "coalesce"], + )), + } + } + + /// Match the more complex set_tweaks action object as a key-value map + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + Tweak::deserialize(serde::de::value::MapAccessDeserializer::new(map)) + .map(Action::SetTweak) + } + } + + deserializer.deserialize_any(ActionVisitor) + } +} + +impl Serialize for Action { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"), + Action::DontNotify => serializer.serialize_unit_variant("Action", 1, "dont_notify"), + Action::Coalesce => serializer.serialize_unit_variant("Action", 2, "coalesce"), + Action::SetTweak(kind) => kind.serialize(serializer), + } + } +} + +#[cfg(test)] +mod tests { + use matches::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_action() { + assert_eq!(to_json_value(&Action::Notify).unwrap(), json!("notify")); + } + + #[test] + fn serialize_tweak_sound_action() { + assert_eq!( + to_json_value(&Action::SetTweak(Tweak::Sound("default".into()))).unwrap(), + json!({ "set_tweak": "sound", "value": "default" }) + ); + } + + #[test] + fn serialize_tweak_highlight_action() { + 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_action() { + assert_matches!( + from_json_value::(json!("notify")).unwrap(), + Action::Notify + ); + } + + #[test] + fn deserialize_tweak_sound_action() { + let json_data = json!({ + "set_tweak": "sound", + "value": "default" + }); + assert_matches!( + &from_json_value::(json_data).unwrap(), + Action::SetTweak(Tweak::Sound(value)) if value == "default" + ); + } + + #[test] + fn deserialize_tweak_highlight_action() { + let json_data = json!({ + "set_tweak": "highlight", + "value": true + }); + assert_matches!( + from_json_value::(json_data).unwrap(), + Action::SetTweak(Tweak::Highlight(true)) + ); + } + + #[test] + fn deserialize_tweak_highlight_action_with_default_value() { + assert_matches!( + from_json_value::(json!({ "set_tweak": "highlight" })).unwrap(), + Action::SetTweak(Tweak::Highlight(true)) + ); + } +} diff --git a/ruma-common/src/push/tweak_serde.rs b/ruma-common/src/push/tweak_serde.rs new file mode 100644 index 00000000..1d022ef8 --- /dev/null +++ b/ruma-common/src/push/tweak_serde.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; + +/// Values for the `set_tweak` action. +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Tweak { + Sound(SoundTweak), + Highlight(HighlightTweak), + Custom { + #[serde(rename = "set_tweak")] + name: String, + value: Box, + }, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(tag = "set_tweak", rename = "sound")] +pub struct SoundTweak { + value: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(tag = "set_tweak", rename = "highlight")] +pub struct HighlightTweak { + #[serde( + default = "ruma_serde::default_true", + skip_serializing_if = "ruma_serde::is_true" + )] + value: bool, +} + +impl From 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 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 }, + } + } +}