From 6fe905c2befa1085a9ff8042504b9646f836f46f Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Wed, 5 Apr 2023 13:44:00 +0200 Subject: [PATCH 01/19] refactor!: simplifies and extends typings for Event types BREAKING CHANGE: reworks the API and types for how Events are used within this library. Removes the existing `EventType` in favor of a simpler version where event properties are expressed directly on the enum variants. Fixes some composition issues specifically with message events. Adds some additional types to better distinguish between basic messages and message subtypes that are used in the Slack Events API. Introduces the `assert-json-diff` crate to validate serialization and deserialization of different types --- Cargo.toml | 6 +- examples/events_app_mention.rs | 6 +- src/event_api/event.rs | 219 +++++++++++++-------------------- src/event_api/messages.rs | 115 +++++++++++++++++ src/event_api/mod.rs | 1 + src/socket/event.rs | 12 +- src/socket/socket_mode.rs | 8 +- 7 files changed, 216 insertions(+), 151 deletions(-) create mode 100644 src/event_api/messages.rs diff --git a/Cargo.toml b/Cargo.toml index 5f47ab7..e6d5222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,12 @@ features = ["client"] [dev-dependencies] env_logger = "0.9.0" mockall = "0.11.0" +assert-json-diff = "^2.0.2" [dev-dependencies.async-tls] version = "0.11.0" default-features = false -features = ["server"] \ No newline at end of file +features = ["server"] + +[build] +incremental = true diff --git a/examples/events_app_mention.rs b/examples/events_app_mention.rs index e24289d..4e7e43c 100644 --- a/examples/events_app_mention.rs +++ b/examples/events_app_mention.rs @@ -2,7 +2,7 @@ use std::env; use async_trait::async_trait; use slack::chat::post_message::{post_message, PostMessageRequest}; -use slack::event_api::event::{Event, EventCallbackType}; +use slack::event_api::event::{Event, EventType}; use slack::http_client::{default_client, SlackWebAPIClient}; use slack::socket::event::{EventsAPI, HelloEvent}; use slack::socket::socket_mode::{ack, EventHandler, SocketMode, Stream}; @@ -51,8 +51,8 @@ where .expect("socket mode ack error."); match e.payload { - Event::EventCallback(event_callback) => match event_callback.event { - EventCallbackType::AppMention { + Event{event, ..} => match event { + EventType::AppMention { text, channel, ts, diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 0a5833b..b2c9ec1 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -2,123 +2,53 @@ use crate::channels::channel::Channel; use crate::event_api::app::AppRequest; +use crate::event_api::messages::{MessageBasic, MessageSubtype}; use crate::team::teams::Team; use crate::views::view::View; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; /// [Event API](https://api.slack.com/events?filter=Events) -#[derive(Deserialize, Serialize, Debug, PartialEq)] -#[serde(tag = "type")] -pub enum Event { - /// Event callback - #[serde(rename = "event_callback")] - EventCallback(EventCallback), -} - -impl Event { - pub fn block_type(&self) -> EventType { - match self { - Event::EventCallback(event_callback) => match event_callback.event { - EventCallbackType::AppHomeOpened { .. } => EventType::AppHomeOpened, - EventCallbackType::AppMention { .. } => EventType::AppMention, - EventCallbackType::AppRateLimited { .. } => EventType::AppRateLimited, - EventCallbackType::AppRequested { .. } => EventType::AppRequested, - EventCallbackType::AppUninstalled { .. } => EventType::AppUninstalled, - EventCallbackType::ChannelArchive { .. } => EventType::ChannelArchive, - EventCallbackType::ChannelCreated { .. } => EventType::ChannelCreated, - EventCallbackType::ChannelDeleted { .. } => EventType::ChannelDeleted, - EventCallbackType::ChannelHistoryChanged { .. } => EventType::ChannelHistoryChanged, - EventCallbackType::ChannelIDChanged { .. } => EventType::ChannelIDChanged, - EventCallbackType::ChannelLeft { .. } => EventType::ChannelLeft, - EventCallbackType::ChannelRename { .. } => EventType::ChannelRename, - EventCallbackType::ChannelShared { .. } => EventType::ChannelShared, - EventCallbackType::ChannelUnarchive { .. } => EventType::ChannelUnarchive, - EventCallbackType::ChannelUnshared { .. } => EventType::ChannelUnshared, - EventCallbackType::EmojiChanged { .. } => EventType::EmojiChanged, - EventCallbackType::GridMigrationFinished { .. } => EventType::GridMigrationFinished, - EventCallbackType::GridMigrationStarted { .. } => EventType::GridMigrationStarted, - EventCallbackType::GroupArchive { .. } => EventType::GroupArchive, - EventCallbackType::GroupClose { .. } => EventType::GroupClose, - EventCallbackType::GroupDeleted { .. } => EventType::GroupDeleted, - EventCallbackType::GroupHistoryChanged { .. } => EventType::GroupHistoryChanged, - EventCallbackType::GroupLeft { .. } => EventType::GroupLeft, - EventCallbackType::GroupOpen { .. } => EventType::GroupOpen, - EventCallbackType::GroupRename { .. } => EventType::GroupRename, - EventCallbackType::GroupUnarchive { .. } => EventType::GroupUnarchive, - EventCallbackType::ImClose { .. } => EventType::ImClose, - EventCallbackType::ImCreated { .. } => EventType::ImCreated, - EventCallbackType::ImHistoryChanged { .. } => EventType::ImHistoryChanged, - EventCallbackType::ImOpen { .. } => EventType::ImOpen, - EventCallbackType::InviteRequested { .. } => EventType::InviteRequested, - EventCallbackType::LinkShared => EventType::LinkShared, - EventCallbackType::MemberJoinedChannel { .. } => EventType::MemberJoinedChannel, - EventCallbackType::MemberLeftChannel { .. } => EventType::MemberLeftChannel, - EventCallbackType::Message { .. } => EventType::Message, - EventCallbackType::Other => EventType::Other, - }, - } - } -} - /// [Event API Type](https://api.slack.com/events?filter=Events) +/// Example of an [event wrapper](https://api.slack.com/types/event) +/// ``` +/// { +/// "token": "XXYYZZ", +/// "team_id": "TXXXXXXXX", +/// "api_app_id": "AXXXXXXXXX", +/// "event": { +/// "type": "name_of_event", +/// "event_ts": "1234567890.123456", +/// "user": "UXXXXXXX1" +/// }, +/// "type": "event_callback", +/// "authed_users": [ +/// "UXXXXXXX1", +/// "UXXXXXXX2" +/// ], +/// "event_id": "Ev08MFMKH6", +/// "event_time": 1234567890 +/// } +/// ``` #[derive(Deserialize, Serialize, Debug, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum EventType { - AppHomeOpened, - AppMention, - AppRateLimited, - AppRequested, - AppUninstalled, - ChannelArchive, - ChannelCreated, - ChannelDeleted, - ChannelHistoryChanged, - ChannelIDChanged, - ChannelLeft, - ChannelRename, - ChannelShared, - ChannelUnarchive, - ChannelUnshared, - EmojiChanged, - GridMigrationFinished, - GridMigrationStarted, - GroupArchive, - GroupClose, - GroupDeleted, - GroupHistoryChanged, - GroupLeft, - GroupOpen, - GroupRename, - GroupUnarchive, - ImClose, - ImCreated, - ImHistoryChanged, - ImOpen, - InviteRequested, - LinkShared, - MemberJoinedChannel, - MemberLeftChannel, - Message, - #[serde(other)] - Other, -} - -#[skip_serializing_none] -#[derive(Deserialize, Serialize, Debug, PartialEq)] -pub struct EventCallback { +#[serde(tag = "type", rename = "event_callback")] +pub struct Event { pub token: String, pub team_id: String, pub api_app_id: String, - pub event: EventCallbackType, + #[serde(skip_serializing_if = "Option::is_none")] + pub authed_users: Option>, pub event_id: String, - pub event_time: i32, + pub event_time: u32, + pub event: EventType, } #[derive(Deserialize, Serialize, Debug, PartialEq)] #[skip_serializing_none] #[serde(rename_all = "snake_case", tag = "type")] -pub enum EventCallbackType { +// TODO - Message will have a type but an optional subtype as well. How can I avoid encoding that +// in this serde serialization? +pub enum EventType { /// User clicked into your App Home #[serde(rename = "app_home_opened")] AppHomeOpened { @@ -126,7 +56,8 @@ pub enum EventCallbackType { channel: String, event_ts: String, tab: String, - view: View, + #[serde(skip_serializing_if = "Option::is_none")] + view: Option, }, /// Subscribe to only the message events that mention your app or bot AppMention { @@ -280,7 +211,7 @@ pub enum EventCallbackType { inviter: String, }, /// A user left a public or private channel - #[serde(rename = "member_joined_channel")] + #[serde(rename = "member_left_channel")] MemberLeftChannel { user: String, channel: String, @@ -288,22 +219,18 @@ pub enum EventCallbackType { team: String, }, /// A message was sent to a channel - Message { - channel_type: String, - channel: String, - event_ts: String, - text: String, - thread_ts: Option, - ts: String, - user: String, - }, + #[serde(rename = "message")] + Message(MessageBasic), + #[serde(rename = "message")] + MessageSubtype(MessageSubtype), #[serde(other)] Other, } #[cfg(test)] mod test { - use super::*; + use assert_json_diff::*; + use crate::event_api::event::{Event, EventType}; #[test] fn deserialize_app_home_opened_event() { @@ -326,24 +253,50 @@ mod test { } }"##; let event = serde_json::from_str::(json).unwrap(); - match event { - Event::EventCallback(event_callback) => match event_callback.event { - EventCallbackType::AppHomeOpened { - user, - channel, - event_ts, - tab, - view, - } => { - assert_eq!(user, "U061F7AUR"); - assert_eq!(channel, "D0LAN2Q65"); - assert_eq!(event_ts, "1515449522000016"); - assert_eq!(tab, "home"); - assert_eq!(view.id.unwrap(), "VPASKP233"); - } - _ => panic!("Event callback deserialize into incorrect variant"), - }, + match event.event { + EventType::AppHomeOpened{..} => assert!(true, "true"), + _ => panic!("Event callback deserialize into incorrect variant"), + } + } + + #[test] + fn it_serializes_event_from_a_struct() { + let json = r##"{ + "token": "bHKJ2n9AW6Ju3MjciOHfbA1b", + "team_id": "T1234567890", + "api_app_id": "A0000000000", + "event_id": "Ev0000000000", + "event_time": 1600000000, + "type": "event_callback", + "event": { + "type": "app_home_opened", + "user": "U061F7AUR", + "channel": "D0LAN2Q65", + "event_ts": "1515449522000016", + "tab": "home" + } + }"##; + + let struct_thing = Event { + token: "bHKJ2n9AW6Ju3MjciOHfbA1b".to_string(), + team_id: "T1234567890".to_string(), + api_app_id: "A0000000000".to_string(), + event_id: "Ev0000000000".to_string(), + event_time: 1600000000, + authed_users: None, + event: EventType::AppHomeOpened { + user: "U061F7AUR".to_string(), + channel: "D0LAN2Q65".to_string(), + event_ts: "1515449522000016".to_string(), + tab: "home".to_string(), + view: None, } + }; + + let serialized_json = serde_json::to_string(&struct_thing).unwrap(); + let deserialized = serde_json::from_str::(&serialized_json).unwrap(); + let expected = serde_json::from_str::(&json).unwrap(); + assert_json_eq!(deserialized, expected); } #[test] @@ -361,11 +314,9 @@ mod test { }"##; let event = serde_json::from_str::(json).unwrap(); - match event { - Event::EventCallback(event_callback) => match event_callback.event { - EventCallbackType::Other => assert!(true, "true"), - _ => panic!("Event callback deserialize into incorrect variant"), - }, + match event.event { + EventType::Other => assert!(true, "true"), + _ => panic!("Event callback deserialize into incorrect variant"), } } } diff --git a/src/event_api/messages.rs b/src/event_api/messages.rs new file mode 100644 index 0000000..a908f1d --- /dev/null +++ b/src/event_api/messages.rs @@ -0,0 +1,115 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[serde(tag = "subtype")] +pub enum MessageSubtype { + #[serde(rename = "message_updated")] + MessageUpdate, + #[serde(rename = "message_replied")] + MessageReplied { + #[serde(rename = "type", default = "MessageSubtype::default_type")] + _type: String, + channel: String, + event_ts: String, + hidden: bool, + message: MessageReply, + ts: String, + } +} + +impl MessageSubtype { + fn default_type() -> String { + "message".to_string() + } + +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[skip_serializing_none] +pub struct MessageBasic { + pub channel: String, + pub channel_type: String, + pub edited: Option, + pub event_ts: String, + pub text: String, + pub thread_ts: Option, + pub ts: String, + pub user: String, +} + +// TODO - this could probably be merged with MessageBasic +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct MessageReply { + pub replies: Option>, + pub reply_count: u8, + pub text: String, + pub thread_ts: String, + pub ts: String, + pub user: String, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[skip_serializing_none] +pub struct MessageEdit { + pub user: String, + pub ts: String, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[skip_serializing_none] +pub struct MessageUpdate { + pub edited: MessageEdit, + pub subtype: String, + pub text: String, + pub ts: String, + pub user: String, +} + +#[cfg(test)] +mod tests { + use crate::event_api::messages::*; + use serde_json::Value; + + #[test] + fn serializes_message_replied_correctly() { + let expected = r##" + { + "type": "message", + "message": { + "type": "message", + "user": "U1111111", + "text": "Was there was there was there what was there was there what was there was there there was there.", + "thread_ts": "1482960137.003543", + "reply_count": 1, + "replies": [ + { + "user": "U2222222", + "ts": "1483037603.017503" + } + ], + "ts": "1482960137.003543" + }, + "subtype": "message_replied", + "hidden": true, + "channel": "C12345678", + "event_ts": "1483037604.017506", + "ts": "1483037604.017506" + }"##; + + let deserialized = serde_json::from_str::(&expected).unwrap(); + // Reserialize this for later assertions + let serialized = serde_json::to_string(&deserialized).unwrap(); + match deserialized { + MessageSubtype::MessageReplied{message, _type, ..} => { + assert_eq!(message.user, "U1111111"); + assert_eq!(_type, "message".to_string()); + }, + _ => panic!("Event callback deserialize into incorrect variant"), + } + // Validate that reserialized JSON contains expected fields + let json: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(json["subtype"], "message_replied".to_string()); + assert_eq!(json["type"], "message".to_string()); + } +} diff --git a/src/event_api/mod.rs b/src/event_api/mod.rs index b5a6d9d..2b2dc89 100644 --- a/src/event_api/mod.rs +++ b/src/event_api/mod.rs @@ -2,3 +2,4 @@ pub mod app; pub mod event; +pub mod messages; diff --git a/src/socket/event.rs b/src/socket/event.rs index a81535a..091a093 100644 --- a/src/socket/event.rs +++ b/src/socket/event.rs @@ -117,7 +117,7 @@ pub struct AcknowledgeMessage<'s> { #[cfg(test)] mod test { use super::*; - use crate::{event_api::event::EventCallbackType, payloads::interactive::InteractiveEventType}; + use crate::payloads::interactive::InteractiveEventType; #[test] fn deserialize_hello_event() { @@ -200,13 +200,9 @@ mod test { assert_eq!(envelope_id, "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545"); assert!(!accepts_response_payload, "false"); - match payload { - Event::EventCallback(event_callback) => match event_callback.event { - EventCallbackType::AppHomeOpened { user, .. } => { - assert_eq!(user, "U061F7AUR"); - } - _ => panic!("Event callback deserialize into incorrect variant"), - }, + match payload.event { + crate::event_api::event::EventType::AppHomeOpened{user, ..} => assert_eq!(user, "U061F7AUR"), + _ => panic!("Event callback deserialize into incorrect variant"), } } _ => panic!("Event deserialize into incorrect variant"), diff --git a/src/socket/socket_mode.rs b/src/socket/socket_mode.rs index b0dd8f1..f9745f6 100644 --- a/src/socket/socket_mode.rs +++ b/src/socket/socket_mode.rs @@ -183,7 +183,7 @@ pub async fn connector_for_ca_file(ca_file_path: &str) -> Result match event_callback.event { - EventCallbackType::AppHomeOpened { user, .. } => { - assert_eq!(user, "U061F7AUR"); - } + Event{event, ..} => match event { + EventType::AppHomeOpened {user, .. } => assert_eq!(user, "U061F7AUR"), _ => panic!("Event callback deserialize into incorrect variant"), }, } From e40c9a35f5d0bba59c44c48b695cc30e19df1ae9 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sun, 9 Apr 2023 10:59:10 +0200 Subject: [PATCH 02/19] test: adds tests for "app requested" event These tests are known to be failing. This demonstrates that the current enum implementation is incomplete compared to the examples provided by the Slack documentation --- src/event_api/app.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/event_api/app.rs b/src/event_api/app.rs index c17785e..82dd369 100644 --- a/src/event_api/app.rs +++ b/src/event_api/app.rs @@ -6,6 +6,7 @@ use serde_with::skip_serializing_none; #[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq)] +/// See more at pub struct AppRequest { pub id: Option, pub app: Option, @@ -46,3 +47,74 @@ pub struct Scope { pub is_sensitive: Option, pub token_type: Option, } + +#[cfg(test)] +mod tests { + use crate::event_api::event::EventType; + use assert_json_diff::*; + use serde_json::Value; + + #[test] + fn deserialized_app_requested_event() { + let json = r##" + { + "type": "app_requested", + "app_request":{ + "id": "1234", + "app": { + "id": "A5678", + "name": "Brent's app", + "description": "They're good apps, Bront.", + "help_url": "brontsapp.com", + "privacy_policy_url": "brontsapp.com", + "app_homepage_url": "brontsapp.com", + "app_directory_url": "https://slack.slack.com/apps/A102ARD7Y", + "is_app_directory_approved": true, + "is_internal": false, + "additional_info": "none" + }, + "previous_resolution": { + "status": "approved", + "scopes": [ + { + "name": "app_requested", + "description": "allows this app to listen for app install requests", + "is_sensitive": false, + "token_type": "user" + }] + }, + "user":{ + "id": "U1234", + "name": "Bront", + "email": "bront@brent.com" + }, + "team": { + "id": "T1234", + "name": "Brant App Team", + "domain": "brantappteam" + }, + "enterprise": null, + "scopes": [ + { + "name": "app_requested", + "description": "allows this app to listen for app install requests", + "is_sensitive": false, + "token_type": "user" + } + ], + "message": "none" + } + } + "##; + let deserialized = serde_json::from_str::(&json).unwrap(); + // Make comparison between our deserialized struct and generic serde_json Value to ensure + // that all expected keys exist + let expected: Value = serde_json::from_str(&json).unwrap(); + assert_json_eq!(deserialized, expected); + + match deserialized { + EventType::AppRequested{..} => assert!(true), + _ => panic!("unrecognized variant"), + } + } +} From 6ac5a559a0433d9e79ba36eb65fc1545634d389f Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sun, 9 Apr 2023 11:00:30 +0200 Subject: [PATCH 03/19] test: adds additional tests to Events API Adds additional tests to validate that data is serializing / deserializing into the expected types with expected fields --- src/event_api/event.rs | 66 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index b2c9ec1..29f8ec9 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -46,9 +46,11 @@ pub struct Event { #[derive(Deserialize, Serialize, Debug, PartialEq)] #[skip_serializing_none] #[serde(rename_all = "snake_case", tag = "type")] -// TODO - Message will have a type but an optional subtype as well. How can I avoid encoding that -// in this serde serialization? pub enum EventType { + /// The list of accounts a user is signed into has changed + /// + /// + AccountsChanged, /// User clicked into your App Home #[serde(rename = "app_home_opened")] AppHomeOpened { @@ -60,6 +62,8 @@ pub enum EventType { view: Option, }, /// Subscribe to only the message events that mention your app or bot + /// + /// AppMention { channel: String, event_ts: String, @@ -69,7 +73,8 @@ pub enum EventType { user: String, }, /// Indicates your app's event subscriptions are being rate limited - #[serde(rename = "app_rate_limited")] + /// + /// AppRateLimited { token: String, team_id: String, @@ -77,8 +82,11 @@ pub enum EventType { api_app_id: String, }, /// User requested an app - #[serde(rename = "app_requested")] - AppRequested { app_request: AppRequest }, + /// + /// + AppRequested { + app_request: AppRequest + }, /// Your Slack app was uninstalled. AppUninstalled, /// A channel was archived @@ -299,6 +307,54 @@ mod test { assert_json_eq!(deserialized, expected); } + #[test] + fn deserializes_accounts_changed() { + let json = r##" + { + "type": "accounts_changed" + }"##; + let event = serde_json::from_str::(json).unwrap(); + match event { + EventType::AccountsChanged{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + + #[test] + fn deserializes_app_rate_limited() { + let json = r##" + { + "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl", + "type": "app_rate_limited", + "team_id": "T123456", + "minute_rate_limited": 1518467820, + "api_app_id": "A123456" + }"##; + let event = serde_json::from_str::(json).unwrap(); + match event { + EventType::AppRateLimited{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + + #[test] + fn deserializes_app_mention() { + let json = r##" + { + "type": "app_mention", + "user": "U061F7AUR", + "text": "<@U0LAN0Z89> is it everything a river should be?", + "ts": "1515449522.000016", + "channel": "C0LAN2Q65", + "event_ts": "1515449522000016" + }"##; + let event = serde_json::from_str::(json).unwrap(); + match event { + EventType::AppMention{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From f0bff62448e94bc0eeed0809a3f5068eda895264 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 10 Apr 2023 08:40:15 +0200 Subject: [PATCH 04/19] fix: correct field definition for `app_requested` event Fixes an issue where the fields for the `app_requested` event were defined incorrectly --- src/event_api/app.rs | 17 ++++++++++++----- src/users/user.rs | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/event_api/app.rs b/src/event_api/app.rs index 82dd369..a77aa92 100644 --- a/src/event_api/app.rs +++ b/src/event_api/app.rs @@ -3,14 +3,25 @@ use crate::users::user::User; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use serde_json::Value; #[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq)] /// See more at +/// +/// Note that the `enterprise` field is a bit ambiguous. Slack's official +/// documentation says this value may be null or not null (but does not provide +/// an example). Therefore we use the `serde_json::Value::Null` variant to +/// represent this possibility pub struct AppRequest { - pub id: Option, pub app: Option, + pub enterprise: Value, + pub id: Option, + pub message: Option, pub previous_resolution: Option, + pub scopes: Option>, + pub team: Option, + pub user: Option, } #[skip_serializing_none] @@ -26,10 +37,6 @@ pub struct App { pub is_app_directory_approved: Option, pub is_internal: Option, pub additional_info: Option, - pub user: Option, - pub team: Option, - pub scopes: Option>, - pub message: Option, } #[skip_serializing_none] diff --git a/src/users/user.rs b/src/users/user.rs index 95aa3bf..2613763 100644 --- a/src/users/user.rs +++ b/src/users/user.rs @@ -38,6 +38,7 @@ pub struct User { pub team_id: Option, pub name: Option, pub deleted: Option, + pub email: Option, pub color: Option, pub real_name: Option, pub tz: Option, From ee62688df29e1ffce373e08b6440cdae960e0f2c Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 10 Apr 2023 10:12:29 +0200 Subject: [PATCH 05/19] test: adds tests for Events API for `call_rejected` and others --- src/event_api/event.rs | 79 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 29f8ec9..ae2e132 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -88,7 +88,18 @@ pub enum EventType { app_request: AppRequest }, /// Your Slack app was uninstalled. + /// + /// AppUninstalled, + /// A call was rejected + /// + /// + CallRejected { + call_id: String, + channel_id: String, + external_unique_id: String, + user_id: String, + }, /// A channel was archived #[serde(rename = "channel_archive")] ChannelArchive { channel: String, user: String }, @@ -311,10 +322,20 @@ mod test { fn deserializes_accounts_changed() { let json = r##" { - "type": "accounts_changed" - }"##; - let event = serde_json::from_str::(json).unwrap(); - match event { + "token": "12345FVmRUzNDOAu12345h", + "team_id": "TL1BBBQBD", + "api_app_id": "BBBU04BB4", + "event": { + "type": "accounts_changed" + }, + "type": "event_callback", + "event_id": "EvLLACMB6BB", + "event_time": 1563448153, + "authed_users": ["UBBB1TYR5"] + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { EventType::AccountsChanged{..} => assert!(true), _ => panic!("Did not deserialize into expected variant"), } @@ -355,6 +376,56 @@ mod test { } } + #[test] + fn deserializes_call_rejected() { + let json = r##" + { + "token": "12345FVmRUzNDOAu12345h", + "team_id": "TL1BBBQBD", + "api_app_id": "BBBU04BB4", + "event": { + "type": "call_rejected", + "call_id": "RL731AVEF", + "user_id": "ULJS1TYR5", + "channel_id": "DL5JN9K0T", + "external_unique_id": "123-456-7890" + }, + "type": "event_callback", + "event_id": "EvLLACMB6BB", + "event_time": 1563448153, + "authed_users": ["UBBB1TYR5"] + } + "##; + let event = serde_json::from_str::(json).unwrap(); + println!("{:?}", event.event); + match event.event { + EventType::CallRejected{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant CallRejected"), + } + } + + #[test] + fn deserializes_app_uninstalled() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "app_uninstalled" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::AppUninstalled{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 57b378ba3130ff8a54aa41722623d2562faaae94 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Thu, 13 Apr 2023 10:45:12 +0200 Subject: [PATCH 06/19] test: adds additional tests for Event API events Also adds links to Event API documentation for each variant for convenience --- src/event_api/event.rs | 121 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 6 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index ae2e132..ebf344b 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -101,15 +101,30 @@ pub enum EventType { user_id: String, }, /// A channel was archived + /// + /// #[serde(rename = "channel_archive")] - ChannelArchive { channel: String, user: String }, + ChannelArchive { + channel: String, + user: String + }, /// A channel was created + /// + /// #[serde(rename = "channel_created")] - ChannelCreated { channel: Channel }, + ChannelCreated { + channel: Channel + }, /// A channel was deleted + /// + /// #[serde(rename = "channel_deleted")] - ChannelDeleted { channel: String }, + ChannelDeleted { + channel: String + }, /// Bulk updates were made to a channel's history + /// + /// #[serde(rename = "channel_history_changed")] ChannelHistoryChanged { latest: String, @@ -117,6 +132,8 @@ pub enum EventType { event_ts: String, }, /// A channel ID changed + /// + /// #[serde(rename = "channel_id_changed")] ChannelIDChanged { old_channel_id: String, @@ -124,12 +141,22 @@ pub enum EventType { event_ts: String, }, /// You left a channel + /// + /// #[serde(rename = "channel_left")] - ChannelLeft { channel: String }, + ChannelLeft { + channel: String + }, /// A channel was renamed + /// + /// #[serde(rename = "channel_rename")] - ChannelRename { channel: Channel }, + ChannelRename { + channel: Channel + }, /// A channel has been shared with an external workspace + /// + /// #[serde(rename = "channel_shared")] ChannelShared { connected_team_id: String, @@ -137,9 +164,16 @@ pub enum EventType { event_ts: String, }, /// A channel was unarchived + /// + /// #[serde(rename = "channel_unarchive")] - ChannelUnarchive { channel: String, user: String }, + ChannelUnarchive { + channel: String, + user: String + }, ///A channel has been unshared with an external workspace + /// + /// #[serde(rename = "channel_unshared")] ChannelUnshared { previously_connected_team_id: String, @@ -426,6 +460,81 @@ mod test { } } + #[test] + fn deserializes_channel_archive() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "channel_archive", + "channel": "C024BE91L", + "user": "U024BE7LH" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::ChannelArchive{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + + #[test] + fn deserializes_channel_created() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "channel_created", + "channel": { + "id": "C024BE91L", + "name": "fun", + "created": 1360782804, + "creator": "U024BE7LH" + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::ChannelCreated{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant"), + } + } + + #[test] + fn deserializes_channel_deleted() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "channel_deleted", + "channel": "C024BE91L" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::ChannelDeleted{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant ChannelDeleted"), + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From ab82b234423aff735518efe7bc53e93e8f23d860 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Thu, 13 Apr 2023 10:59:15 +0200 Subject: [PATCH 07/19] feat: adds missing Event API types (dnd_updated, dnd_updated_user) --- src/event_api/event.rs | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index ebf344b..2110cd9 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -181,6 +181,29 @@ pub enum EventType { is_ext_shared: bool, event_ts: String, }, + /// Do not Disturb settings changed for the current user + /// + /// + #[serde(rename = "dnd_updated")] + DoNotDisturbUpdated { + user: String, + dnd_status: DoNotDisturbStatus + }, + /// Do not Disturb settings changed for a member + /// + /// + #[serde(rename = "dnd_updated_user")] + DoNotDisturbUpdatedUser { + user: String, + dnd_status: DoNotDisturbStatus + }, + /// The workspace email domain has changed + /// + /// + EmailDomainChanged { + email_domain: String, + event_ts: String, + }, /// A custom emoji has been added or changed #[serde(rename = "emoji_changed")] EmojiChanged { @@ -280,6 +303,15 @@ pub enum EventType { Other, } +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct DoNotDisturbStatus { + pub dnd_enabled: bool, + pub next_dnd_start_ts: u32, + pub next_dnd_end_ts: u32, + pub snooze_enabled: Option, + pub snooze_endtime: Option, +} + #[cfg(test)] mod test { use assert_json_diff::*; @@ -535,6 +567,64 @@ mod test { } } + #[test] + fn deserializes_do_not_disturb_updated() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "dnd_updated", + "user": "U1234", + "dnd_status": { + "dnd_enabled": true, + "next_dnd_start_ts": 1450387800, + "next_dnd_end_ts": 1450423800, + "snooze_enabled": true, + "snooze_endtime": 1450373897 + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::DoNotDisturbUpdated{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant DoNotDisturbUpdated"), + } + } + + #[test] + fn deserializes_do_not_disturb_updated_user() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "dnd_updated_user", + "user": "U1234", + "dnd_status": { + "dnd_enabled": true, + "next_dnd_start_ts": 1450387800, + "next_dnd_end_ts": 1450423800 + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::DoNotDisturbUpdatedUser{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant DoNotDisturbUpdatedUser"), + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 9ded706997be11f1d86e89d6d030d3d2845a320b Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Thu, 13 Apr 2023 13:47:05 +0200 Subject: [PATCH 08/19] refactor: update `emoji_changed` event to support subtypes --- src/event_api/event.rs | 91 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 2110cd9..babd832 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -205,12 +205,10 @@ pub enum EventType { event_ts: String, }, /// A custom emoji has been added or changed + /// + /// #[serde(rename = "emoji_changed")] - EmojiChanged { - subtype: String, - names: Vec, - event_ts: String, - }, + EmojiChanged(EmojiSubtype), /// An enterprise grid migration has finished on this workspace. GridMigrationFinished, /// An enterprise grid migration has started on this workspace. @@ -312,10 +310,30 @@ pub struct DoNotDisturbStatus { pub snooze_endtime: Option, } +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[serde(tag = "subtype", rename_all = "snake_case")] +pub enum EmojiSubtype { + Add { + name: String, + value: String, + event_ts: String, + }, + Remove { + names: Vec, + event_ts: String, + }, + Rename { + old_name: String, + new_name: String, + value: String, + event_ts: String, + } +} + #[cfg(test)] mod test { use assert_json_diff::*; - use crate::event_api::event::{Event, EventType}; + use crate::event_api::event::{Event, EventType, EmojiSubtype}; #[test] fn deserialize_app_home_opened_event() { @@ -625,6 +643,67 @@ mod test { } } + #[test] + fn deserializes_emoji_changed() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "emoji_changed", + "subtype": "add", + "name": "picard_facepalm", + "value": "https://my.slack.com/emoji/picard_facepalm/db8e287430eaa459.gif", + "event_ts" : "1361482916.000004" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::EmojiChanged(subtype) => { + match subtype { + EmojiSubtype::Add{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant EmojiSubtype::Add") + } + }, + _ => panic!("Did not deserialize into expected variant EmojiChanged"), + } + } + + #[test] + fn deserializes_emoji_changed_removed() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "emoji_changed", + "subtype": "remove", + "names": ["picard_facepalm"], + "event_ts" : "1361482916.000004" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::EmojiChanged(subtype) => { + match subtype { + EmojiSubtype::Remove{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant EmojiSubtype::Remove") + } + }, + _ => panic!("Did not deserialize into expected variant EmojiChanged"), + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From cbe7b5722ccecd51641c7228964253647e51bd35 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Fri, 14 Apr 2023 10:24:52 +0200 Subject: [PATCH 09/19] feat: adds various Event API for file operations (shared, created, etc) --- src/event_api/event.rs | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index babd832..7486d00 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -3,6 +3,7 @@ use crate::channels::channel::Channel; use crate::event_api::app::AppRequest; use crate::event_api::messages::{MessageBasic, MessageSubtype}; +use crate::files::file::{File}; use crate::team::teams::Team; use crate::views::view::View; use serde::{Deserialize, Serialize}; @@ -209,6 +210,51 @@ pub enum EventType { /// #[serde(rename = "emoji_changed")] EmojiChanged(EmojiSubtype), + /// A file was changed + /// + /// + FileChange { + file_id: String, + file: File, + }, + /// A file was created + /// + /// + FileCreated { + file_id: String, + file: File, + }, + /// A file was deleted + /// + /// + FileDeleted { + file_id: String, + event_ts: String, + }, + /// A file was made public + /// + /// + FilePublic { + file_id: String, + file: File, + }, + /// A file was shared + /// + /// + FileShared { + channel_id: String, + event_ts: String, + file_id: String, + file: File, + user_id: String, + }, + /// A file was unshared + /// + /// + FileUnshared { + file_id: String, + file: File, + }, /// An enterprise grid migration has finished on this workspace. GridMigrationFinished, /// An enterprise grid migration has started on this workspace. @@ -704,6 +750,32 @@ mod test { } } + #[test] + fn deserializes_file_change() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "file_change", + "file_id": "F2147483862", + "file": { + "id": "F2147483862" + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::FileChange{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant EmojiSubtype::Remove") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 8aac7de6a91096901ebdd0f752fef78c4a0d1262 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Fri, 14 Apr 2023 10:36:51 +0200 Subject: [PATCH 10/19] chore: formatting and documentation for group events in Events API --- src/event_api/event.rs | 66 +++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 7486d00..ec76b55 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -256,37 +256,69 @@ pub enum EventType { file: File, }, /// An enterprise grid migration has finished on this workspace. - GridMigrationFinished, + /// + /// + GridMigrationFinished { + enterprise_id: String, + }, /// An enterprise grid migration has started on this workspace. - GridMigrationStarted, + /// + /// + GridMigrationStarted { + enterprise_id: String, + }, /// A private channel was archived - #[serde(rename = "group_archive")] - GroupArchive { channel: String }, + /// + /// + GroupArchive { + channel: String + }, /// You closed a private channel - #[serde(rename = "group_close")] - GroupClose { user: String, channel: String }, + /// + /// + GroupClose { + user: String, + channel: String + }, /// A private channel was deleted - #[serde(rename = "group_deleted")] - GroupDeleted { channel: String }, + /// + /// + GroupDeleted { + channel: String + }, /// A private channel was deleted - #[serde(rename = "group_history_changed")] + /// + /// GroupHistoryChanged { latest: String, ts: String, event_ts: String, }, /// You left a private channel - #[serde(rename = "group_left")] - GroupLeft { channel: String }, + /// + /// + GroupLeft { + channel: String + }, /// You created a group DM - #[serde(rename = "group_open")] - GroupOpen { user: String, channel: String }, + /// + /// + GroupOpen { + user: String, + channel: String + }, /// A private channel was renamed - #[serde(rename = "group_rename")] - GroupRename { channel: Channel }, + /// + /// + GroupRename { + channel: Channel + }, /// A private channel was unarchived - #[serde(rename = "group_unarchive")] - GroupUnarchive { channel: String }, + /// + /// + GroupUnarchive { + channel: String + }, /// You closed a DM #[serde(rename = "im_close")] ImClose { user: String, channel: String }, From 4cc97fd4ba2004f023501aec5194eea133f10039 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sat, 15 Apr 2023 11:39:32 +0200 Subject: [PATCH 11/19] feat: implement `link_shared` from Event API --- src/event_api/event.rs | 106 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index ec76b55..9f27681 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -320,23 +320,37 @@ pub enum EventType { channel: String }, /// You closed a DM - #[serde(rename = "im_close")] - ImClose { user: String, channel: String }, + /// + /// + ImClose { + user: String, + channel: String + }, /// A DM was created - #[serde(rename = "im_created")] - ImCreated { user: String, channel: Channel }, + /// + /// + ImCreated { + user: String, + channel: Channel + }, /// Bulk updates were made to a DM's history - #[serde(rename = "im_history_changed")] + /// + /// ImHistoryChanged { latest: String, ts: String, event_ts: String, }, /// You opened a DM - #[serde(rename = "im_open")] - ImOpen { user: String, channel: String }, + /// + /// + ImOpen { + user: String, + channel: String + }, /// User requested an invite - #[serde(rename = "invite_requested")] + /// + /// InviteRequested { id: String, email: String, @@ -349,10 +363,19 @@ pub enum EventType { request_reason: String, team: Team, }, - // TODO: To be implemented in the future /// A message was posted containing one or more links relevant to your application - #[serde(rename = "link_shared")] - LinkShared, + /// + /// + LinkShared { + channel: String, + is_bot_user_member: bool, + user: String, + message_ts: String, + unfurl_id: String, + thread_ts: String, + source: LinkSource, + links: Vec, + }, /// A user joined a public or private channel #[serde(rename = "member_joined_channel")] MemberJoinedChannel { @@ -408,6 +431,20 @@ pub enum EmojiSubtype { } } +#[derive(Deserialize, Serialize, Debug, PartialEq)] +// TODO implement a better type for URL (besides a string) +pub struct Link { + pub domain: String, + pub url: String, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum LinkSource { + Composer, + ConversationsHistory, +} + #[cfg(test)] mod test { use assert_json_diff::*; @@ -808,6 +845,53 @@ mod test { } } + #[test] + fn deserializes_link_shared() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "link_shared", + "channel": "Cxxxxxx", + "is_bot_user_member": true, + "user": "Uxxxxxxx", + "message_ts": "123456789.9875", + "unfurl_id": "C123456.123456789.987501.1b90fa1278528ce6e2f6c5c2bfa1abc9a41d57d02b29d173f40399c9ffdecf4b", + "thread_ts": "123456621.1855", + "source": "conversations_history", + "links": [ + { + "domain": "example.com", + "url": "https://example.com/12345" + }, + { + "domain": "example.com", + "url": "https://example.com/67890" + }, + { + "domain": "another-example.com", + "url": "https://yet.another-example.com/v/abcde" + } + ] + }, + "type": "event_callback", + "authed_users": [ + "UXXXXXXX1", + "UXXXXXXX2" + ], + "event_id": "Ev08MFMKH6", + "event_time": 123456789 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::LinkShared{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant EventType::LinkShared") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 344f23c29537e33e0eee65e8e8c5a36e1723be03 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sat, 15 Apr 2023 11:59:05 +0200 Subject: [PATCH 12/19] refactor: clean up member_joined_channel in Events API --- src/event_api/event.rs | 46 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 9f27681..1ab5b51 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -377,20 +377,22 @@ pub enum EventType { links: Vec, }, /// A user joined a public or private channel - #[serde(rename = "member_joined_channel")] + /// + /// MemberJoinedChannel { user: String, channel: String, - channel_type: String, + channel_type: ChannelType, team: String, inviter: String, }, /// A user left a public or private channel - #[serde(rename = "member_left_channel")] + /// + /// MemberLeftChannel { user: String, channel: String, - channel_type: String, + channel_type: ChannelType, team: String, }, /// A message was sent to a channel @@ -445,6 +447,15 @@ pub enum LinkSource { ConversationsHistory, } +/// See for more +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub enum ChannelType { + #[serde(rename = "C")] + Public, + #[serde(rename = "G")] + Private +} + #[cfg(test)] mod test { use assert_json_diff::*; @@ -892,6 +903,33 @@ mod test { } } + #[test] + fn deserializes_member_joined_channel() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "member_joined_channel", + "user": "W06GH7XHN", + "channel": "C0698JE0H", + "channel_type": "C", + "team": "T024BE7LD", + "inviter": "U123456789" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::MemberJoinedChannel{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant MemberJoinedChannel") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From c1a2bf5f4d19f75f8de20d676ec7814538c5c4dc Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sun, 16 Apr 2023 10:30:42 +0200 Subject: [PATCH 13/19] feat: implement `message_metadata_posted` in Events API types --- src/event_api/event.rs | 57 ++++++++++++++++++++++++++++++++++++++- src/event_api/messages.rs | 15 +++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 1ab5b51..d6ea5bd 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -2,7 +2,7 @@ use crate::channels::channel::Channel; use crate::event_api::app::AppRequest; -use crate::event_api::messages::{MessageBasic, MessageSubtype}; +use crate::event_api::messages::{MessageBasic, MessageSubtype, MessageMetadata}; use crate::files::file::{File}; use crate::team::teams::Team; use crate::views::view::View; @@ -400,6 +400,20 @@ pub enum EventType { Message(MessageBasic), #[serde(rename = "message")] MessageSubtype(MessageSubtype), + + /// Message metadata was posted + /// + /// + MessageMetadataPosted { + app_id: String, + bot_id: String, + channel_id: String, + event_ts: String, + message_ts: String, + metadata: MessageMetadata, + team_id: String, + user_id: String, + }, #[serde(other)] Other, } @@ -930,6 +944,47 @@ mod test { } } + #[test] + fn deserializes_message_metadata_posted() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "message_metadata_posted", + "app_id": "AQF4F123M", + "bot_id": "B8241P2B34D", + "user_id": "UA8829BFL", + "team_id": "T12F3JCAP", + "channel_id": "CJN879K8A", + "metadata": + { + "event_type": "task_created", + "event_payload": + { + "id": "TK-2132", + "summary": "New issue with the display of mobile element", + "description": "An end user has found a problem with the new mobile container for data entry. It was reproduced in the current version of IOS.", + "priority": "HIGH", + "resource_type": "TASK" + } + }, + "message_ts": "1658903885.673769", + "event_ts": "1658903885.673769" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::MessageMetadataPosted{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant MessageMetadataPosted") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ diff --git a/src/event_api/messages.rs b/src/event_api/messages.rs index a908f1d..d269bc6 100644 --- a/src/event_api/messages.rs +++ b/src/event_api/messages.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use serde_json::Value; #[derive(Deserialize, Serialize, Debug, PartialEq)] #[serde(tag = "subtype")] @@ -66,6 +67,20 @@ pub struct MessageUpdate { pub user: String, } +#[derive(Deserialize, Serialize, Debug, PartialEq)] +/// Metadata Events are structured data payloads that contain information about +/// events occurring in your Slack-connected application, in the form of a +/// custom `event_payload` as part of a message's `metadata` property. +/// +/// See for more +/// +/// Note here that we use the `serde_json::Value` since the `event_payload` can +/// be dynamic (user-defined keys and values) which cannot be represented here +pub struct MessageMetadata { + pub event_type: String, + pub event_payload: Value, +} + #[cfg(test)] mod tests { use crate::event_api::messages::*; From a8a33af04f548222f70e08293d6730363c550245 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 17 Apr 2023 11:17:10 +0200 Subject: [PATCH 14/19] feat: implement `message_metadata_deleted`, `message_metadata_updated` events --- src/event_api/event.rs | 123 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index d6ea5bd..7a198e8 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -400,7 +400,20 @@ pub enum EventType { Message(MessageBasic), #[serde(rename = "message")] MessageSubtype(MessageSubtype), - + /// Message metadata was deleted + /// + /// + MessageMetadataDeleted { + app_id: String, + bot_id: String, + channel_id: String, + deleted_ts: String, + event_ts: String, + message_ts: String, + previous_metadata: MessageMetadata, + team_id: String, + user_id: String, + }, /// Message metadata was posted /// /// @@ -414,6 +427,20 @@ pub enum EventType { team_id: String, user_id: String, }, + /// Message metadata was updated + /// + /// + MessageMetadataUpdated { + app_id: String, + bot_id: String, + channel_id: String, + event_ts: String, + message_ts: String, + metadata: MessageMetadata, + previous_metadata: MessageMetadata, + team_id: String, + user_id: String, + }, #[serde(other)] Other, } @@ -944,6 +971,47 @@ mod test { } } + #[test] + fn deserializes_message_metadata_deleted() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "message_metadata_deleted", + "channel_id": "CJN879K8A", + "event_ts": "1658907498.002500", + "previous_metadata": + { + "event_type": "task_created", + "event_payload": + { + "id": "TK-2135", + "summary": "New issue with the display of mobile element", + "description": "An end user has found a problem with the new mobile container for data entry. It was reproduced in the current version of IOS.", + "priority": "HIGH", + "resource_type": "TASK" + } + }, + "app_id": "AQF4F123M", + "bot_id": "B8241P2B34D", + "user_id": "UA8829BFL", + "team_id": "T12F3JCAP", + "message_ts": "1658905974.587109", + "deleted_ts": "1658907498.002500" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::MessageMetadataDeleted{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant MessageMetadataDeleted") + } + } #[test] fn deserializes_message_metadata_posted() { let json = r##" @@ -985,6 +1053,59 @@ mod test { } } + #[test] + fn deserializes_message_metadata_updated() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "message_metadata_updated", + "channel_id": "CJN879K8A", + "event_ts": "1658906295.002200", + "previous_metadata": + { + "event_type": "task_created", + "event_payload": + { + "id": "TK-2132", + "summary": "New issue with the display of mobile element", + "description": "An end user has found a problem with the new mobile container for data entry. It was reproduced in the current version of IOS.", + "priority": "HIGH", + "resource_type": "TASK" + } + }, + "app_id": "AQF4F123M", + "bot_id": "B8241P2B34D", + "user_id": "UA8829BFL", + "team_id": "T12F3JCAP", + "message_ts": "1658905974.587109", + "metadata": + { + "event_type": "task_created", + "event_payload": + { + "id": "TK-2135", + "summary": "New issue with the display of mobile element", + "description": "An end user has found a problem with the new mobile container for data entry. It was reproduced in the current version of IOS.", + "priority": "HIGH", + "resource_type": "TASK" + } + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::MessageMetadataUpdated{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant MessageMetadataUpdated") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 3a10be8b7663b146ef12a71e091931a671422c48 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 17 Apr 2023 11:38:34 +0200 Subject: [PATCH 15/19] feat: implements several new Event API events Adds `reaction_added`, `reaction_removed`, `pin_added`, `pin_removed` events --- src/event_api/event.rs | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 7a198e8..a0b61c1 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -3,6 +3,7 @@ use crate::channels::channel::Channel; use crate::event_api::app::AppRequest; use crate::event_api::messages::{MessageBasic, MessageSubtype, MessageMetadata}; +use crate::items::item::{Item}; use crate::files::file::{File}; use crate::team::teams::Team; use crate::views::view::View; @@ -441,6 +442,45 @@ pub enum EventType { team_id: String, user_id: String, }, + /// A pin was added to a channel + /// + /// + PinAdded { + channel_id: String, + event_ts: String, + item: Item, + user: String, + }, + /// A pin was removed from a channel + /// + /// + PinRemoved { + channel_id: String, + event_ts: String, + has_pins: bool, + item: Item, + user: String, + }, + /// A member added an emoji reaction + /// + /// + ReactionAdded { + event_ts: String, + item: Item, + item_user: String, + reaction: String, + user: String, + }, + /// A member removed an emoji reaction + /// + /// + ReactionRemoved { + event_ts: String, + item: Item, + item_user: String, + reaction: String, + user: String, + }, #[serde(other)] Other, } @@ -1106,6 +1146,37 @@ mod test { } } + #[test] + fn deserializes_reaction_added() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "reaction_added", + "user": "U024BE7LH", + "reaction": "thumbsup", + "item_user": "U0G9QF9C6", + "item": { + "type": "message", + "channel": "C0G9QF9GZ", + "ts": "1360782400.498405" + }, + "event_ts": "1360782804.083113" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::ReactionAdded{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant MessageMetadataUpdated") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ From 15e02c5110de79b005a401e6113364af25e1f4ec Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Tue, 18 Apr 2023 10:40:43 +0200 Subject: [PATCH 16/19] feat: adds Event API types for shared channel actions Adds events for `shared_channel_invite_received`, `shared_channel_invite_declined`, `shared_channel_invite_approved`, `shared_channel_invite_rejected` Fixes an typo for field in `UserProfile` --- src/event_api/event.rs | 203 ++++++++++++++++++++++++++++++++++++++++- src/invites/invites.rs | 15 +++ src/invites/mod.rs | 1 + src/lib.rs | 1 + src/team/info.rs | 4 + src/team/teams.rs | 2 + src/users/user.rs | 3 +- 7 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 src/invites/invites.rs create mode 100644 src/invites/mod.rs diff --git a/src/event_api/event.rs b/src/event_api/event.rs index a0b61c1..b6c71b4 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -3,9 +3,11 @@ use crate::channels::channel::Channel; use crate::event_api::app::AppRequest; use crate::event_api::messages::{MessageBasic, MessageSubtype, MessageMetadata}; +use crate::invites::invites::{Invite}; use crate::items::item::{Item}; use crate::files::file::{File}; -use crate::team::teams::Team; +use crate::team::teams::{Team}; +use crate::users::user::{User}; use crate::views::view::View; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -481,6 +483,47 @@ pub enum EventType { reaction: String, user: String, }, + /// A shared channel invite was accepted + /// + /// + SharedChannelInviteAccepted { + accepting_user: User, + approval_required: bool, + channel: Channel, + event_ts: String, + invite: Invite, + teams_in_channel: Vec, + }, + /// A shared channel invite was approved + /// + /// + SharedChannelInviteApproved { + approving_user: User, + approving_team_id: String, + channel: Channel, + event_ts: String, + invite: Invite, + teams_in_channel: Vec, + }, + /// A shared channel invite was declined + /// + /// + SharedChannelInviteDeclined { + channel: Channel, + declining: User, + declining_team_id: String, + event_ts: String, + invite: Invite, + teams_in_channel: Vec, + }, + /// A shared channel invite was received + /// + /// + SharedChannelInviteReceived { + channel: Channel, + event_ts: String, + invite: Invite, + }, #[serde(other)] Other, } @@ -1146,6 +1189,164 @@ mod test { } } + #[test] + fn deserializes_shared_channel_invite_accepted() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "shared_channel_invite_accepted", + "approval_required": false, + "invite": { + "id": "I028YDERZSQ", + "date_created": 1626876000, + "date_invalid": 1628085600, + "inviting_team": { + "id": "T12345678", + "name": "Corgis", + "is_verified": false, + "domain": "corgis", + "date_created": 1480946400 + }, + "inviting_user": { + "id": "U12345678", + "team_id": "T12345678", + "name": "crus", + "updated": 1608081902, + "profile": { + "real_name": "Corgis Rus", + "display_name": "Corgis Rus", + "real_name_normalized": "Corgis Rus", + "display_name_normalized": "Corgis Rus", + "team": "T12345678", + "avatar_hash": "gcfh83a4c72k", + "email": "corgisrus@slack-corp.com", + "image_24": "https://placekitten.com/24/24", + "image_32": "https://placekitten.com/32/32", + "image_48": "https://placekitten.com/48/48", + "image_72": "https://placekitten.com/72/72", + "image_192": "https://placekitten.com/192/192", + "image_512": "https://placekitten.com/512/512" + } + }, + "recipient_email": "golden@doodle.com", + "recipient_user_id": "U87654321" + }, + "channel": { + "id": "C12345678", + "is_private": false, + "is_im": false, + "name": "test-slack-connect" + }, + "teams_in_channel": [ + { + "id": "T12345678", + "name": "Corgis", + "is_verified": false, + "domain": "corgis", + "date_created": 1626789600 + } + ], + "accepting_user": { + "id": "U87654321", + "team_id": "T87654321", + "name": "golden", + "updated": 1624406113, + "profile": { + "real_name": "Golden Doodle", + "display_name": "Golden", + "real_name_normalized": "Golden Doodle", + "display_name_normalized": "Golden", + "team": "T87654321", + "avatar_hash": "g717728b118x", + "email": "golden@doodle.com", + "image_24": "https://placekitten.com/24/24", + "image_32": "https://placekitten.com/32/32", + "image_48": "https://placekitten.com/48/48", + "image_72": "https://placekitten.com/72/72", + "image_192": "https://placekitten.com/192/192", + "image_512": "https://placekitten.com/512/512" + } + }, + "event_ts": "1626877800.000000" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::SharedChannelInviteAccepted{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant SharedChannelInviteAccepted") + } + } + + #[test] + fn deserializes_shared_channel_invite_received() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "shared_channel_invite_received", + "invite": { + "id": "I028YDERZSQ", + "date_created": 1626876000, + "date_invalid": 1628085600, + "inviting_team": { + "id": "T12345678", + "name": "Corgis", + "is_verified": false, + "domain": "corgis", + "date_created": 1480946400 + }, + "inviting_user": { + "id": "U12345678", + "team_id": "T12345678", + "name": "crus", + "updated": 1608081902, + "profile": { + "real_name": "Corgis Rus", + "display_name": "Corgis Rus", + "real_name_normalized": "Corgis Rus", + "display_name_normalized": "Corgis Rus", + "team": "T12345678", + "avatar_hash": "gcfh83a4c72k", + "email": "corgisrus@slack-corp.com", + "image_24": "https://placekitten.com/24/24", + "image_32": "https://placekitten.com/32/32", + "image_48": "https://placekitten.com/48/48", + "image_72": "https://placekitten.com/72/72", + "image_192": "https://placekitten.com/192/192", + "image_512": "https://placekitten.com/512/512" + } + }, + "recipient_user_id": "U87654321" + }, + "channel": { + "id": "C12345678", + "is_private": false, + "is_im": false, + "name": "test-slack-connect" + }, + "event_ts": "1626876010.000100" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::SharedChannelInviteReceived{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant SharedChannelInviteReceived") + } + } + #[test] fn deserializes_reaction_added() { let json = r##" diff --git a/src/invites/invites.rs b/src/invites/invites.rs new file mode 100644 index 0000000..c159a17 --- /dev/null +++ b/src/invites/invites.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use crate::team::teams::{Team}; +use crate::users::user::{User}; + +#[derive(Deserialize, Serialize, Debug, Default, PartialEq)] +pub struct Invite { + pub id: String, + pub date_created: u32, + pub date_invalid: u32, + pub inviting_team: Team, + pub inviting_user: User, + pub recipient_email: Option, + pub recipient_user_id: String, +} diff --git a/src/invites/mod.rs b/src/invites/mod.rs new file mode 100644 index 0000000..ad19db8 --- /dev/null +++ b/src/invites/mod.rs @@ -0,0 +1 @@ +pub mod invites; diff --git a/src/lib.rs b/src/lib.rs index c7b9915..4efc856 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,6 +128,7 @@ pub mod error; pub mod event_api; pub mod files; pub mod http_client; +pub mod invites; pub mod items; pub mod payloads; pub mod profiles; diff --git a/src/team/info.rs b/src/team/info.rs index 33a0634..7b19719 100644 --- a/src/team/info.rs +++ b/src/team/info.rs @@ -67,6 +67,7 @@ mod test { team: Some(Team { id: Some("T12345".to_string()), name: Some("My Team".to_string()), + date_created: None, domain: Some("example".to_string()), email_domain: Some("example.com".to_string()), icon: Some(Icon { @@ -78,6 +79,7 @@ mod test { image_132: Some("https://...".to_string()), image_default: Some(true), }), + is_verified: None, enterprise_id: Some("E1234A12AB".to_string()), enterprise_name: Some("Umbrella Corporation".to_string()), }), @@ -150,6 +152,7 @@ mod test { team: Some(Team { id: Some("T12345".to_string()), name: Some("My Team".to_string()), + date_created: None, domain: Some("example".to_string()), email_domain: Some("example.com".to_string()), icon: Some(Icon { @@ -161,6 +164,7 @@ mod test { image_132: Some("https://...".to_string()), image_default: Some(true), }), + is_verified: None, enterprise_id: Some("E1234A12AB".to_string()), enterprise_name: Some("Umbrella Corporation".to_string()), }), diff --git a/src/team/teams.rs b/src/team/teams.rs index 35a1bc2..33045b2 100644 --- a/src/team/teams.rs +++ b/src/team/teams.rs @@ -6,9 +6,11 @@ use serde_with::skip_serializing_none; pub struct Team { pub id: Option, pub name: Option, + pub date_created: Option, pub domain: Option, pub email_domain: Option, pub icon: Option, + pub is_verified: Option, pub enterprise_id: Option, pub enterprise_name: Option, } diff --git a/src/users/user.rs b/src/users/user.rs index 2613763..c8b1221 100644 --- a/src/users/user.rs +++ b/src/users/user.rs @@ -4,6 +4,7 @@ use serde_with::skip_serializing_none; #[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, Default, PartialEq)] pub struct UserProfile { + pub avatar_hash: Option, pub first_name: Option, pub last_name: Option, pub real_name: Option, @@ -19,7 +20,7 @@ pub struct UserProfile { pub image_72: Option, pub image_192: Option, pub image_512: Option, - pub mage_original: Option, + pub image_original: Option, pub title: Option, pub bot_id: Option, pub api_app_id: Option, From 64c708cd7249254c53344b37385e9e79bf733999 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Sun, 23 Apr 2023 10:04:07 +0200 Subject: [PATCH 17/19] feat: implement the `subteam_created` Event type --- src/event_api/event.rs | 74 +++++++++++++++++++++++++++++++++++++++++- src/team/mod.rs | 1 + src/team/subteams.rs | 41 +++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/team/subteams.rs diff --git a/src/event_api/event.rs b/src/event_api/event.rs index b6c71b4..2d733d0 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -7,10 +7,12 @@ use crate::invites::invites::{Invite}; use crate::items::item::{Item}; use crate::files::file::{File}; use crate::team::teams::{Team}; +use crate::team::subteams::*; use crate::users::user::{User}; use crate::views::view::View; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; +use serde_json::{Value}; /// [Event API](https://api.slack.com/events?filter=Events) /// [Event API Type](https://api.slack.com/events?filter=Events) @@ -524,6 +526,28 @@ pub enum EventType { event_ts: String, invite: Invite, }, + /// A member has starred an item + /// + /// + StarAdded { + event_ts: String, + item: Item, + user: String, + }, + /// A member removed a star + /// + /// + StarRemoved { + event_ts: String, + item: Item, + user: String, + }, + /// A User Group has been added to the workspace + /// + /// + SubteamCreated { + subteam: Subteam, + }, #[serde(other)] Other, } @@ -580,6 +604,7 @@ pub enum ChannelType { Private } + #[cfg(test)] mod test { use assert_json_diff::*; @@ -1378,6 +1403,53 @@ mod test { } } + #[test] + fn deserializes_subteam_created() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "subteam_created", + "subteam": { + "id": "S0615G0KT", + "team_id": "T060RNRCH", + "is_usergroup": true, + "name": "Marketing Team", + "description": "Marketing gurus, PR experts and product advocates.", + "handle": "marketing-team", + "is_external": false, + "date_create": 1446746793, + "date_update": 1446746793, + "date_delete": 0, + "auto_type": null, + "created_by": "U060RNRCZ", + "updated_by": "U060RNRCZ", + "deleted_by": null, + "prefs": { + "channels": [ + + ], + "groups": [ + + ] + }, + "user_count": "0" + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::SubteamCreated{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant SubteamCreated") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ diff --git a/src/team/mod.rs b/src/team/mod.rs index 76d9a58..fb927f0 100644 --- a/src/team/mod.rs +++ b/src/team/mod.rs @@ -7,4 +7,5 @@ pub mod integration_logs; pub mod log; pub mod preferences_list; pub mod profile_get; +pub mod subteams; pub mod teams; diff --git a/src/team/subteams.rs b/src/team/subteams.rs new file mode 100644 index 0000000..5bb11ef --- /dev/null +++ b/src/team/subteams.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::{Value}; + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct Subteam { + pub id: String, + pub team_id: String, + pub is_usergroup: bool, + pub name: String, + pub description: String, + pub handle: String, + pub is_external: bool, + pub date_create: u32, + pub date_update: u32, + pub date_delete: u32, + pub auto_type: Value, + pub created_by: Value, + pub updated_by: Value, + pub deleted_by: Value, + pub prefs: SubteamPrefs, + /// Note that the Slack API examples have this as an integer value encoded + /// as a string + #[serde(deserialize_with = "Subteam::make_int")] + pub user_count: u32, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct SubteamPrefs { + pub channels: Vec, + pub groups: Vec, +} + +impl Subteam { + fn make_int<'de, D>(deserializer: D) -> Result + where D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + From 0aa076f41a018b96a861df5b31bbf9ac9c9529a3 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 24 Apr 2023 20:32:23 +0200 Subject: [PATCH 18/19] feat: implement "subteam_members_changed" event Also introduces a custom deserializer to handle string quoted integer values ("7") and unquoted integer values (7). Note that the Slack API documentation is ambiguous and often uses both --- src/event_api/event.rs | 130 ++++++++++++++++++++++++++++++++++++++++- src/team/subteams.rs | 40 ++++++++----- 2 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 2d733d0..4b31a54 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -10,9 +10,8 @@ use crate::team::teams::{Team}; use crate::team::subteams::*; use crate::users::user::{User}; use crate::views::view::View; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use serde_json::{Value}; /// [Event API](https://api.slack.com/events?filter=Events) /// [Event API Type](https://api.slack.com/events?filter=Events) @@ -548,6 +547,39 @@ pub enum EventType { SubteamCreated { subteam: Subteam, }, + /// The membership of an existing User Group has changed + /// + /// + SubteamMembersChanged { + added_users: Vec, + #[serde(deserialize_with = "Subteam::make_int")] + added_users_count: u64, + date_previous_update: u32, + date_update: u32, + removed_users: Vec, + #[serde(deserialize_with = "Subteam::make_int")] + removed_users_count: u64, + subteam_id: String, + team_id: String, + }, + /// You have been added to a User Group + /// + /// + SubteamSelfAdded { + subteam_id: String, + }, + /// You have been removed from a User Group + /// + /// + SubteamSelfRemoved { + subteam_id: String, + }, + /// An existing User Group has been updated or its members changed + /// + /// + SubteamUpdated { + subteam: Subteam, + }, #[serde(other)] Other, } @@ -1403,6 +1435,42 @@ mod test { } } + #[test] + fn deserializes_subteam_members_changed() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "subteam_members_changed", + "subteam_id": "S0614TZR7", + "team_id": "T060RNRCH", + "date_previous_update": 1446670362, + "date_update": 1492906952, + "added_users": [ + "U060RNRCZ", + "U060ULRC0", + "U061309JM" + ], + "added_users_count": "3", + "removed_users": [ + "U06129G2V" + ], + "removed_users_count": "1" + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::SubteamMembersChanged{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant SubteamMembersChanged") + } + } + #[test] fn deserializes_subteam_created() { let json = r##" @@ -1450,6 +1518,64 @@ mod test { } } + #[test] + fn deserializes_subteam_updated() { + let json = r##" + { + "token": "XXYYZZ", + "team_id": "TXXXXXXXX", + "api_app_id": "AXXXXXXXXX", + "event": { + "type": "subteam_updated", + "subteam": { + "id": "S516MPG9X", + "team_id": "T0GMXV71T", + "is_usergroup": true, + "is_subteam": true, + "name": "My User Group Test", + "description": "User Group Test", + "handle": "user-group-test", + "is_external": false, + "date_create": 1492655498, + "date_update": 1595814882, + "date_delete": 0, + "auto_type": null, + "auto_provision": false, + "enterprise_subteam_id": "", + "created_by": "U0GN26UBG", + "updated_by": "U0GN26UBG", + "deleted_by": null, + "prefs": { + "channels": [ + "CJG07GZDK" + ], + "groups": [] + }, + "users": [ + "U0GN26UBG", + "U0GNBSG1G", + "U0K0XMM2R", + "U1FLR7FB8", + "U3S7347ED", + "U5ZR5M0FM", + "U600XRXS9" + ], + "user_count": "7", + "channel_count": 0 + } + }, + "type": "event_callback", + "event_id": "EvXXXXXXXX", + "event_time": 1234567890 + } + "##; + let event = serde_json::from_str::(json).unwrap(); + match event.event { + EventType::SubteamUpdated{..} => assert!(true), + _ => panic!("Did not deserialize into expected variant SubteamUpdated") + } + } + #[test] fn deserialize_unknown_event() { let json = r##"{ diff --git a/src/team/subteams.rs b/src/team/subteams.rs index 5bb11ef..4bc583d 100644 --- a/src/team/subteams.rs +++ b/src/team/subteams.rs @@ -3,25 +3,30 @@ use serde_json::{Value}; #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct Subteam { - pub id: String, - pub team_id: String, - pub is_usergroup: bool, - pub name: String, - pub description: String, - pub handle: String, - pub is_external: bool, - pub date_create: u32, - pub date_update: u32, - pub date_delete: u32, pub auto_type: Value, + pub auto_provision: Option, + pub channel_count: Option, pub created_by: Value, - pub updated_by: Value, + pub date_create: u32, + pub date_delete: u32, + pub date_update: u32, pub deleted_by: Value, + pub description: String, + pub enterprise_subteam_id: Option, + pub handle: String, + pub id: String, + pub is_external: bool, + pub is_usergroup: bool, + pub is_subteam: Option, + pub name: String, pub prefs: SubteamPrefs, + pub team_id: String, + pub updated_by: Value, + pub users: Option>, /// Note that the Slack API examples have this as an integer value encoded /// as a string #[serde(deserialize_with = "Subteam::make_int")] - pub user_count: u32, + pub user_count: u64, } #[derive(Deserialize, Serialize, Debug, PartialEq)] @@ -31,11 +36,16 @@ pub struct SubteamPrefs { } impl Subteam { - fn make_int<'de, D>(deserializer: D) -> Result + /// Implement a custom deserializer to handle both string quoted integer values and unquoted + /// integer values + pub fn make_int<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) + match serde::Deserialize::deserialize(deserializer)? { + Value::String(s) => s.parse().map_err(serde::de::Error::custom), + Value::Number(num) => num.as_u64().ok_or_else(|| serde::de::Error::custom(format!("Could not deserialize {} as number", num))), + _ => return Err(serde::de::Error::custom("invalid type")), + } } } From 942bf4328356716a2aead92b1d5d885ffdc20a06 Mon Sep 17 00:00:00 2001 From: Evan Reeves Date: Mon, 24 Apr 2023 20:45:04 +0200 Subject: [PATCH 19/19] feat: implements additional "team_xxx" from Events API --- src/event_api/event.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/event_api/event.rs b/src/event_api/event.rs index 4b31a54..2255fcb 100644 --- a/src/event_api/event.rs +++ b/src/event_api/event.rs @@ -580,6 +580,39 @@ pub enum EventType { SubteamUpdated { subteam: Subteam, }, + /// Access to a set of teams was granted to your org app + /// + /// + TeamAccessGranted { + team_ids: Vec + }, + /// Access to a set of teams was revoked from your org app + /// + /// + TeamAccessRevoked { + team_ids: Vec + }, + /// The workspace domain has changed + /// + /// + TeamAccessRevoked { + domain: String, + team_id: String, + url: String, + }, + /// A new member has joined + /// + /// + TeamAccessRevoked { + user: User, + }, + /// The workspace name has changed + /// + /// + TeamRename { + name: String, + team_id: String, + }, #[serde(other)] Other, }