Skip to main content

rivescript/
lib.rs

1//! Implementation of the RiveScript chatbot scripting language.
2//!
3//! RiveScript is a simple scripting language designed for implementing
4//! chatbots that communicate with a user through plain language. This
5//! module provides an official RiveScript engine for Rust written by the
6//! language author.
7
8use crate::ast::AST;
9use crate::macros::proxy::Proxy;
10use log::{debug, warn};
11use rivescript_core::macros::{LanguageLoader, SubroutineResult};
12use rivescript_core::{sessions, parser::Parser};
13use std::{collections::HashMap, error::Error, fs, sync::Arc};
14use futures::future::BoxFuture;
15use Result::Ok;
16
17use rivescript_core::{DEFAULT_DEPTH, ast};
18mod errors;
19mod inheritance;
20mod macros;
21mod reply;
22mod sorting;
23mod tags;
24mod tests;
25mod utils;
26
27/// Rust library version.
28pub const VERSION: &str = "0.3.0";
29
30/// Loader for the JavaScript object macro parser (optional builtin feature).
31#[cfg(feature = "javascript")]
32pub fn register_default_js_handler(rs: &mut RiveScript) {
33    use rivescript_js::JavaScriptLoader;
34    rs.set_handler("javascript", JavaScriptLoader::new());
35}
36
37
38/// RiveScript represents a single chatbot personality in memory.
39pub struct RiveScript {
40    pub debug: bool,
41    pub utf8: bool,
42    pub depth: usize,
43    pub case_sensitive: bool,
44    unicode_punctuation: ::regex::Regex,
45
46    pub sessions: Arc<dyn sessions::SessionManager + Send + Sync>,
47    parser: Parser,
48    brain: AST,
49    sorted_topics: HashMap<String, Vec<ast::Trigger>>,
50    sorted_thats: HashMap<String, Vec<ast::Trigger>>,
51    sorted_subs: Vec<String>,
52    sorted_person: Vec<String>,
53    macro_handlers: HashMap<String, Box<dyn LanguageLoader>>,
54    subroutines: HashMap<String, macros::Subroutine>,
55    object_langs: HashMap<String, String>,
56
57    // Runtime (in-reply) variables.
58    in_reply_context: bool,
59    current_username: String,
60}
61
62impl RiveScript {
63    /// Initialize a new RiveScript chatbot personality.
64    ///
65    /// A single instance of RiveScript is able to have its own set of responses ("brain") independently
66    /// of other instances of RiveScript. Also, by default, RiveScript keeps track of temporary user
67    /// variables (such as recent reply history and any variables the bot has learned about them) at
68    /// in local memory of this instance, with each instance keeping its own separate data store.
69    pub fn new() -> Self {
70        Self {
71            debug: false,
72            utf8: false,
73            depth: DEFAULT_DEPTH,
74            case_sensitive: false,
75            unicode_punctuation: ::regex::Regex::new(r"[.,!?;:]").unwrap(),
76
77            sessions: Arc::new(sessions::memory::MemorySession::new()),
78            parser: Parser::new(),
79            brain: AST::new(),
80            sorted_topics: HashMap::new(),
81            sorted_thats: HashMap::new(),
82            sorted_subs: Vec::new(),
83            sorted_person: Vec::new(),
84            macro_handlers: HashMap::new(),
85            subroutines: HashMap::new(),
86            object_langs: HashMap::new(),
87
88            in_reply_context: false,
89            current_username: String::new(),
90        }
91    }
92
93    /// Replace the Unicode punctuation regexp when running with UTF-8 mode enabled.
94    ///
95    /// In UTF-8 mode, the user's message is (for the most part) left untouched, with only
96    /// backslashes and HTML angle brackets stripped. This can cause matching errors though
97    /// if common punctuation symbols were left intact, for example, a trigger that looks for
98    /// `+ hello bot` might not match the string "Hello bot." because of the period at the end.
99    ///
100    /// The default regexp is `[.,!?;:]` which matches common English punctuation symbols to be
101    /// removed. In case you need to customize this, you can provide your own regexp here.
102    pub fn set_unicode_punctuation(&mut self, re: ::regex::Regex) {
103        self.unicode_punctuation = re;
104    }
105
106    /// Replace the default in-memory User Variable Session manager with an alternative.
107    pub fn set_session_manager(&mut self, manager: impl sessions::SessionManager + Send + Sync + 'static) {
108        self.sessions = Arc::new(manager);
109    }
110
111    /// Load a directory of RiveScript documents (.rive or .rs extension) from a folder on disk.
112    /// Example
113    /// ```rust
114    /// # use rivescript::RiveScript;
115    /// # fn main() {
116    ///     let mut bot = RiveScript::new();
117    ///     bot.load_directory("../eg/brain").expect("Couldn't load directory!");
118    /// # }
119    /// ```
120    pub fn load_directory(&mut self, path: &str) -> Result<bool, Box<dyn Error>> {
121        debug!("load_directory called on: {}", path);
122
123        let paths = fs::read_dir(path)?;
124
125        for filename in paths {
126            let filepath = match filename {
127                Ok(res) => res.path(),
128                Err(err) => return Err(Box::new(err)),
129            };
130
131            match filepath.extension() {
132                Some(ext) => {
133                    if ext.eq_ignore_ascii_case("rive") || ext.eq_ignore_ascii_case(".rs") {
134                        self.load_file(filepath.as_path().display().to_string().as_str())?;
135                    }
136                }
137                None => continue,
138            }
139        }
140
141        return Ok(true);
142    }
143
144    /// Load a RiveScript document by filename on disk.
145    /// Example
146    /// ```rust
147    /// # use rivescript::RiveScript;
148    /// # fn main() {
149    ///     let mut bot = RiveScript::new();
150    ///     bot.load_file("../eg/brain/eliza.rive").expect("Couldn't load file from disk!");
151    /// # }
152    /// ```
153    pub fn load_file(&mut self, path: &str) -> Result<bool, Box<dyn Error>> {
154        debug!("load_file called on: {}", path);
155        let contents = fs::read_to_string(path)?;
156        self._stream(path, contents)
157    }
158
159    /// Stream a string containing RiveScript syntax into the bot, rather than read from the filesystem.
160    /// Example
161    /// ```rust
162    /// # use rivescript::RiveScript;
163    /// # fn main() {
164    ///     let mut bot = RiveScript::new();
165    ///     let code = String::from(
166    ///         "
167    ///         + hello bot
168    ///         - Hello, human!
169    ///         ",
170    ///     );
171    ///     bot.stream(code).expect("Couldn't parse code!");
172    /// # }
173    /// ```
174    pub fn stream(&mut self, source: String) -> Result<bool, Box<dyn Error>> {
175        self._stream("stream()", source)
176    }
177
178    // Internal, centralized funnel to load a RiveScript document.
179    fn _stream(&mut self, filename: &str, source: String) -> Result<bool, Box<dyn Error>> {
180        let ast = self.parser.parse(filename, source)?;
181        let objects = ast.objects.clone();
182        self.brain.extend(ast);
183
184        // In case the parse changed the depth variable, update it.
185        if let Ok(depth) = self.brain.get_global("depth").parse() {
186            self.depth = depth;
187        }
188
189        // Load all the parsed object macros.
190        for (name, object) in objects {
191            if !self.macro_handlers.contains_key(&object.language) {
192                debug!("Note: object macro '{}' is written in an unhandled language '{}'; skipping", name, object.language);
193                continue;
194            }
195
196            debug!("Loading object macro {} ({})", name, object.language);
197            let handler: &mut Box<dyn LanguageLoader> = self.macro_handlers.get_mut(&object.language).unwrap();
198            match handler.load(&name, object.code) {
199                Ok(_) => {
200                    // Store the language handler for this macro's name.
201                    self.object_langs.insert(name, object.language);
202                },
203                Err(e) => warn!("Error parsing object macro '{}': {}", name, e),
204            };
205        }
206
207        Ok(true)
208    }
209
210    /// Sort the internal data structures for optimal matching.
211    pub fn sort_triggers(&mut self) {
212        warn!("sort_triggers called, final AST is: {:#?}", self.brain);
213        match sorting::sort_triggers(&self.brain) {
214            Ok(result) => {
215                self.sorted_topics = result.topics;
216                self.sorted_thats = result.thats;
217                self.sorted_subs = result.subs;
218                self.sorted_person = result.person;
219            },
220            Err(_) => (),
221        }
222
223        // DEBUG
224        // debug!("sorted_topics: {:#?}", self.sorted_topics);
225        // debug!("sorted_thats: {:#?}", self.sorted_thats);
226        // debug!("sorted_subs: {:#?}", self.sorted_subs);
227        // debug!("sorted_person: {:#?}", self.sorted_person);
228    }
229
230    /// Get a reply from the chatbot.
231    pub async fn reply(&mut self, username: &str, message: &str) -> Result<String, String> {
232        // let msg = reply::Message{
233        //     username: String::from("username"),
234        // }
235        reply::reply(self, username, message).await
236    }
237
238    /// Define an object macro handler from a Rust function.
239    ///
240    /// This is a named function that you can call from RiveScript using the `<call>` tag. The parameters
241    /// to your function will be the RiveScript interpreter and the array of arguments (shell quote style)
242    /// passed in to the call.
243    ///
244    /// Example: `<call>example "hello world"</call>`
245    pub fn set_subroutine<F>(&mut self, name: &str, f: F)
246    where
247        F: for<'a> Fn(&'a mut Proxy<'a>, Vec<String>) -> BoxFuture<'a, Result<SubroutineResult, String>> + Send + Sync + 'static
248    {
249        self.subroutines.insert(name.to_string(), Box::new(f));
250    }
251
252    /// Set a handler for custom object macros written in other programming languages.
253    pub fn set_handler(&mut self, language: &str, loader: impl LanguageLoader + 'static) {
254        self.macro_handlers.insert(language.to_string(), Box::new(loader));
255    }
256
257    /// Get the current user's username.
258    ///
259    /// This is only valid from within a reply context, e.g. from a Rust object macro subroutine.
260    pub fn current_username(&self) -> Result<String, String> {
261        if !self.in_reply_context {
262            Err("current_username is only valid during a reply context".to_string())
263        } else {
264            Ok(self.current_username.to_string())
265        }
266    }
267
268    /// Set a user variable for a user.
269    ///
270    /// Equivalent to `<set name=value>` in RiveScript for the username.
271    pub async fn set_uservar(&self, username: &str, name: &str, value: &str) {
272        self.sessions.set(username, HashMap::from([
273            (name.to_string(), value.to_string()),
274        ])).await
275    }
276
277    /// Get a user variable from a user.
278    ///
279    /// Equivalent to `<get name>` in RiveScript.
280    ///
281    /// Returns the string "undefined" if not set.
282    pub async fn get_uservar(&self, username: &str, name: &str) -> String {
283        self.sessions.get(username, name).await
284    }
285
286    /// Set many user variables for a given user.
287    ///
288    /// With this function, you could restore a full set of user variables (e.g. which
289    /// were previously retrieved from `get_uservars`) by providing a full HashMap of
290    /// key/value pairs.
291    pub async fn set_uservars(&self, username: &str, vars: HashMap<String, String>) {
292        self.sessions.set(username, vars).await
293    }
294
295    /// Get all stored user variables for a given user.
296    pub async fn get_uservars(&self, username: &str) -> HashMap<String, String> {
297        self.sessions.get_any(username).await
298    }
299
300    /// Get all stored user variables about all users.
301    ///
302    /// This function may be most useful when using the default in-memory user variable storage.
303    /// It returns a HashMap of usernames paired to the HashMap of all of their data.
304    ///
305    /// If you are using a third-party storage driver (such as to use Redis or SQL), you
306    /// will probably not want to call this function in case it scrapes your entire table
307    /// end-to-end and returns ALL data about ALL users.
308    pub async fn get_all_uservars(&self) -> HashMap<String, HashMap<String, String>> {
309        self.sessions.get_all().await
310    }
311
312    /// Debugging: print the loaded bot's brain (AST) to console.
313    pub fn debug_print_brain(&self) {
314        println!("{:#?}", self.brain);
315    }
316
317    /// Debugging: print the sorted trigger lists.
318    pub fn debug_sorted_replies(&self) {
319        println!("{:#?}", self.sorted_topics);
320    }
321}