#chat-bot #twitch

nightly bot-rs-core

Core library for implementing plugins and plugin-loaders for chatbots

2 releases

0.4.4 Oct 25, 2020
0.4.3 Oct 25, 2020

#60 in #twitch

MIT license

82KB
1.5K SLoC

Bot-RS Core library

Build Workflow Coverage Status

Benchmarks

Used to implement plugins and plugin-loaders for the botrs cli tool to build bots. Please read the documentation at Docs rs for instructions to implement a plugin. \r\n


lib.rs:

Bot-RS Core

This library is used to implement Plugins and Plugin-Loaders for the Bot-RS platform.

Implementing a Plugin

There are two kinds of plugins: Simple Plugin and Streamable Plugin. While the Simple Plugin has a simple interface the Streamable Plugin enables the developer to implement a custom way to handle the incoming Stream of Messages and convert it into a Stream of outgoing messages. This also means sending asynchronous messages. While Simple Plugins only react to messages, Streamable Plugins can sand messages at any time.

The process from creating the cargo project to building the library files will be described in the following sections.

Implementing a Simple Plugin

  1. Install required tools:

  2. Create the Cargo project with cargo new --lib --vcs git <project name>. And change your working directory to it (cd hello-plugin).

    cargo new --lib --vcs git hello-plugin
    cd hello-plugin
    
  3. Add the dependency of bot-rs-core to the project. Every plugin has to implement the StreamablePlugin trait. To simplify this the "derive" feature enables the derive-macro which generates a valid StreamablePlugin implementation from a struct which implements the Plugin trait. The derived code requires the plugin crate to have the dependency to the futures crate. Which we'll also add. As the Plugin trait contains async functions it's also required to have an dependency to the async_trait crate. To handle irc messages coming from twitch (only supported platform yet) a dependency to the irc-rust crate is also required. To also be able to log messages we'll use the log crate with its env_logger implementation for simplicity.

    cargo add bot-rs-core --features derive && \
    cargo add futures && \
    cargo add async-trait && \
    cargo add irc-rust && \
    cargo add log && \
    cargo add env_logger
    
  4. Add the following snippet to your Cargo.toml to compile the library to a loadable library file which will be loaded by a plugin-loader implementation.

    [lib]
    crate-type = ["cdylib"]
    
  5. Now we can implement the actual plugin. Replace the contents of the library root file src/lib.rs with the following content:

    // Simple logging facade
    #[macro_use]
    extern crate log;
    
    // Enables the derive-macro for StreamablePlugin.
    #[macro_use]
    extern crate bot_rs_core;
    
    use async_trait::async_trait;
    
    use bot_rs_core::Message;
    use bot_rs_core::plugin::{StreamablePlugin, Plugin, InvocationError, PluginInfo, PluginRegistrar};
    use std::sync::Arc;
    use bot_rs_core::profile::Profile;
    
    // Reacts to an invocation of `!hello` with `Hello, @<sender name>!`.
    #[derive(StreamablePlugin)]
    struct HelloPlugin {
        profile: Profile
    }
    
    #[async_trait::async_trait]
    impl Plugin for HelloPlugin {
        async fn call(&self, msg: Message) -> Result<Vec<Message>, InvocationError> {
            // Check AccessRights before processing
            if let Some(false) = self.profile.rights().allowed(&msg) {
                // Only process messages not handled by filters or allowed
                return Ok(Vec::with_capacity(0));
            }
            match &msg {
                Message::Irc(irc_message) => {
                    match irc_message.get_command() {
                        "PRIVMSG" => {
                            let params = irc_message
                                .params()
                                .expect("missing params in PRIVMSG");
                            // First param in a PRIVMSG is the channel name
                            let channel = params.iter()
                                .next()
                                .expect("no params in PRIVMSG");
                            let trailing = params.trailing;
                            if trailing.is_none() {
                                // Return no messages
                                return Ok(Vec::with_capacity(0));
                            }
                            let trailing = trailing.unwrap();
    
                            // Check if '!hello' command was called
                            if !trailing.starts_with("!hello") {
                                return Ok(Vec::with_capacity(0));
                            }
    
                            let prefix = irc_message.prefix().expect("missing prefix in PRIVMSG");
                            let name = prefix.name();
    
                            Ok(vec![Message::Irc(irc_rust::Message::builder("PRIVMSG")
                                .param(channel)
                                .trailing(&format!("Hello, @{}!", name))
                                .build()
                                .expect("failed to build irc message")
                            )])
                        }
                    }
                }
            }
        }
    
        // Return information about the plugin for identification.
        fn info(&self) -> PluginInfo {
            PluginInfo {
                name: "Hello Plugin".to_string(),
                version: env!("CARGO_PKG_VERSION").to_string(),
                authors: env!("CARGO_PKG_AUTHORS").to_string(),
                repo: option_env!("CARGO_PKG_REPOSITORY")
                    .map(|repo| if repo.is_empty() { "No repo".to_string() } else { repo.to_string() }),
                commands: vec!["!hello".to_string()]
            }
        }
    }
    
    // This macro creates a static field which can be loaded by the plugin loader.
    bot_rs_core::export_command!(register);
    
    // The plugin loading mechanism uses this function for load and register. Initializing loggers and other dependencies has to be done here.
    extern "C" fn register(registrar: &mut PluginRegistrar) {
        env_logger::init();
        // Is set on startup by Bot-RS CLI Tool
        let profile = Profile::active().unwrap();
        registrar.register(Arc::new(HelloPlugin{ profile }))
    }
    
  6. (Optional) Optimize plugin file for size to reduce the file size produced through cargo build. To do this copy the following snippet to your Cargo.toml:

    [profile.release]
    lto = true
    codegen-units = 1
    opt-level = "z"
    

    For more infos read this guide on reducing the size of rust binaries/libraries: .

  7. Building the plugin file: cargo build --release

