What is Serverless Reactor

Serverless Reactor aims to provide the developers of instant messaging apps such as Slack, Feishu, DingTalk, etc., with a development tool to quickly build and launch third-party applications. With Serverless Reactor, developers only need to focus on the business logic, saving the efforts to build application servers, set up domain name, store data and other cumbersome processes.

The program is distributed using WebAssembly (WASM), and the runtime uses WasmEdge-napi, which gives a wide language choice and ensures execution efficiency. The detailed process is as below:

  1. The developer writes each callback required by the Slack platform as a function
  2. The developer uploads the function code to the Serverless Reactor platform and receives a callback URL
  3. The developer fills in the callback URL where the Slack platform needs callback

Using Serverless Reactor to develop third-party applications on Slack has lower costs and higher efficiency. At the same time, we also provide a function demo so that developers can get started quickly.

Serverless Reactor currently only supports the bot application on Slack and Feishu 🤖.

Serverless Reactor Registration Instructions

Serverless Reactor right now is in its beta version and only needed to be registered with email. Serverless Reactor needs to be used together with Feishu, and developers need to register for Feishu account. If you already have a Feishu account, please proceed directly to the next step.

If the registration fails, please email reactor@secondstate.com, we will process your email as soon as possible.

Setting Up Serverless Reactor

For Video Tutorial: 👉 Every Step to Set Up Serverless Reactor

1. Create a new app on Slack

Go to Application section of Slack API. Click Create An App.

1

Select From Scratch.

2

Enter the name for the app and pick a workspace for your app. Then click Create App.

3

2. Set up OAuth

Go to Features > OAuth & Permissions on the sidebar.

4

Scroll down to Scopes. Set up Bot Token Scopes and User Token Scopes as follows:

Bot Token Scopes:
- chat:write
- files:read
- im:history
- im:read

User Token Scopes:
- users:read
- files:write

5

Go back to the top. Click Install to Workspace under OAuth Tokens for Your Workspace.

6

Click Allow to grant permissions.

7

Now you have User OAuth Token and Bot User OAuth Token. These tokens will be used later when setting up parameters on Serverless Reactor console.

8

3. Set up parameters on Serverless Reactor console

Log in to Serverless Reactor console. Go to Apps on the top bar. Click New App.

9

Fill in the name and description of your app under Name and Desc.

10

Fill in App ID and Verification Token. They can be found in Settings > Basic Information > App Credentials of your Slack app.

Fill in App Secret with Client Secret.

11

Fill in Slack User OAuth Token and Slack Bot User OAuth Token with the tokens we created in Step 2.

13

Click Save after setting up all the parameters.

Unfortunately, Welcome Message is currently not supported for Slack.

4. Code with RUST and compile into Wasm

Now you can write your RUST code and compile it into a Wasm file.

We have provided you with a coding template for this step. Simply fork this repo on GitHub and edit the RUST file in src/lib.rs. Then use rustwasmc build to compile your code into a Wasm file. The coding template implements a simple calculator.

Please refer to the repo's README.md for a detailed walk-through.

5. Upload Wasm to Serverless Reactor console

Go to Apps on Serverless Reactor console . Find your app and click Upload Wasm File.

14

Choose your local Wasm file to upload. Then, click Save.

15

6. Subscribe for events

Copy the service URL for Slack of your app from Serverless Reactor console. (Click For Slack on the Service URL column.)

16

Go to Features > Event Subscription on the sidebar. Turn on Enable Events.

17

Fill in Request URL. Request URL is the service URL you have copied appending /event to the end.

Example:
Service URL copied: 		https://slack.reactor.secondstate.info/60c774c6a15a7bc1e0b9a65d
Request URL to fill in: https://slack.reactor.secondstate.info/60c774c6a15a7bc1e0b9a65d/event

Expand Subscribe to bot events. Click Add Bot User Event and add message.im.

Click Save Changes.

18

