slack-flows 0.3.4

Slack extension for flows.network
Documentation
//! Slack integration for [Flows.network](https://flows.network)
//!
//! # Quick Start
//!
//! To get started, the easist way is to write a flow function
//! that acts as a `Hello World` Slack bot.
//!
//! ```
//! use slack_flows::{listen_to_channel, send_message_to_channel};
//!
//! #[no_mangle]
//! pub fn run() {
//!     listen_to_channel("myworkspace", "mychannel", |sm| {
//!         send_message_to_channel("myworkspace", "mychannel", format!("Hello, {}",
//!         sm.text))
//!     }).await;
//! }
//! ```
//!
//! [listen_to_channel()] is responsible for registering a listener for
//! channel `mychannel` of workspace `myworkspace`. Whenever a new message
//! is sent to the channel, the callback closure is called with received
//! message then [send_message_to_channel()]
//! is used to send a response message to the same channel.

use flowsnet_platform_sdk::write_error_log;
use http_req::{
    request::{self, Method, Request},
    uri::Uri,
};
use lazy_static::lazy_static;
use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::io::{self, Write};

lazy_static! {
    static ref SLACK_API_PREFIX: String = String::from(
        std::option_env!("SLACK_API_PREFIX")
            .unwrap_or("https://slack-flows-extension.vercel.app/api")
    );
}

extern "C" {
    // Flag if current running is for listening(1) or message receving(0)
    fn is_listening() -> i32;

    // Return the user id of the flows platform
    fn get_flows_user(p: *mut u8) -> i32;

    // Return the flow id
    fn get_flow_id(p: *mut u8) -> i32;

    fn get_event_body_length() -> i32;
    fn get_event_body(p: *mut u8) -> i32;
    fn set_output(p: *const u8, len: i32);
    fn set_error_code(code: i16);
    // fn redirect_to(p: *const u8, len: i32);
}

/// A struct corresponding to the
/// [Slack message API](https://api.slack.com/events/message)
#[derive(Debug, Serialize, Deserialize)]
pub struct SlackMessage {
    #[serde(rename = "type")]
    pub event_type: String,
    pub channel: String,
    pub user: String,
    pub text: String,
    pub channel_type: String,
}

/// A struct corresponding to the
/// [Slack event API](https://api.slack.com/apis/connections/events-api#the-events-api__receiving-events__callback-field-overview)
#[derive(Debug, Serialize, Deserialize)]
pub struct Event {
    pub challenge: Option<String>,
    pub team_id: Option<String>,
    pub event: Option<SlackMessage>,
}

/// Revoke previous registered listener of current flow.
///
/// Most of the time you do not need to call this function. As inside
/// the [listen_to_channel()] it will revoke previous registered
/// listener, so the only circumstance you need this function is when
/// you want to change the listener from Slack to others.
pub async fn revoke_listeners() {
    unsafe {
        let mut flows_user = Vec::<u8>::with_capacity(100);
        let c = get_flows_user(flows_user.as_mut_ptr());
        flows_user.set_len(c as usize);
        let flows_user = String::from_utf8(flows_user).unwrap();

        let mut flow_id = Vec::<u8>::with_capacity(100);
        let c = get_flow_id(flow_id.as_mut_ptr());
        if c == 0 {
            panic!("Failed to get flow id");
        }
        flow_id.set_len(c as usize);
        let flow_id = String::from_utf8(flow_id).unwrap();

        let mut writer = Vec::new();
        let res = request::get(
            format!(
                "{}/{}/{}/revoke",
                SLACK_API_PREFIX.as_str(),
                flows_user,
                flow_id
            ),
            &mut writer,
        )
        .unwrap();

        match res.status_code().is_success() {
            true => (),
            false => {
                write_error_log!(String::from_utf8_lossy(&writer));
                set_error_code(format!("{}", res.status_code()).parse::<i16>().unwrap_or(0));
            }
        }
    }
}

/// Create a listener for channel `channel_name` of workspace `team_name`.
///
/// If you have not connected your workspace with [Flows.network platform](https://flows.network),
/// you will receive an error in the flow's building log or running log.
///
/// Before creating the listener, this function will revoke previous
/// registered listener of current flow so you don't need to do it manually.
///
/// `cb` is a callback function which will be called when the new [SlackMessage] is received.
pub async fn listen_to_channel<F, Fut>(team_name: &str, channel_name: &str, cb: F)
where
    F: FnOnce(SlackMessage) -> Fut,
    Fut: Future<Output = ()>,
{
    unsafe {
        match is_listening() {
            // Calling register
            1 => {
                let mut flows_user = Vec::<u8>::with_capacity(100);
                let c = get_flows_user(flows_user.as_mut_ptr());
                flows_user.set_len(c as usize);
                let flows_user = String::from_utf8(flows_user).unwrap();

                let mut flow_id = Vec::<u8>::with_capacity(100);
                let c = get_flow_id(flow_id.as_mut_ptr());
                if c == 0 {
                    panic!("Failed to get flow id");
                }
                flow_id.set_len(c as usize);
                let flow_id = String::from_utf8(flow_id).unwrap();

                let mut writer = Vec::new();
                let res = request::get(
                    format!(
                        "{}/{}/{}/listen?team={}&channel={}",
                        SLACK_API_PREFIX.as_str(),
                        flows_user,
                        flow_id,
                        team_name,
                        channel_name
                    ),
                    &mut writer,
                )
                .unwrap();

                match res.status_code().is_success() {
                    true => {
                        let output = format!(
                            "[{}] Listening to channel `{}` on workspace `{}`",
                            std::env!("CARGO_CRATE_NAME"),
                            channel_name,
                            team_name
                        );
                        set_output(output.as_ptr(), output.len() as i32);

                        if let Ok(sm) = serde_json::from_slice::<SlackMessage>(&writer) {
                            cb(sm);
                        }
                    }
                    false => {
                        write_error_log!(String::from_utf8_lossy(&writer));
                        set_error_code(
                            format!("{}", res.status_code()).parse::<i16>().unwrap_or(0),
                        );
                    }
                }
            }
            _ => {
                if let Some(sm) = message_from_channel() {
                    cb(sm).await;
                }
            }
        }
    }
}

