Skip to content

Commit 4ae3487

Browse files
committed
add: initial with some message handling
0 parents  commit 4ae3487

File tree

7 files changed

+223
-0
lines changed

7 files changed

+223
-0
lines changed

.editorconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[*.rs]
2+
indent_style = space
3+
indent_size = 4
4+
end_of_line = lf
5+
trim_trailing_whitespace = true
6+
insert_final_newline = true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
.vscode

Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "slack-socket-mode-client"
3+
version = "0.1.0"
4+
authors = ["S.Percentage <Syn.Tri.Naga@gmail.com>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[features]
10+
default = []
11+
runtime-async-std = ["async-std", "surf"]
12+
13+
[dependencies]
14+
serde = { version = "1.0", features = ["derive"] }
15+
serde_json = "1.0"
16+
tungstenite = "0.12"
17+
async-tungstenite = "0.12"
18+
url = "2.2"
19+
futures-util = "0.3"
20+
log = "0.4"
21+
async-tls = { version = "0.11", default-features = false, features = ["client"] }
22+
23+
async-std = { version = "1.9", optional = true }
24+
surf = { version = "2.1", optional = true }
25+
26+
[dev-dependencies]
27+
env_logger = "0.5"
28+
async-std = { version = "1.9", features = ["attributes"] }

examples/simple.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#[async_std::main]
2+
async fn main() {
3+
env_logger::init();
4+
5+
let dr = slack_socket_mode_client::run(env!("SLACK_APP_TOKEN"), &mut EventHandler)
6+
.await
7+
.expect("Failed to run socket mode client");
8+
println!("disconnected: {:?}", dr);
9+
}
10+
11+
pub struct EventHandler;
12+
impl slack_socket_mode_client::EventHandler for EventHandler {
13+
fn on_hello(
14+
&mut self,
15+
_: slack_socket_mode_client::protocol::ConnectionInfo,
16+
_: u32,
17+
d: slack_socket_mode_client::protocol::DebugInfo,
18+
) {
19+
println!("Hello! approx_connection_time: {}s", d.approximate_connection_time.unwrap_or(0));
20+
}
21+
}

rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
max_width = 120

src/lib.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! Slack Socket Mode API Client Library
2+
3+
use futures_util::{SinkExt, StreamExt};
4+
use url::Url;
5+
6+
pub mod protocol;
7+
8+
#[allow(unused_variables)]
9+
pub trait EventHandler {
10+
fn on_hello(
11+
&mut self,
12+
connection_info: protocol::ConnectionInfo,
13+
num_connections: u32,
14+
debug_info: protocol::DebugInfo,
15+
) {
16+
}
17+
18+
fn on_events_api(&mut self) {}
19+
}
20+
21+
#[derive(Debug, Clone)]
22+
pub enum DisconnectReason {
23+
RefreshRequested,
24+
Other(String),
25+
Unknown,
26+
}
27+
28+
#[derive(Debug)]
29+
pub enum RunError {
30+
HttpClientError(HttpClientError),
31+
OpenConnectionApiError(Option<String>),
32+
UrlParseError(url::ParseError),
33+
#[cfg(feature = "runtime-async-std")]
34+
TcpStreamConnectionError(async_std::io::Error),
35+
TlsConnectionError(std::io::Error),
36+
WebSocketError(tungstenite::Error),
37+
}
38+
impl From<HttpClientError> for RunError {
39+
fn from(e: HttpClientError) -> Self {
40+
Self::HttpClientError(e)
41+
}
42+
}
43+
impl From<url::ParseError> for RunError {
44+
fn from(e: url::ParseError) -> Self {
45+
Self::UrlParseError(e)
46+
}
47+
}
48+
impl From<tungstenite::Error> for RunError {
49+
fn from(e: tungstenite::Error) -> Self {
50+
Self::WebSocketError(e)
51+
}
52+
}
53+
54+
pub async fn run<H: EventHandler + ?Sized>(token: &str, handler: &mut H) -> Result<DisconnectReason, RunError> {
55+
let ws_url = open_connection(token)
56+
.await?
57+
.map_err(RunError::OpenConnectionApiError)?;
58+
let ws_parsed = Url::parse(&ws_url)?;
59+
let ws_domain = ws_parsed.domain().expect("WebSocket URL doesn't have domain");
60+
61+
#[cfg(feature = "runtime-async-std")]
62+
let tcp_stream = async_std::net::TcpStream::connect((ws_domain, 443))
63+
.await
64+
.map_err(RunError::TcpStreamConnectionError)?;
65+
let enc_stream = async_tls::TlsConnector::default()
66+
.connect(ws_domain, tcp_stream)
67+
.await
68+
.map_err(RunError::TlsConnectionError)?;
69+
let (mut ws, _) = async_tungstenite::client_async(&ws_url, enc_stream).await?;
70+
71+
while let Some(msg) = ws.next().await {
72+
match msg? {
73+
tungstenite::Message::Text(t) => match serde_json::from_str(&t) {
74+
Ok(protocol::Message::Hello {
75+
num_connections,
76+
connection_info,
77+
debug_info,
78+
}) => {
79+
handler.on_hello(connection_info, num_connections, debug_info);
80+
}
81+
Ok(protocol::Message::Disconnect { reason, .. }) => {
82+
return match reason {
83+
"refresh_requested" => Ok(DisconnectReason::RefreshRequested),
84+
s => Ok(DisconnectReason::Other(String::from(s))),
85+
}
86+
}
87+
Err(e) => {
88+
log::error!("Failed to parse incoming message: {}: {:?}", t, e);
89+
}
90+
},
91+
tungstenite::Message::Ping(p) => {
92+
ws.send(tungstenite::Message::Pong(p)).await?;
93+
}
94+
tungstenite::Message::Close(_) => {
95+
break;
96+
}
97+
m => {
98+
log::warn!("Unsupported WebSocket Message: {:?}", m);
99+
}
100+
}
101+
}
102+
103+
Ok(DisconnectReason::Unknown)
104+
}
105+
106+
#[cfg(feature = "runtime-async-std")]
107+
type HttpClientError = surf::Error;
108+
109+
async fn open_connection(token: &str) -> Result<Result<String, Option<String>>, HttpClientError> {
110+
#[derive(serde::Deserialize)]
111+
pub struct ApiResponse {
112+
ok: bool,
113+
url: Option<String>,
114+
error: Option<String>,
115+
}
116+
let mut tok_bearer = String::with_capacity(token.len() + 7);
117+
tok_bearer.push_str("Bearer ");
118+
tok_bearer.push_str(token);
119+
120+
#[cfg(feature = "runtime-async-std")]
121+
let r: ApiResponse = surf::post("https://slack.com/api/apps.connections.open")
122+
.header(surf::http::headers::AUTHORIZATION, tok_bearer)
123+
.recv_json()
124+
.await?;
125+
126+
Ok(if r.ok {
127+
Ok(r.url.expect("no url returned from api?"))
128+
} else {
129+
Err(r.error)
130+
})
131+
}

src/protocol.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! JSON Types
2+
3+
use serde::Deserialize;
4+
5+
#[derive(Deserialize, Debug)]
6+
#[serde(rename_all = "snake_case", tag = "type")]
7+
pub enum Message<'s> {
8+
Hello {
9+
num_connections: u32,
10+
#[serde(borrow = "'s")]
11+
connection_info: ConnectionInfo<'s>,
12+
#[serde(borrow = "'s")]
13+
debug_info: DebugInfo<'s>,
14+
},
15+
Disconnect {
16+
reason: &'s str,
17+
#[serde(borrow = "'s")]
18+
debug_info: DebugInfo<'s>,
19+
},
20+
}
21+
22+
#[derive(Deserialize, Debug)]
23+
pub struct ConnectionInfo<'s> {
24+
pub app_id: &'s str,
25+
}
26+
27+
#[derive(Deserialize, Debug)]
28+
pub struct DebugInfo<'s> {
29+
pub host: &'s str,
30+
pub started: Option<&'s str>,
31+
pub build_number: Option<u32>,
32+
pub approximate_connection_time: Option<u64>,
33+
}

0 commit comments

Comments
 (0)