7. Install app on Slack

Go to Features > App Home on the sidebar.

Turn on Always Show My Bot as Online.

19

Under Show Tabs > Message Tab, check Allow users to send Slash commands and messages from the messages tab.

20

Now your Slack app is ready to go!

8. Testing and updating your Slack app

You can test your app by sending messages and Slash commands to your app.

In the example below, our app is a calculator:

21

If you want to update your app, you can simply upload a new Wasm file to your app on Serverless Reactor console.

Serverless Reactor Wasm Developer Instructions

The target audience for this documentation, which includes all the info on developing Wasm applications for Serverless Reactor, is developers.

We recommend rust language for Wasm programs development, and the examples in this document are written with rust.

Reference Link:

👉 Serverless Reactor sample project

👉 Register on Serverless Reactor

👉 Serverless Reactor Platform Guide

Wasm Interface

According to the use cases of Slack bots, we define the following 5 functions for Wasm program to communicate with Host program. The 5 functions are as follows:

  • text_received
  • image_received
  • expect_text
  • expect_image
  • initiate

If you implement a simple question-and-answer chat bot, you only need to implement the first text_received function.

For quick start template functions, please go to Serverless Reactor Starter github repo

text_received

text_received(msg: String, user_info: String, step_data: String) -> String

When the user sends a text message to the bot, the Host will call Wasm's text_received function, parse the return value, and return the result to the user.

Parameters

  • msg - The original text message sent by the user
  • user_info - Slack user information, Json string {open_id: "Slack User Id", tenant_key: "Slack Workspace Identity", "name": "User Name"}
  • step_data - The step data of the previous chat between the user and the bot

image_received

image_received(img_buf: Vec<u8>, image_key: String, user_info: String, step_data: String) -> String

When the user sends an image message to the bot, the Host will call Wasm's image_received function, parse the return value, and return the result to the user.

Parameters

  • img_buf - The binary string of the original picture sent by the user
  • image_key - Feishu image_key corresponding to the picture sent by the user, which can be stored in the step information for subsequent use in a multi-step session
  • user_info - Slack user information, Json string {open_id: "Slack User Id", tenant_key: "Slack Workspace Identity", "name": "User Name"}
  • step_data - The step data of the previous chat between the user and the bot

expect_text

expect_text(img_buf: Vec<u8>, user_info: String, step_data: String, resp_index: i32) -> String

When the bot needs to call Wasm's functions multiple times in a reply to the user, except for the first call to text_received or image_received,it will then decide whether to call expect_text or expect_image based on the return information of the first call.

Parameters

  • img_buf - The binary string of the original picture sent by the user
  • user_info - Slack user information, Json string {open_id: "Slack User Id", tenant_key: "Slack Workspace Identity", "name": "User Name"}
  • step_data - The step data of the previous chat between the user and the bot
  • resp_index - expect_text or expect_image being called multiple times’ order index

expect_image

expect_image(img_buf: Vec<u8>, user_info: String, step_data: String, resp_index: i32) -> Vec<u8>

When the bot needs to call Wasm's functions multiple times in a reply to the user, except for the first call to text_received or image_received,it will then decide whether to call expect_text or expect_image based on the return information of the first call.

Parameters

  • img_buf - The binary string of the original picture sent by the user
  • user_info - Slack user information, Json string {open_id: "Slack User Id", tenant_key: "Slack Workspace Identity", "name": "User Name"}
  • step_data - The step data of the previous chat between the user and the bot
  • resp_index - expect_text or expect_image being called multiple times’ order index

Return Value

The 3 functions: text_received, image_received and expect_text all return strings. The simplest return content can be the content returned to the user, such as the calculator application, the user enters 1 + 1, then text_received only needs to return "2".

In addition, the returned string can also be a Json String:

{
  "next": [{"type": "image", "require_image": "img_key_*****"}, {"type": "text"}],
  "result": "to be sent to Slack",
  "receiver": "Slack user open_id",
  "step": "",
  "new_step": true | false
}

