#chat-bot #twitch #plugin #documentation #read #plugin-loaders #streamable-plugin

nightly bot-rs-core

实现聊天机器人插件和插件加载器的核心库

2 个版本

0.4.4 2020年10月25日
0.4.3 2020年10月25日

#50 in #chat-bot

MIT 协议

82KB
1.5K SLoC

Bot-RS 核心库

Build Workflow Coverage Status

基准测试

用于实现构建机器人脚本的命令行工具 botrs 的插件和插件加载器。请阅读 Docs rs 中的文档以了解如何实现插件。 \r\n


lib.rs:

Bot-RS 核心库

此库用于实现 Bot-RS 平台的插件和插件加载器。

实现插件

有两种类型的插件:简单插件可流式插件。虽然 简单插件 拥有简单的接口,但 可流式插件 允许开发者以自定义方式处理传入的消息流并将其转换为传出消息流。这也意味着发送异步消息。而 简单插件 只对消息做出反应,可流式插件 可以随时发送消息。

以下各节将描述从创建 cargo 项目到构建库文件的整个过程。

实现简单插件

  1. 安装所需工具

  2. 使用以下命令创建 Cargo 项目(cargo new --lib --vcs git <项目名称>)。然后更改工作目录到该目录(cd hello-plugin)。

    cargo new --lib --vcs git hello-plugin
    cd hello-plugin
    
  3. bot-rs-core 的依赖添加到项目中。每个插件都必须实现 StreamablePlugin 特性。为了简化这个过程,"derive" 功能启用了 derive-macro,它可以从实现 Plugin 特性的结构体生成有效的 StreamablePlugin 实现代码。派生代码需要插件包包含对 futures crate 的依赖。我们也将添加这个依赖。由于 Plugin 特性包含异步函数,还需要对 async_trait crate 有依赖。为了处理来自 Twitch 的 (目前唯一支持的平台) irc 消息,还需要对 irc-rust crate 有依赖。为了能够记录消息,我们将使用 log crate 以及其 env_logger 实现来简化过程。

    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. 将以下片段添加到您的 Cargo.toml 中,以便将库编译为可加载的库文件,该文件将由插件加载器实现加载。

    [lib]
    crate-type = ["cdylib"]
    
  5. 现在我们可以实现实际的插件。将库根文件 src/lib.rs 的内容替换为以下内容

    // 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. (可选) 优化插件文件以减小文件大小,以减少通过 cargo build 产生的文件大小。为此,将以下片段复制到您的 Cargo.toml

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

    有关缩小 Rust 二进制文件/库大小的更多信息,请阅读此指南:

  7. 构建插件文件: cargo build --release

实现 StreamablePlugin

  1. 安装所需工具

  2. 使用以下命令创建 Cargo 项目(cargo new --lib --vcs git <项目名称>)。然后更改工作目录到该目录(cd hello-plugin)。

    cargo new --lib --vcs git hello-plugin
    cd hello-plugin
    
  3. bot-rs-core 的依赖添加到项目中。由于 StreamablePlugin 特性包含异步函数,还需要对 async_trait crate 有依赖。为了处理来自 Twitch 的 (目前唯一支持的平台) irc 消息,还需要对 irc-rust crate 有依赖。为了能够记录消息,我们将使用 log crate 以及其 env_logger 实现来简化过程。这次我们还将需要 futures crate 的依赖,用于我们的 StreamablePlugin 实现。

    cargo add bot-rs-core && \
    cargo add async-trait && \
    cargo add irc-rust && \
    cargo add log && \
    cargo add env_logger && \
    cargo add futures
    
  4. 将以下片段添加到您的 Cargo.toml 中,以便将库编译为可加载的库文件,该文件将由插件加载器实现加载。

    [lib]
    crate-type = ["cdylib"]
    
  5. 现在我们可以实现实际的插件。将库根文件 src/lib.rs 的内容替换为以下内容

    // 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. (可选) 优化插件文件以减小文件大小,以减少通过 cargo build 产生的文件大小。为此,将以下片段复制到您的 Cargo.toml

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

    有关缩小 Rust 二进制文件/库大小的更多信息,请阅读此指南:

  7. 构建插件文件: cargo build --release

插件加载器

目前尚未公开文档。目前唯一存在的实现是 botrs cli。要使用插件,需要此 CLI 工具。

依赖项

~6–16MB
~327K SLoC