fn message_from_channel() -> Option<SlackMessage> {
    unsafe {
        let l = get_event_body_length();
        let mut event_body = Vec::<u8>::with_capacity(l as usize);
        let c = get_event_body(event_body.as_mut_ptr());
        assert!(c == l);
        event_body.set_len(c as usize);
        match serde_json::from_slice::<Event>(&event_body) {
            Ok(e) => e.event,
            Err(_) => None,
        }
    }
}

/// Send message to channel `channel_name` of workspace `team_name`.
///
/// For now this function only support the
/// [text](https://api.slack.com/methods/chat.postMessage#arg_text) message.
///
/// If you have not connected your workspace with [Flows.network platform](https://flows.network),
/// you will receive an error in the flow's building log or running log.
pub async fn send_message_to_channel(team_name: &str, channel_name: &str, text: String) {
    unsafe {
        let mut flows_user = Vec::<u8>::with_capacity(100);
        let c = get_flows_user(flows_user.as_mut_ptr());
        flows_user.set_len(c as usize);
        let flows_user = String::from_utf8(flows_user).unwrap();

        let mut writer = Vec::new();
        if let Ok(res) = request::post(
            format!(
                "{}/{}/send?team={}&channel={}",
                SLACK_API_PREFIX.as_str(),
                flows_user,
                team_name,
                channel_name
            ),
            text.as_bytes(),
            &mut writer,
        ) {
            if !res.status_code().is_success() {
                write_error_log!(String::from_utf8_lossy(&writer));
                set_error_code(format!("{}", res.status_code()).parse::<i16>().unwrap_or(0));
            }
        }
    }
}

/// Upload a file to channel `channel_name` of workspace `team_name`.
///
/// `file_name` is the filename of the uploading file.
///
/// `file_type` refers to [file type](https://api.slack.com/types/file#file_types) of Slack.
///
/// `file_bytes` is file's raw bytes.
pub async fn upload_file(
    team_name: &str,
    channel_name: &str,
    file_name: &str,
    file_type: &str,
    file_bytes: Vec<u8>,
) {
    unsafe {
        let mut flows_user = Vec::<u8>::with_capacity(100);
        let c = get_flows_user(flows_user.as_mut_ptr());
        flows_user.set_len(c as usize);
        let flows_user = String::from_utf8(flows_user).unwrap();

        let boundary: String = rand::thread_rng()
            .sample_iter(&Alphanumeric)
            .take(15)
            .map(char::from)
            .collect();
        let boundary = format!("------------------------{}", boundary);

        if let Ok(file_part) =
            compose_file_part(&boundary, channel_name, file_name, file_type, file_bytes)
        {
            let mut writer = Vec::new();

            let uri = format!(
                "{}/{}/upload?team={}&channel={}",
                SLACK_API_PREFIX.as_str(),
                flows_user,
                team_name,
                channel_name
            );
            let uri = Uri::try_from(uri.as_str()).unwrap();
            if let Ok(res) = Request::new(&uri)
                .method(Method::POST)
                .header(
                    "Content-Type",
                    &format!("multipart/form-data; boundary={}", boundary),
                )
                .header("Content-Length", &file_part.len())
                .body(&file_part)
                .send(&mut writer)
            {
                if !res.status_code().is_success() {
                    write_error_log!(String::from_utf8_lossy(&writer));
                    set_error_code(format!("{}", res.status_code()).parse::<i16>().unwrap_or(0));
                }
            }
        }
    }
}

fn compose_file_part(
    boundary: &str,
    channel: &str,
    file_name: &str,
    file_type: &str,
    file_bytes: Vec<u8>,
) -> io::Result<Vec<u8>> {
    let mut data = Vec::new();
    write!(data, "--{}\r\n", boundary)?;
    write!(data, "Content-Disposition: form-data; name=\"channel\"\r\n")?;
    write!(data, "\r\n{}\r\n", channel)?;
    write!(data, "--{}\r\n", boundary)?;
    write!(
        data,
        "Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n",
        file_name
    )?;
    write!(data, "Content-Type: {}\r\n\r\n", file_type)?;

    data.extend_from_slice(&file_bytes);

    write!(data, "\r\n")?; // The key thing you are missing
    write!(data, "--{}--\r\n", boundary)?;

    Ok(data)
}