#![deny(missing_docs)]
use crate::{adaptive_card::AdaptiveCard, error};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::convert::TryFrom;
use std::{collections::HashMap, fmt};
use uuid::Uuid;
pub(crate) use api::{Gettable, ListResult};
mod api {
use super::{
AttachmentAction, Message, MessageListParams, Organization, Person, Room, RoomListParams,
Team,
};
pub trait Gettable {
const API_ENDPOINT: &'static str;
type ListParams<'a>: serde::Serialize;
}
#[derive(crate::types::Serialize, Clone, Debug)]
pub enum Infallible {}
impl Gettable for Message {
const API_ENDPOINT: &'static str = "messages";
type ListParams<'a> = MessageListParams<'a>;
}
impl Gettable for Organization {
const API_ENDPOINT: &'static str = "organizations";
type ListParams<'a> = Option<Infallible>;
}
impl Gettable for AttachmentAction {
const API_ENDPOINT: &'static str = "attachment/actions";
type ListParams<'a> = Option<Infallible>;
}
impl Gettable for Room {
const API_ENDPOINT: &'static str = "rooms";
type ListParams<'a> = RoomListParams<'a>;
}
impl Gettable for Person {
const API_ENDPOINT: &'static str = "people";
type ListParams<'a> = Option<Infallible>;
}
impl Gettable for Team {
const API_ENDPOINT: &'static str = "teams";
type ListParams<'a> = Option<Infallible>;
}
#[derive(crate::types::Deserialize)]
pub struct ListResult<T> {
pub items: Vec<T>,
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Room {
pub id: String,
pub title: Option<String>,
#[serde(rename = "type")]
pub room_type: String,
pub is_locked: bool,
pub team_id: Option<String>,
pub last_activity: String,
pub creator_id: String,
pub created: String,
}
#[derive(Clone, Debug, Eq, PartialEq, crate::types::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SortRoomsBy {
Id,
LastActivity,
Created,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Eq, PartialEq, crate::types::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RoomListParams<'a> {
pub team_id: Option<&'a str>,
#[serde(rename = "type")]
pub room_type: Option<RoomType>,
pub org_public_spaces: Option<bool>,
pub from: Option<&'a str>,
pub to: Option<&'a str>,
pub sort_by: Option<SortRoomsBy>,
pub max: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Organization {
pub id: String,
pub display_name: Option<String>,
pub created: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Team {
pub id: String,
pub name: Option<String>,
pub created: String,
pub description: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CatalogReply {
pub service_links: Catalog,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Catalog {
pub atlas: String,
#[serde(rename = "broadworksIdpProxy")]
pub broadworks_idp_proxy: String,
#[serde(rename = "clientLogs")]
pub client_logs: String,
pub ecomm: String,
pub fms: String,
pub idbroker: String,
pub idbroker_guest: String,
pub identity: String,
pub identity_guest_cs: String,
pub license: String,
#[serde(rename = "meetingRegistry")]
pub meeting_registry: String,
pub metrics: String,
pub oauth_helper: String,
pub settings_service: String,
pub u2c: String,
pub wdm: String,
pub web_authentication: String,
pub webex_appapi_service: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Destination {
RoomId(String),
ToPersonId(String),
ToPersonEmail(String),
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageOut {
pub parent_id: Option<String>,
pub room_id: Option<String>,
pub to_person_id: Option<String>,
pub to_person_email: Option<String>,
pub text: Option<String>,
pub markdown: Option<String>,
pub files: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum RoomType {
#[default]
Direct,
Group,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub id: Option<String>,
pub room_id: Option<String>,
pub room_type: Option<RoomType>,
pub to_person_id: Option<String>,
pub to_person_email: Option<String>,
pub text: Option<String>,
pub markdown: Option<String>,
pub html: Option<String>,
pub files: Option<Vec<String>>,
pub person_id: Option<String>,
pub person_email: Option<String>,
pub mentioned_people: Option<Vec<String>>,
pub mentioned_groups: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>,
pub created: Option<String>,
pub updated: Option<String>,
pub parent_id: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageListParams<'a> {
pub room_id: &'a str,
pub parent_id: Option<&'a str>,
#[serde(skip_serializing_if = "<[_]>::is_empty")]
pub mentioned_people: &'a [&'a str],
pub before: Option<&'a str>,
pub before_message: Option<&'a str>,
pub max: Option<u32>,
}
impl<'a> MessageListParams<'a> {
#[allow(clippy::must_use_candidate)]
pub const fn new(room_id: &'a str) -> Self {
Self {
room_id,
parent_id: None,
mentioned_people: &[],
before: None,
before_message: None,
max: None,
}
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageEditParams<'a> {
pub room_id: &'a str,
pub text: Option<&'a str>,
pub markdown: Option<&'a str>,
pub html: Option<&'a str>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct EmptyReply {}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DeviceError {
pub description: String,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct DevicesReply {
pub devices: Option<Vec<DeviceData>>,
pub message: Option<String>,
pub errors: Option<Vec<DeviceError>>,
#[serde(rename = "trackingId")]
pub tracking_id: Option<String>,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceData {
pub url: Option<String>,
#[serde(rename = "webSocketUrl")]
pub ws_url: Option<String>,
pub device_name: Option<String>,
pub device_type: Option<String>,
pub localized_model: Option<String>,
pub modification_time: Option<chrono::DateTime<chrono::Utc>>,
pub model: Option<String>,
pub name: Option<String>,
pub system_name: Option<String>,
pub system_version: Option<String>,
}
impl fmt::Display for DeviceData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "name: {:?}, device_name: {:?}, device_type: {:?}, model: {:?}, system_name: {:?}, system_version: {:?}, url: {:?}",
self.name, self.device_name, self.device_type, self.model, self.system_name, self.system_version, self.url)
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Authorization {
pub id: String,
#[serde(rename = "type")]
pub auth_type: String,
data: AuthToken,
}
impl Authorization {
#[must_use]
pub fn new(token: &str) -> Self {
Self {
id: Uuid::new_v4().to_string(),
auth_type: "authorization".to_string(),
data: AuthToken {
token: format!("Bearer {token}"),
},
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct AuthToken {
pub token: String,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Actor {
pub id: String,
pub object_type: String,
pub display_name: Option<String>,
pub org_id: Option<String>,
pub email_address: Option<String>,
#[serde(rename = "entryUUID")]
pub entry_uuid: String,
#[serde(rename = "type")]
pub actor_type: Option<String>,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EventData {
pub event_type: String,
pub actor: Option<Actor>,
pub conversation_id: Option<String>,
pub activity: Option<Activity>,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityParent {
pub actor_id: String,
pub id: String,
pub published: String,
#[serde(rename = "type")]
pub parent_type: String,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Activity {
pub actor: Actor,
pub client_temp_id: Option<String>,
pub encryption_key_url: Option<String>,
pub id: String,
pub object_type: String,
pub object: Object,
pub parent: Option<ActivityParent>,
pub published: String,
pub target: Option<Target>,
pub url: Option<String>,
pub vector_counters: Option<VectorCounters>,
pub verb: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ActivityType {
Message(MessageActivity),
Space(SpaceActivity),
AdaptiveCardSubmit,
Locus,
Janus,
StartTyping,
Highlight,
Unknown(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MessageActivity {
Posted,
Shared,
Acknowledged,
Deleted,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SpaceActivity {
Changed,
Created,
Favorite,
Joined,
Left,
Locked,
MeetingScheduled,
ModeratorAssigned,
ModeratorUnassigned,
Unfavorite,
Unlocked,
}
impl TryFrom<&str> for MessageActivity {
type Error = ();
fn try_from(s: &str) -> Result<Self, ()> {
match s {
"post" => Ok(Self::Posted),
"share" => Ok(Self::Shared),
"acknowledge" => Ok(Self::Acknowledged),
"delete" => Ok(Self::Deleted),
_ => Err(()),
}
}
}
impl TryFrom<&str> for SpaceActivity {
type Error = ();
fn try_from(s: &str) -> Result<Self, ()> {
match s {
"add" => Ok(Self::Joined),
"assignModerator" => Ok(Self::ModeratorAssigned),
"create" => Ok(Self::Created),
"favorite" => Ok(Self::Favorite),
"leave" => Ok(Self::Left),
"lock" => Ok(Self::Locked),
"schedule" => Ok(Self::MeetingScheduled),
"unassignModerator" => Ok(Self::ModeratorUnassigned),
"unfavorite" => Ok(Self::Unfavorite),
"unlock" => Ok(Self::Unlocked),
"update" | "assign" | "unassign" => Ok(Self::Changed),
_ => Err(()),
}
}
}
impl MessageActivity {
#[must_use]
pub const fn is_created(&self) -> bool {
matches!(*self, Self::Posted | Self::Shared)
}
}
impl Event {
#[must_use]
pub fn activity_type(&self) -> ActivityType {
match self.data.event_type.as_str() {
"conversation.activity" => {
let activity_type = self
.data
.activity
.as_ref()
.expect("Conversation activity should have activity set")
.verb
.as_str();
#[allow(clippy::option_if_let_else)]
match activity_type {
"cardAction" => ActivityType::AdaptiveCardSubmit,
_ => {
if let Ok(type_) = MessageActivity::try_from(activity_type) {
ActivityType::Message(type_)
} else if let Ok(type_) = SpaceActivity::try_from(activity_type) {
ActivityType::Space(type_)
} else {
log::error!(
"Unknown activity type `{}`, returning Unknown",
activity_type
);
ActivityType::Unknown(format!("conversation.activity.{activity_type}"))
}
}
}
}
"conversation.highlight" => ActivityType::Highlight,
"status.start_typing" => ActivityType::StartTyping,
"locus.difference" => ActivityType::Locus,
"janus.user_sessions" => ActivityType::Janus,
e => {
log::debug!("Unknown data.event_type `{}`, returning Unknown", e);
ActivityType::Unknown(e.to_string())
}
}
}
#[deprecated(since = "0.10.0", note = "please use `try_global_id` instead")]
pub fn get_global_id(&self) -> GlobalId {
self.try_global_id()
.expect("Could not get global ID from event")
}
pub fn try_global_id(&self) -> Result<GlobalId, crate::error::Error> {
let activity = self
.data
.activity
.as_ref()
.ok_or(crate::error::Error::Api("Missing activity in event"))?;
let id = match self.activity_type() {
ActivityType::Space(SpaceActivity::Created) => self.room_id_of_space_created_event()?,
ActivityType::Space(
SpaceActivity::Changed | SpaceActivity::Joined | SpaceActivity::Left,
) => Self::target_global_id(activity)?,
ActivityType::Message(MessageActivity::Deleted) => Self::target_global_id(activity)?,
_ => activity.id.clone(),
};
Ok(GlobalId::new_with_cluster_unchecked(
self.activity_type().into(),
id,
None,
))
}
fn target_global_id(activity: &Activity) -> Result<String, error::Error> {
activity
.target
.clone()
.and_then(|t| t.global_id)
.ok_or(crate::error::Error::Api("Missing target id in activity"))
}
fn room_id_of_space_created_event(&self) -> Result<String, crate::error::Error> {
assert_eq!(
self.activity_type(),
ActivityType::Space(SpaceActivity::Created),
"Expected space created event, got {:?}",
self.activity_type()
);
let activity_id = self
.data
.activity
.clone()
.ok_or(crate::error::Error::Api(
"Missing activity in space created event",
))?
.id;
if Uuid::parse_str(&activity_id).is_err() {
return Ok(activity_id);
}
let mut uuid = activity_id;
if uuid.as_bytes()[7] == b'2' {
uuid.replace_range(7..8, "0");
Ok(uuid)
} else {
Err(crate::error::Error::Api(
"Space created event uuid could not be not patched",
))
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GlobalIdType {
Message,
Person,
Room,
Team,
AttachmentAction,
Unknown,
}
impl From<ActivityType> for GlobalIdType {
fn from(a: ActivityType) -> Self {
match a {
ActivityType::AdaptiveCardSubmit => Self::AttachmentAction,
ActivityType::Message(_) => Self::Message,
ActivityType::Space(
SpaceActivity::Changed
| SpaceActivity::Created
| SpaceActivity::Joined
| SpaceActivity::Left,
) => Self::Room,
ActivityType::Unknown(_) => Self::Unknown,
a => {
log::error!(
"Failed to convert {:?} to GlobalIdType, this may cause errors later",
a
);
Self::Unknown
}
}
}
}
impl std::fmt::Display for GlobalIdType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(
f,
"{}",
match self {
Self::Message => "MESSAGE",
Self::Person => "PEOPLE",
Self::Room => "ROOM",
Self::Team => "TEAM",
Self::AttachmentAction => "ATTACHMENT_ACTION",
Self::Unknown => "<UNKNOWN>",
}
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[must_use]
pub struct GlobalId {
id: String,
type_: GlobalIdType,
}
impl GlobalId {
pub fn new(type_: GlobalIdType, id: String) -> Result<Self, error::Error> {
Self::new_with_cluster(type_, id, None)
}
pub fn new_with_cluster(
type_: GlobalIdType,
id: String,
cluster: Option<&str>,
) -> Result<Self, error::Error> {
if type_ == GlobalIdType::Unknown {
return Err("Cannot get globalId for unknown ID type".into());
}
if let Ok(decoded_id) = base64::engine::general_purpose::STANDARD_NO_PAD.decode(&id) {
let decoded_id = std::str::from_utf8(&decoded_id)?;
Self::check_id(decoded_id, cluster, &type_.to_string())?;
} else if Uuid::parse_str(&id).is_err() {
return Err("Expected ID to be base64 geo-id or uuid".into());
}
Ok(Self::new_with_cluster_unchecked(type_, id, cluster))
}
pub fn new_with_cluster_unchecked(
type_: GlobalIdType,
id: String,
cluster: Option<&str>,
) -> Self {
let id = if Uuid::parse_str(&id).is_ok() {
base64::engine::general_purpose::STANDARD.encode(format!(
"ciscospark://{}/{}/{}",
cluster.unwrap_or("us"),
type_,
id
))
} else {
id
};
Self { id, type_ }
}
fn check_id(id: &str, cluster: Option<&str>, type_: &str) -> Result<(), error::Error> {
let decoded_parts: Vec<&str> = id.split('/').collect();
if decoded_parts.len() != 5
|| decoded_parts[0] != "ciscospark:"
|| !decoded_parts[1].is_empty()
{
return Err(
"Expected base64 ID to be in the form ciscospark://[cluster]/[type]/[id]".into(),
);
} else if let Some(expected_cluster) = cluster {
if decoded_parts[2] != expected_cluster {
return Err(format!(
"Expected base64 cluster to equal expected cluster {expected_cluster}"
)
.into());
}
} else if decoded_parts[3] != type_ {
return Err(format!("Expected base64 type to equal {type_}").into());
}
Ok(())
}
#[inline]
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
pub fn check_type(&self, expected_type: GlobalIdType) -> Result<(), error::Error> {
if expected_type == self.type_ {
Ok(())
} else {
Err(format!(
"GlobalId type {} does not match expected type {expected_type}",
self.type_
)
.into())
}
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VectorCounters {
#[serde(rename = "sourceDC")]
pub source_dc: String,
pub counters: HashMap<String, i64>,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Target {
pub id: String,
pub object_type: String,
pub url: String,
pub participants: Option<MiscItems>,
pub activities: Option<MiscItems>,
pub tags: Vec<String>,
pub global_id: Option<String>,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Object {
pub object_type: String,
pub content: Option<String>,
pub display_name: Option<String>,
pub mentions: Option<MiscItems>,
pub inputs: Option<String>,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct MiscItems {
#[serde(default)]
pub items: Vec<MiscItem>,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MiscItem {
pub id: String,
#[serde(rename = "objectType")]
pub object_type: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AlertType {
#[default]
None,
Full,
Visual,
}
#[allow(missing_docs)]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Event {
pub id: String,
#[allow(missing_docs)]
pub data: EventData,
pub timestamp: i64,
pub tracking_id: String,
pub alert_type: Option<AlertType>,
pub headers: HashMap<String, String>,
pub sequence_number: i64,
pub filter_message: bool,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct Attachment {
#[serde(rename = "contentType")]
pub content_type: String,
pub content: AdaptiveCard,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AttachmentAction {
pub id: String,
#[serde(rename = "type")]
pub action_type: Option<String>,
pub message_id: Option<String>,
pub inputs: Option<HashMap<String, serde_json::Value>>,
pub person_id: Option<String>,
pub room_id: Option<String>,
pub created: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Person {
pub id: String,
pub emails: Vec<String>,
pub phone_numbers: Vec<PhoneNumber>,
pub display_name: String,
pub nick_name: String,
pub first_name: String,
pub last_name: String,
pub avatar: String,
pub org_id: String,
pub created: String,
pub last_activity: String,
pub status: String,
#[serde(rename = "type")]
pub person_type: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct PhoneNumber {
#[serde(rename = "type")]
pub number_type: String,
pub value: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_event(event_type: &str, activity_verb: &str) -> Event {
Event {
data: EventData {
event_type: event_type.to_string(),
activity: Some(Activity {
verb: activity_verb.to_string(),
..Activity::default()
}),
..EventData::default()
},
..Event::default()
}
}
#[test]
fn event_parsing() {
let test_events = [
(
"conversation.activity",
"post",
ActivityType::Message(MessageActivity::Posted),
),
(
"conversation.activity",
"share",
ActivityType::Message(MessageActivity::Shared),
),
(
"conversation.activity",
"unknown",
ActivityType::Unknown("conversation.activity.unknown".to_string()),
),
("unknown", "", ActivityType::Unknown("unknown".to_string())),
("conversation.highlight", "", ActivityType::Highlight),
];
for test_e in test_events {
let event = create_event(test_e.0, test_e.1);
let result = test_e.2;
assert_eq!(event.activity_type(), result);
}
}
#[test]
fn msg_is_created() {
assert!(MessageActivity::Posted.is_created());
assert!(MessageActivity::Shared.is_created());
assert!(!MessageActivity::Deleted.is_created());
}
#[test]
fn global_id_without_padding() {
let id = "Y2lzY29zcGFyazovL3VzL1BFT1BMRS82YmIwODVmYS1mNmIyLTQyMTAtYjI2Ny1iZTBmZGViYjA3YzQ";
let global_id = GlobalId::new(GlobalIdType::Person, id.to_string()).unwrap();
assert_eq!(global_id.id(), id);
}
#[test]
fn test_space_created_event_patched_room_id() {
let mut event = Event {
id: "assumed_valid_base64".to_string(),
data: EventData {
event_type: "conversation.activity".to_string(),
activity: Some(Activity {
verb: "create".to_string(),
id: "1ab849e2-9ab4-11ee-a70f-d9b57e49f8bf".to_string(),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
assert_eq!(
event.room_id_of_space_created_event().unwrap(),
"1ab849e0-9ab4-11ee-a70f-d9b57e49f8bf"
);
event.data.activity = Some(Activity {
verb: "create".to_string(),
id: "bogus".to_string(),
..Default::default()
});
assert_eq!(event.room_id_of_space_created_event().unwrap(), "bogus");
event.data.activity = Some(Activity {
verb: "create".to_string(),
id: "1ab849e9-9ab4-11ee-a70f-d9b57e49f8bf".to_string(),
..Default::default()
});
assert!(event.room_id_of_space_created_event().is_err());
}
}