Fields Explained

  • next In a reply from the bot to the user, the Wasm function still needs to be called, because it may have to reply to multiple messages from the user one by one, to get each and every message it needs to be called a time. If there are no pictures among the multiple pieces of messages, that is, text only, next can also be empty, and the result is represented with array for multiple pieces of messages.

In the example, when the Host receives "next": [{"type": "image", "require_image": "img_key_..."}, {"type": "text"}] , it will call expect_image and then expect_text. require_image means the corresponding expect_? functions require picture. In the example, the host will get image_key_... ‘s corresponding binary string to pass to expect_image.

  • result

    This call needs to reply to the user's text message. Empty means that no information will be returned to the user. If it is an array ["message 1", "message 2"], it means to reply to the user with two messages. The reply to the user can also be an object, for example, sending an image to the user through image_key like {"msg": "image_key", "type": "image"}.

  • receiver The Slack user that needs to be replied. No return means replying to the object chatting with the bot.

  • step It can be information of any structure, and it can be included only in the last function return of a reply. Details.

  • new_step Tell Host whether to record a new step

Note:Host will ignore the "next" and "new_step" fields returned by expect_text

About step

step is used to store historical data of the conversation. Since the Wasm file does not provide a way to store historical data, we need to use step if a task requires information from multiple messages in a conversation.

Every time Host calls functions in Wasm, all the functions except expect_image will return a step value. Host will store step and include it as a parameter in the following function calls. In this way, Wasm can get all the necessary information in previous messages.

Examples

  • JSON Formatter: This example exhibits the basic functionality of Serverless Reactor
  • Multi-Step Calculator: This example shows how to create stateful functions
  • AI Classification: This example shows how to use TensorFlow to create functions for your bot
  • External API Calls: This example shows how to use an external API to build a currency converter bot
  • Picture Resizing: This example shows how to build a bot that resizes images sent by the user

JSON Formatter

This is a bot for formatting JSON. This bot will return a formatted JSON string when a user sends an unformatted JSON string.


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use serde_json::{Value};

#[wasm_bindgen]
pub fn text_received(msg: String, _user_info: String, _step_data: String) -> String {
  let obj: Value = match serde_json::from_str(&msg) {
    Ok(v) => v,
    Err(e) => return format!("{:?}", e)
  };
  let str = serde_json::to_string_pretty(&obj).unwrap();
  return format!(r#"{{"result": "{}"}}"#, str.replace("\"", "\\\""));
}
}

The bot uses serde_json to format JSON strings. There is no need for too much code.

One thing to be noted is that you should not return the JSON string directly, but should return it as { "result": "JSON string to return" }. Only in this way can the Host parse out the correct content to return.

Furthermore, the code did not use _step_data because there is no need to cache previous chat messages. The only message that matters is the last message sent by the user that contains an unformatted JSON.

Multi-Step Calculator

This example implements a multi-step calculator bot. Users can send arithmetic expressions in multiple consecutive messages and get the final result. For instance, if the user sends 1+1, the bot will return 2. The user can then send *3, and the bot will return 6.

Refer to multi-step-calculator for a complete code example.


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use meval;

