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}