Implementing a StreamablePlugin

  1. Install required tools:

  2. Create the Cargo project with cargo new --lib --vcs git <project name>. And change your working directory to it (cd hello-plugin).

    cargo new --lib --vcs git hello-plugin
    cd hello-plugin
    
  3. Add the dependency of bot-rs-core to the project. As the StreamablePlugin trait contains async functions it's also required to have an dependency to the async_trait crate. To handle irc messages coming from twitch (only supported platform yet) a dependency to the irc-rust crate is also required. To also be able to log messages we'll use the log crate with its env_logger implementation for simplicity. This time we'll also require the dependency to the futures crate for our StreamablePlugin implementation.

    cargo add bot-rs-core && \
    cargo add async-trait && \
    cargo add irc-rust && \
    cargo add log && \
    cargo add env_logger && \
    cargo add futures
    
  4. Add the following snippet to your Cargo.toml to compile the library to a loadable library file which will be loaded by a plugin-loader implementation.

    [lib]
    crate-type = ["cdylib"]
    
  5. Now we can implement the actual plugin. Replace the contents of the library root file src/lib.rs with the following content:

    // Simple logging facade
    #[macro_use]
    extern crate log;
    extern crate futures;
    
    // Enables the derive-macro for StreamablePlugin.
    #[macro_use]
    extern crate bot_rs_core;
    
    use async_trait::async_trait;
    
    use bot_rs_core::Message;
    use bot_rs_core::plugin::{StreamablePlugin, Plugin, InvocationError, PluginInfo, PluginRegistrar};
    use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
    use std::sync::Arc;
    use futures::{StreamExt, SinkExt};
    
    /// Reacts to an invocation of `!hello` with `Hello, @<sender name>!`.
    struct HelloPlugin;
    
    // For simplicity we'll be using the Plugin implementation showed in the previous section. But we'll implement the `StreamablePlugin` ourself this time.
    
    // <insert Plugin implementation of previous section here>
    
    // This implementation doesn'T require spawning new threads. This should be handled by the plugin-loader.
    #[async_trait]
    impl StreamablePlugin for HelloPlugin {
        async fn stream(&self,
            mut input: UnboundedReceiver<Message>,
            mut output: UnboundedSender<Vec<Message>>)
        -> Result<(), InvocationError> {
            // Read next message from input channel
            while let Some(msg) = input.next().await {
                // Call out Plugin implementation
                let results = self.call(msg).await?;
                // Send the results to the output channel
                output.send(results)
                    .await.expect("failed to send results to output");
           }
           Ok(())
        }
    
        // Return information about the plugin for identification.
        fn info(&self) -> PluginInfo {
            Plugin::info(self)
        }
    }
    
    // This macro creates a static field which can be loaded by the plugin loader.
    export_command!(register);
    
    // The plugin loading mechanism uses this function for load and register. Initializing loggers and other dependencies has to be done here.
    extern "C" fn register(registrar: &mut PluginRegistrar) {
        env_logger::init();
        registrar.register(Arc::new(HelloPlugin))
    }
    
  6. (Optional) Optimize plugin file for size to reduce the file size produced through cargo build. To do this copy the following snippet to your Cargo.toml:

    [profile.release]
    lto = true
    codegen-units = 1
    opt-level = "z"
    

    For more infos read this guide on reducing the size of rust binaries/libraries: .

  7. Building the plugin file: cargo build --release

Plugin-Loader

This is currently not publicly documented. The only implementation currently present is the botrs cli. To use the plugins this cli tool is required.

Dependencies

~7–18MB
~353K SLoC