#[wasm_bindgen]
pub fn text_received(msg: String, _user_info: String, step_data: String) -> String {
  if msg == "#" {
    return format!(r#"{{"new_step": true}}"#);
  } else {
    let exp = match step_data == ""{
      true => msg,
      _ => format!("({}){}", step_data, msg)
    };
    let x = meval::eval_str(&exp).unwrap();
    return format!(r#"{{"result": "{}", "step": "{}"}}"#, x, exp);
  }
}
}

The example code uses the meval crate for calculation. It also uses step because the bot required multi-step calculation. The return JSON value from every function call will include all the previous arithmetic expressions in step_data.

AI Classification

We can implement an image-recognition bot based on wasmedge-tensorflow.

Refer to food-classification for a complete code example.


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use ssvm_tensorflow_interface;

#[wasm_bindgen]
pub fn text_received(_msg: String, _user_info: String, _step_data: String) -> String {
  "Please send a food picture (must be JPEG)".to_string()
}
#[wasm_bindgen]
pub fn image_received(img_buf: Vec<u8>, _image_key: String, _user_info: String, _step_data: String) -> String {
  let model_data: &[u8] = include_bytes!("lite-model_aiy_vision_classifier_food_V1_1.tflite");
  let labels = include_str!("aiy_food_V1_labelmap.txt");

  let flat_img = ssvm_tensorflow_interface::load_jpg_image_to_rgb8(&img_buf, 192, 192);

  let mut session = ssvm_tensorflow_interface::Session::new(&model_data, ssvm_tensorflow_interface::ModelType::TensorFlowLite);
  session.add_input("input", &flat_img, &[1, 192, 192, 3])
         .run();
  let res_vec: Vec<u8> = session.get_output("MobilenetV1/Predictions/Softmax");

  let mut i = 0;
  let mut max_index: i32 = -1;
  let mut max_value: u8 = 0;
  while i < res_vec.len() {
      let cur = res_vec[i];
      if cur > max_value {
          max_value = cur;
          max_index = i as i32;
      }
      i += 1;
  }

  let mut confidence = "possible";
  if max_value > 200 {
      confidence = "very likely";
  } else if max_value > 125 {
      confidence = "likely";
  } else if max_value > 50 {
      confidence = "possible";
  }

  let mut label_lines = labels.lines();
  for _i in 0..max_index {
    label_lines.next();
  }

  let class_name = label_lines.next().unwrap();

  if max_value > 50 {
    return format!("It is {} that the uploaded picture contains {}." , confidence.to_string(), class_name);
  } else {
    return format!("No food detected in the uploaded picture");
  }
}
}

When the user uploaded a picture of food, the bot will recognize and return the name of the food.

No step is used since there is no need to use all the historical messages. text_received is defined only to prompt the user to send pictures, and the function logic is defined in image_received.

In this example,the TensorFlow model comes from our starter program

External API Calls

Starting from Version 0.7.3, WasmEdge provides the capability of making system calls. For safety reasons, we only support http_proxy in our service. We can make requests to external APIs using http_proxy to expand the functionality of our bot.

Refer to currency-converter for a complete code example.


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use serde_json::Value;
use meval;
use std::str;
use ssvm_process_interface::Command;

#[wasm_bindgen]
pub fn text_received(msg: String, _user_info: String, _step_data: String) -> String {
  let v: Vec<&str> = msg.split_whitespace().collect();
  let mut v1 = v[1].to_string();
  v1.make_ascii_uppercase();
  let mut v3 = v[3].to_string();
  v3.make_ascii_uppercase();
  let pair = v1 + "_" + &v3;

  let mut cmd = Command::new("http_proxy");
  // [currencyconverterapi key] comes from https://free.currencyconverterapi.com
  cmd.arg("get")
    .arg(format!("https://free.currconv.com/api/v7/convert?q={}&compact=ultra&apiKey=[currencyconverterapi key]", pair))
    .stdin_u8vec("".as_bytes());

  let out = cmd.output();
  if out.status != 0 {
      println!("Code: {}", out.status);
      println!("STDERR: {}", str::from_utf8(&out.stderr).unwrap());
      println!("STDOUT: {}", str::from_utf8(&out.stdout).unwrap());
      return str::from_utf8(&out.stderr).unwrap().to_string();
  }

  let cur = str::from_utf8(&out.stdout).unwrap();
  let cur: Value = serde_json::from_str(cur).unwrap();
  let cur = cur[pair].as_f64().unwrap();

  return format!(
    r#"{{"result": "{}", "step": "{}"}}"#,
    meval::eval_str(v[0].to_owned() + "*" + &cur.to_string()).unwrap(),
    ""
  );
}
}

Using http_proxy, this program retrieves real-time currency exchange rate from https://free.currencyconverterapi.com. It then uses meval to calculate the converted money value and return it to the Host.

Picture Resizing

This example shows that a bot can generate an image and send it to the user. We used expect_image here.

Refer to resize-picture for a complete code example.


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use serde_json::{json, Value};
use image;
use image::{GenericImageView};

#[wasm_bindgen]
pub fn text_received(msg: String, _user_info: String, step_data: String) -> String {
  let mut last_step_data = match step_data.len() > 0 {
    true => serde_json::from_str(step_data.as_str()).unwrap(),
    false => return String::from("Please first send a picture 请先发送一张图片")
  };

  let width: u32 = match msg.parse().ok() {
    Some(width) => width,
    None => return String::from("Please send a valid number 请回复数字")
  };
  extend_step_data(&mut last_step_data, vec!(
    (String::from("width"), json!(width))
  ));
  let step = Some(format!("{}", last_step_data));

  let next = Some(vec!(
    json!({"type": "image", "require_image": last_step_data["pic"]})
  ));

  return format_resp(next, None, None, step, false);
}

#[wasm_bindgen]
pub fn image_received(_img_buf: Vec<u8>, image_key: String, _user_info: String, _step_data: String) -> String {
  let step_data = json!({
    "pic": image_key
  });
  let step = Some(format!("{}", step_data));
  let resp_msg = Some(vec!(Value::String(String::from("How wide do you want the resized picture to be? 你想把图片改成多宽?"))));
  format_resp(None, resp_msg, None, step, true)
}

#[wasm_bindgen]
pub fn expect_image(img_buf: Vec<u8>, _user_info: String, step_data: String, _resp_index: i32) -> Vec<u8> {
  let last_step_data: Value = serde_json::from_str(step_data.as_str()).unwrap();
  let x = resize_pic(
    &img_buf,
    last_step_data["width"].as_u64().unwrap() as u32,
  );
  return x;
}

fn format_resp(next: Option<Vec<Value>>, result: Option<Vec<Value>>, receiver: Option<&str>, step: Option<String>, new_step: bool) -> String {
  let resp = json!({
    "next": next,
    "result": result,
    "receiver": receiver,
    "step": step,
    "new_step": new_step
  });

  return format!("{}", resp);
}

fn extend_step_data(step_data: &mut Value, items: Vec<(String, Value)>) {
  let m = step_data.as_object_mut().unwrap();
  m.extend(items);
}

fn resize_pic(img_buf: &[u8], width: u32) -> Vec<u8> {
  let img = image::load_from_memory(img_buf).unwrap();
  let height = ((img.height() as f32 / img.width() as f32) * width as f32) as u32 + 100;
  let img = img.resize(width, height, image::imageops::FilterType::Triangle);
  let mut buf = vec![];
  img.write_to(&mut buf, image::ImageOutputFormat::Png).unwrap();
  return buf;
}
}

The user should first send the original picture to the bot.

Upon receiving the picture, image_received will store image_key to step. Then the user can send a desired width, and text_received will store this value to step as well. It will also return a next field to tell Host to call expect_image for the image after resizing.

The first parameter of expect_image , img_buf, is information of the picture in a binary string that Host requested from Slack with image_key of require_image in next.

Frequently Asked Questions

  1. What languages are supported?

For now, the best supported language is Rust. We also support C and C++.

  1. Do I need to pay for Severless Reactor?

Free-lance developers can use Severless Reactor free of charge. App developers should contact us at reactor@secondstate.com before uploading the app to an app store.

  1. What do I need to install before using Serverless Reactor?

If you are coding with Rust, please install Rust and rustwasmc beforehand. rustwasmc compiles Rust codes into wasm files.